Приложения с интерфейсом в виде командной строки (Command-Line Interface — CLI) стали популярными в экосистеме разработчиков по целому ряду причин. Самые банальные из них — это простота использования (CLI) и то, что многие важнейшие инструменты разработки представляют из себя терминальные приложения или предоставляют интерфейс командной строки, и многие разработчики уже к ним привыкли.
По мере того, как сложность и функциональность этих терминальных приложений растет, также растет и потребность в простоте и быстроте их создания.
В нашей предыдущей статье мы рассказали вам о том, как создать CLI-приложение с помощью Node.js. Один из ключевых выводов после построения интерфейса командной строки с использованием Node.js заключался в том, что делать это достаточно сложно и утомительно.
React же упрощает создание мощных и очень интерактивных CLI-приложений. В этой статье мы реализуем командную строку с помощью React.js вместо Node.js и увидим разницу.
Что от вас требуется:
Базовое понимание JavaScript.
Базовые знания React.js.
Базовые знания npm и/или yarn.
Почему React.js, а не Node.js?
React освобождает нас от всех хлопот по синтаксическому анализу (парсингу) аргументов, выполняя их в фоновом режиме. React также позволяет рендерить компоненты в терминале, так как вы бы делали это в браузере.
Чтобы реализовать интерфейс командной строки с помощью React, мы будем использовать библиотеку под названием INK, которая значительно упростит нашу работу. Ink также позволяет вам использовать flexbox, то есть нам больше не нужно полагаться на раскрашенные стринговые выводы, как в Node.js.
Для примера вот несколько популярных приложений, сделанных с помощью React и Ink:
Jest
Gatsby
Prisma
Typescript
Twilio SIGNAL
Начало работы с React INK
Ink — это фреймворк React.js, который значительно упрощает утомительную задачу создания CLI-приложений. По сравнению с Node.js Ink не требует от вас особого обучения работе с ним. Если вы знакомы с React, тогда вы готовы.
Начнем с создания простого Hello World приложения. Для этого нам понадобятся React и Ink из нашего npm. Чтобы упростить нашу работу, ink поставляется с командой для бутстрапа React CLI-приложения.
Введите в терминале:
mkdir section-example && cd section-example
npx create-ink-app
Последняя команда создает исполняемый файл для нашего приложения. Для завершения этого процесса может потребоваться некоторое время. Когда вы запустите node cli в терминале, он должен вернуть следующее:
Вот она, ваша первая командная строка на основе React. Чтобы добиться этого в Node.js, нам потребовалось бы написать много кода, и это заняло бы больше времени, не говоря уже о большем количестве задействованных библиотек.
Простой проект
Давайте продолжим работу над чуть более сложным проектом — это поможет вам понять элементы и структуру проекта React ink. Мы будем работать в файле ui.js. Файл входа для приложения — cli.js..
Код будет выглядеть примерно так:
"use strict";
const React = require("react");
const { Text } = require("ink");
const App = ({ name = "Stranger" }) => (
<Text>
Hello, <Text color="green">{name}</Text>
</Text>
);
module.exports = App;
Сначала мы импортируем React из пакета react. Затем мы импортируем элемент Text, который поставляется с пакетом ink. У нас также есть функция, которая принимает на вход имя и рендерит его. Давайте создадим простое CLI-приложение, которое принимает страну в качестве входных данных. Затем оно возвращает некоторую информацию об этой стране в виде таблицы.
Для этого нам понадобится пакет npm под названием world-countries-capitals, который содержит информацию о странах.
Начнем с ввода данных пользователем. Для этого нам понадобится ввод текста (text input). К счастью для нас, ink уже предоставляет для этого пакет. Просто запустите:
npm install ink-text-input
Мы импортируем и используем ввод текста в терминале в нашем ui.js. Мы также будем использовать хук React useState
для хранения значения нашей страны и обработки изменений ее названия. Проще говоря, думайте о хуках useState
как о способе работы с переменными в React. Чтобы узнать больше о хуках React, я рекомендую почитать эту документацию.
Теперь наш код будет выглядеть так:
"use strict";
const React = require("react");
const { Box } = require("ink");
const TextInput = require("ink-text-input").default;
const App = () => {
const [country, setCountry] = React.useState("");
return (
<Box>
<TextInput
placeholder="Enter your country..."
value={country}
onChange={setCountry}
/>
</Box>
);
};
module.exports = App;
При запуске node cli
в терминале, у вас должна появиться возможность ввести название страны.
Нам нужно будет искать страну в режиме реального времени и отображать результаты в таблице. Для этого мы вызовем npm-пакет world-countries-capitals. Мы будем использовать еще один хук React useEffect
для извлечения наших данных и обновления компонента по мере его рендеринга. Давайте реализуем это.
Сначала установим и импортируем пакет.
Введите в терминале:
npm i world-countries-capitals
Мы импортируем этот пакет вверху нашего файла:
const wcc = require("world-countries-capitals");
Мы создадим несколько переменных для хранения данных, которые мы получаем от хука useEffect
, используя useState
. Они пригодятся при обновлении таблицы в реальном времени.
const [capital, setCapital] = React.useState("");
const [currency, setCurrency] = React.useState("");
const [phone, setPhone] = React.useState("");
Наконец, мы заполняем наши переменные информацией из npm-пакета. В итоге наш хук useEffect
будет выглядеть так:
React.useEffect(() => {
const getCountry = wcc.getCountryDetailsByName(country);
setCapital(getCountry[0].capital);
setCurrency(getCountry[0].currency);
setPhone(getCountry[0].phone_code);
});
На данный момент наш код, включая хук useEffect
, будет выглядеть следующим образом:
"use strict";
const React = require("react");
const { Box } = require("ink");
const TextInput = require("ink-text-input").default;
const wcc = require("world-countries-capitals");
const App = () => {
const [country, setCountry] = React.useState("");
const [capital, setCapital] = React.useState("");
const [currency, setCurrency] = React.useState("");
const [phone, setPhone] = React.useState("");
React.useEffect(() => {
const getCountry = wcc.getCountryDetailsByName(country);
setCapital(getCountry[0].capital);
setCurrency(getCountry[0].currency);
setPhone(getCountry[0].phone_code);
});
return (
<Box>
<TextInput
placeholder="Enter your country..."
value={country}
onChange={setCountry}
/>
</Box>
);
};
module.exports = App;
Наконец, давайте отобразим информацию в таблице. Нам нужно будет вложить друг в друга много боксов с некоторыми атрибутами. Наиболее распространенными атрибутами будут flex-direction
и borderStyle
. Поскольку мы используем React, мы все еще оперируем в реалиях JSX, и нам нужен родительский атрибут.
Внутри элемента Box
под элементом TextBox
мы и добавим нашу таблицу:
<Box flexDirection="column" width={80} borderStyle="single">
<Box>
<Box width="40%">
<Text>Country Code</Text>
</Box>
<Box width="40%">
<Text>Capital City</Text>
</Box>
<Box width="40%">
<Text>Currency</Text>
</Box>
</Box>
<Box>
<Box width="40%">
<Text>{phone}</Text>
</Box>
<Box width="40%">
<Text>{capital}</Text>
</Box>
<Box width="40%">
<Text>{currency}</Text>
</Box>
</Box>
</Box>
Давайте добавим баннер в наше приложение. Просто потому, что мы можем. Мы добавим его в корневой элемент Box поверх ввода текста.
<Box borderStyle="round" borderColor="green">
<Text>Welcome to Country CLI</Text>
</Box>
Готово.
Теперь наш код выглядит так:
"use strict";
const React = require("react");
const { Text, Box } = require("ink");
const TextInput = require("ink-text-input").default;
const wcc = require("world-countries-capitals");
const App = () => {
const [country, setCountry] = React.useState("");
const [capital, setCapital] = React.useState("");
const [currency, setCurrency] = React.useState("");
const [phone, setPhone] = React.useState("");
React.useEffect(() => {
const getCountry = wcc.getCountryDetailsByName(country);
setCapital(getCountry[0].capital);
setCurrency(getCountry[0].currency);
setPhone(getCountry[0].phone_code);
});
return (
<Box flexDirection="column">
<Box borderStyle="round" borderColor="green">
<Text>Welcome to Country CLI</Text>
</Box>
<TextInput
placeholder="Enter your country..."
value={country}
onChange={setCountry}
/>
<Box flexDirection="column" width={80} borderStyle="single">
<Box>
<Box width="40%">
<Text>Country Code</Text>
</Box>
<Box width="40%">
<Text>Capital City</Text>
</Box>
<Box width="40%">
<Text>Currency</Text>
</Box>
</Box>
<Box>
<Box width="40%">
<Text>{phone}</Text>
</Box>
<Box width="40%">
<Text>{capital}</Text>
</Box>
<Box width="40%">
<Text>{currency}</Text>
</Box>
</Box>
</Box>
</Box>
);
};
module.exports = App;
Чтобы протестировать наше творение, запустим в нашем терминале node cli
.
Он должен вернуть это:
Вы можете посмотреть гифку работы приложения по этой ссылке ссылке.
Примечание: запуск тестовой команды (npm run test) не сработает, потому что мы не написали никаких тестов. Ink по умолчанию использует для тестирования ava. Вы можете прочитать больше об ava в этой документации.
Заключение
Мы только что создали наш первый более-менее сложный интерфейс командной строки с использованием React, и вот несколько моментов, на которые следует обратить внимание. Ink содержит много элементов, позволяющих лучше контролировать пользовательский интерфейс командной строки. Он также поставляется с настраиваемыми хуками для управления данными, полученными с терминала. Хорошим примером является useInput
, который слушает пользовательский ввод.
Создавать CLI-приложения еще никогда не было так просто. Получайте удовольствие, создавая более сложные и красивые приложения с интерфейсом командной строки. Чтобы ознакомиться с кодом, использованный в статье, вы можете перейти по этой ссылке.
Домашнее задание: на данный момент приложение вылетает, когда пользователь вводит несуществующую страну или совершает опечатку. Чтобы узнать больше о кастомных хуках React Ink и попрактиковаться в хуках React, попробуйте исправить эту ошибку, например, чтобы отображалась пустая таблица или сообщение об ошибке. Отправьте решение как PR в репо проекта.
Материал подготовлен в рамках специализации "Fullstack Developer".
Всех желающих приглашаем на бесплатное demo-занятие «Карточка товара». На открытом вебинаре создадим карточку товара. Возьмем за основу макет по продаже мебели, сделанный в figma, добавим html, css, анимации и js, если потребуется.
>> РЕГИСТРАЦИЯ
Комментарии (14)
Ritan
30.11.2021 18:54+5Теперь ждём бум консольных приложений, использующих больше гигабайта памяти и требующих 30 секунд на запуск
iliazeus
01.12.2021 08:00+6Для примера вот несколько популярных приложений, сделанных с помощью React и Ink:
- Jest
Не вижу у него в dependencies ни React, ни Ink: https://github.com/facebook/jest/blob/main/package.json
- Gatsby
Не вижу у него в dependencies ни React, ни Ink: https://github.com/gatsbyjs/gatsby/blob/master/package.json
- Typescript
Не вижу у него в dependencies ни React, ни Ink: https://github.com/microsoft/TypeScript/blob/main/package.json
Возможно, я не туда смотрю?
prognosis
08.12.2021 15:59В гэтсби вроде бы есть. Не туда, но смысл тот же https://github.com/facebook/jest/blob/main/packages/jest-cli/package.json . С джестом количество установок было бы совсем другим.
matheu042
01.12.2021 12:23+1Я могу быть не прав, но работа useEffect здесь как-то странно реализована или неверно использована. Вторым параметром useEffect передается сущность, изменение которой влечет работу useEffect'а, то есть триггерит. Если указать пустой массив в качестве этого параметра, то useEffect отработает при первой отрисовке компонента, но тут не передали ничего, поэтому я немного не понимаю в какой момент срабатывает useEffect. Если предположить, что при изменении состояния компонента App, тогда образовывается infinite loop, т.к. useEffect меняет состояние. Просьба, кто знает принцип работы useEffect в ink пояснить его использование. А так спасибо автору, отличная статья) Как раз искал недавно что-то подобное.
faiwer
01.12.2021 12:38+1Если
deps
не указаны, тоuseEffect
срабатывает на каждый рендер. И да возможен вечный loop. НоsetState
ничего не сделает еслиnewValue === oldValue
. Код, конечно же, кривой. Должно быть примерно так:const [countryInfo] = wcc.getCountryDetailsByName(country); React.useEffect(() => { setCapital(countryInfo.capital); setCurrency(countryInfo.currency); setPhone(countryInfo.phone_code); }, [countryInfo]);
hamMElion
Нет, это не сон и не первоапрельская шутка. Я действительно вижу приложение для командной строки на реакте... Я озадачен до глубины души
vvadzim
А мне нра)
faiwer
React по сути состоит из двух частей:
react
, который ничего не знает ни о DOM, ни и об окружении в целомreact-dom
, которые используя API движка работают с нужным окружениемТак что в принципе нет ничего сильно сложного с тем, чтобы сделать CLI-UI на React под nodeJS. Можно даже попробовать скрестить
ужа с ежомвзять какой-нибудь замороченный UI-CLI Toolkit и написать для него renderer-прослойку.Я лет 5 назад взял какую-то популярную библиотеку под NodeJS и в ней чего только не было. И всякие диалоги, и окна, кнопки, и кажется даже какие-то layout схемы.
Но подключение React прослойки, конечно же, не избавит от необходимости вникать в этот UI Toolkit.
dev_uska
При разработке CLI возникают те же проблемы, что и при разработке GUI. React создан для решения этих проблем. И в статье неплохо показано, что подход рабочий + приведены примеры живых и достаточно сложных CLI-приложений, которые используют этот же подход. Так в чем проблема? Под UI на React "бекенд" можно хоть на Haskell писать, если проблема в этом.