Да, дорогой читатель, ты все правильно понял! Вы можете создавать красивые CLI с помощью React, используя потрясающую библиотеку под названием React Ink!
О том, как работает эта библиотека, лучше всего сказано в README репозитория:
Ink предоставляет тот же опыт создания пользовательского интерфейса на основе компонентов, который React предлагает в браузере, но для приложений командной строки. Он использует Yoga для построения Flexbox-макетов в терминале, поэтому большинство CSS-подобных реквизитов доступны и в Ink. Если вы уже знакомы с React, то вы уже знакомы и с Ink. Поскольку Ink является рендерером React, это означает, что поддерживаются все возможности React. Перейдите на сайт React, чтобы найти документацию по его использованию. В этом readme будут описаны только методы Ink.
Что мы будем создавать ?️.
В этом посте мы изучим, как работает Ink, создав небольшой крутой CLI, который получает информацию о покемонах с помощью PokeAPI!
Создание проекта Ink ?
Это очень просто и понятно.
- Сначала вы создадите пустую директорию
mkdir pokecli && cd pokecli
- Затем вы можете выполнить команду
create-ink-app
.
npx create-ink-app --typescript
В этой заметке я буду использовать TypeScript, но вы можете следовать за мной и на обычном JS.
Если мы посмотрим на то, что создала эта команда, то увидим очень базовую структуру файлов:
pokecli
source/
.editorconfig
.gitattributes
package-lock.json
package.json
readme.md
tsconfig.json
Мы можем игнорировать все, кроме папки source
.
source/ui.tsx
import React, { FC } from "react";
import { Text } from "ink";
const App: FC<{ name?: string }> = ({ name = "Stranger" }) => (
<Text>
Hello, <Text color="green">{name}</Text>
</Text>
);
module.exports = App;
export default App;
Это обычный компонент App, как в обычном React. Этому компоненту передается параметр name
, который по умолчанию имеет значение Stranger. И выводится сообщение «Hello {name}». Обратите внимание, что компонент Text
происходит от ink
. Он может быть использован для стилизации многих аспектов текста, таких как цвет, цвет фона и т.д. Для этого ink
использует библиотеку под названием chalk.
source/cli.tsx
#!/usr/bin/env node
import React from "react";
import { render } from "ink";
import meow from "meow";
import App from "./ui";
const cli = meow(
`
Usage
$ pokecli
Options
--name Your name
Examples
$ pokecli --name=Jane
Hello, Jane
`,
{
flags: {
name: {
type: "string",
},
},
}
);
render(<App name={cli.flags.name} />);
Этот файл является точкой входа приложения CLI. Функция meow
выводит текст, который появится во флаге --help
. А затем она извлекает функцию render из ink
для отображения экспортированного компонента App
из ui.tsx
. name
— это аргумент командной строки, который может быть задан пользователем следующим образом:
pokecli --name=Charmander
Мы видим, что этот аргумент имеет явный тип string
. Поскольку теперь у нас есть базовое понимание того, как работает Ink, давайте перейдем к созданию нашего CLI!
Запуск CLI ?
Мы можем запустить этот код, сначала скомпилировав наш исходный код в исполняемый файл
npm run build
А затем запустить исполняемый файл:
pokecli --name=Charmander
И мы сможем увидеть наш вывод!
Вы также можете запустить pokecli с флагом --help
, чтобы увидеть вывод того, что передается в функцию meow
в cli.tsx
.
Создание нашего CLI ?️
Давайте сначала сделаем простую функцию для получения данных о покемоне через его имя в ui.tsx
.
Для этого мы будем использовать библиотеку под названием axios.
npm i axios
Затем мы можем использовать эту функцию для отправки запроса в PokeAPI.
// fetch pokemon data with its name using pokeapi
const pokemon = (name: string): void => {
axios
.get(`https://pokeapi.co/api/v2/pokemon/${name.toLowerCase()}`)
.then((res) => {
console.log(res.data);
});
};
И если проверить это, мы сможем увидеть данные, связанные с тем, что передано во флаге CLI name.
Проблема в том, что TypeScript не знает свойств, которые существуют в этом объекте данных. Поэтому давайте объявим интерфейсы для ответа API.
interface Type {
slot: number;
type: {
name: string;
};
}
interface Stat {
base_stat: number;
effort: number;
stat: {
name: string;
};
}
interface PokemonData {
name: string;
height: number;
weight: number;
types: Type[];
stats: Stat[];
}
Ref:
Давайте также создадим переменную состояния для хранения данных о наших покемонах:
const [pokemonData, setPokemonData] = React.useState<PokemonData | null>(null);
Теперь мы можем обновить нашу функцию для получения данных о покемонах соответствующим образом:
// fetch pokemon data with its name using pokeapi
const pokemon = (name: string): Promise<PokemonData> => {
const url = `https://pokeapi.co/api/v2/pokemon/${name}`;
return axios
.get<PokemonData>(url)
.then((response: AxiosResponse<PokemonData>) => {
return response.data;
});
};
Круто!
Теперь давайте вызовем эту функцию в хуке useEffect
:
// call useEffect and use store the pokemon data in state
useEffect(() => {
pokemon(name).then((data: PokemonData) => {
setPokemonData(data);
});
}, [name]);
Потрясающе!
Теперь все, что нам нужно сделать, это просто отобразить данные. Поскольку наше состояние будет null
, если данные покемона еще не установлены, мы можем использовать это как индикатор загрузки.
return (
(pokemonData &&
{
/* pokemon stuff */
}) || <Text>Loading...</Text>
);
Затем мы можем отобразить данные покемона:
return (
(pokemonData && (
<Box>
<Text>
<Text bold color="blue">
{pokemonData?.name[0]?.toUpperCase() + pokemonData!.name?.slice(1)}
</Text>
{"n"}
{/* Display a divider */}
<Text color="magentaBright">
{Array(pokemonData?.name.length + 1).join("-")}
</Text>
{"n"}
<Text color="yellowBright">Metrics:</Text> <Text
color="greenBright"
bold
>
{/* Height is in decimeters */}
{pokemonData!.height / 10}m, {pokemonData!.weight / 10}kg
</Text>
{"n"}
<Text color="yellowBright">Type:</Text> <Text color="greenBright" bold>
{/* Display the pokemon's types */}
{pokemonData?.types.map((type: Type) => type.type.name).join(", ")}
</Text>
{"nn"}
{/* Display the pokemon's stats */}
<Text color="yellowBright" bold>
Stats{"n"}
</Text>
<Text color="greenBright">{pokemonData?.stats.map((stat: Stat) => `${stat.stat.name}: ${stat.base_stat}`).join("n")}</Text>
</Text>
</Box>
)) || <Text>Loading...</Text>
);
Теперь вы должны увидеть это:
Мы можем очистить экран терминала перед отображением данных. Существует библиотека NPM под названием [clear](https://www.npmjs.com/package/clear)
, которую мы можем использовать для этого.
npm i clear
Поскольку эта библиотека написана на JS, нам также понадобятся определения типов для нее.
npm i -D @types/clear
Теперь мы можем вызвать функцию clear
над нашим JSX.
clear();
return (
(pokemonData && (
<Box>
<Text>
<Text bold color="blue">
Круто!
Вы также можете изменить текст справки:
cli.tsx
const cli = meow(
`
Usage
$ pokecli
Options
--name The name of the pokemon
Examples
$ pokecli --name=charmander
Charmander
----------
Metrics: 0.6m, 8.5 kg
Type: fire
Stats
hp: 39
attack: 52
defense: 43
special-attack: 60
special-defense: 50
speed: 65
`,
{
flags: {
name: {
type: "string",
},
},
}
);
? Окончательный вывод
Следуя моим указаниям, вы должны увидеть это!
Вы можете найти исходный код репозитория здесь:
carrotfarmer / pokecli
⚽️ CLI для поиска информации о покемонах?
pokecli
CLI для поиска информации о покемонах!
- Создан с использованием
React Ink
.
Установите
$ npm install --global @pokecli/pokecli
CLI
Usage
$ pokecli
Options
--name The name of the pokemon
Examples
$ pokecli --name=charmander
Charmander
----------
Metrics: 0.6m, 8.5 kg
Type: fire
Stats
hp: 39
attack: 52
defense: 43
special-attack: 60
special-defense: 50
speed: 65
Разве не здорово, что все хуки и другие возможности React работают в CLI?
React уверенно захватывает мир ?.
Увидимся в следующем посте! ?