Приложения с интерфейсом в виде командной строки (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 в терминале, он должен вернуть следующее:

image
image

Вот она, ваша первая командная строка на основе 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)


  1. hamMElion
    30.11.2021 16:34
    +13

    Нет, это не сон и не первоапрельская шутка. Я действительно вижу приложение для командной строки на реакте... Я озадачен до глубины души


    1. vvadzim
      30.11.2021 17:55
      -2

      А мне нра)


    1. faiwer
      01.12.2021 12:10
      +3

      React по сути состоит из двух частей:


      • сам движок react, который ничего не знает ни о DOM, ни и об окружении в целом
      • пакеты вроде react-dom, которые используя API движка работают с нужным окружением

      Так что в принципе нет ничего сильно сложного с тем, чтобы сделать CLI-UI на React под nodeJS. Можно даже попробовать скрестить ужа с ежом взять какой-нибудь замороченный UI-CLI Toolkit и написать для него renderer-прослойку.


      Я лет 5 назад взял какую-то популярную библиотеку под NodeJS и в ней чего только не было. И всякие диалоги, и окна, кнопки, и кажется даже какие-то layout схемы.


      Но подключение React прослойки, конечно же, не избавит от необходимости вникать в этот UI Toolkit.


    1. dev_uska
      02.12.2021 16:17

      При разработке CLI возникают те же проблемы, что и при разработке GUI. React создан для решения этих проблем. И в статье неплохо показано, что подход рабочий + приведены примеры живых и достаточно сложных CLI-приложений, которые используют этот же подход. Так в чем проблема? Под UI на React "бекенд" можно хоть на Haskell писать, если проблема в этом.


  1. MentalBlood
    30.11.2021 17:40
    +3

    Фронтенд еще никогда не был настолько суров


  1. Ritan
    30.11.2021 18:54
    +5

    Теперь ждём бум консольных приложений, использующих больше гигабайта памяти и требующих 30 секунд на запуск


    1. namikiri
      01.12.2021 13:01
      +1

      Вот знаете, меня порой напрягает, что Powershell показывает приглашение для ввода команд с задержкой 200мс, а тут это… Нет, спасибо, такой прогресс нам не нужен.


      1. Ritan
        01.12.2021 14:20
        +2

        По той же причине отказался от zsh с плагинами - очень ощутимая задержка приглашения


  1. 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

    Возможно, я не туда смотрю?


    1. prognosis
      08.12.2021 15:59

      В гэтсби вроде бы есть. Не туда, но смысл тот же https://github.com/facebook/jest/blob/main/packages/jest-cli/package.json . С джестом количество установок было бы совсем другим.


  1. matheu042
    01.12.2021 12:23
    +1

    Я могу быть не прав, но работа useEffect здесь как-то странно реализована или неверно использована. Вторым параметром useEffect передается сущность, изменение которой влечет работу useEffect'а, то есть триггерит. Если указать пустой массив в качестве этого параметра, то useEffect отработает при первой отрисовке компонента, но тут не передали ничего, поэтому я немного не понимаю в какой момент срабатывает useEffect. Если предположить, что при изменении состояния компонента App, тогда образовывается infinite loop, т.к. useEffect меняет состояние. Просьба, кто знает принцип работы useEffect в ink пояснить его использование. А так спасибо автору, отличная статья) Как раз искал недавно что-то подобное.


    1. 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]);


      1. faiwer
        01.12.2021 12:43

        Добавлю, что если countryInfo собирается всегда с нуля (т.е. всегда новый объект возвращается), то тогда даже так:


        }, [i.capital, i.currency, i.phone_code]);


        1. matheu042
          01.12.2021 13:07
          +1

          Спасибо за развернутый ответ, примерно так это и представлял.