🧑💻 Создание CLI с помощью React Ink

Да, дорогой читатель, ты все правильно понял! Вы можете создавать красивые 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
Просмотр на GitHub

Разве не здорово, что все хуки и другие возможности React работают в CLI?
React уверенно захватывает мир 😉.

Увидимся в следующем посте! 👋

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *