Создание эмулятора терминала в React

Создание эмулятора терминала в React

23 мая 2022 г.

Окно терминала, еще в те времена, когда компьютеры управлялись только командной строкой, или в настоящее время при разработке даже самого маленького проекта, мы все его используем.


Если это еще не звоночек, вот [определение из Википедии] (https://en.wikipedia.org/wiki/Command-line_interface) о терминалах, также известных как. интерфейсы командной строки:


интерфейс командной строки (CLI) обрабатывает команды в компьютерную программу в виде строк текста. Программа, которая обрабатывает интерфейс, называется интерпретатором командной строки или процессором командной строки.


Эти терминальные интерфейсы используются многими веб-сайтами, такими как Haskell, Hyper, даже Codesandbox имеет терминал (консоль). Давайте сделаем его с помощью ReactJs для нашего веб-сайта.


Что мы будем строить


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


Вы можете просмотреть код на GitHub или увидеть его в действии здесь.



Чтобы создать этот терминал, мы используем create-react-app (CRA) для создания нового приложения React через наш компьютерный терминал. Вы даже можете увидеть анимацию окна терминала на веб-сайте CRA в разделе «Начните работу за считанные секунды».


Перво-наперво


Прежде всего, давайте создадим наш проект React с помощью интерфейса командной строки create-react-app (CRA). Мы создадим приложение с шаблоном Typescript, чтобы сделать наш код лучше и избежать возможных ошибок, таких как добавление двух строк, когда мы этого не хотим.


Я назову приложение «терминал», поэтому на консоли своего компьютера я запущу следующий код:


``` ударить


Терминал npx create-реагировать-приложение --template typescript


После этого я открою только что созданную папку «терминал» в выбранном мной редакторе кода [WebStorm] (https://www.jetbrains.com/webstorm/).


Давайте немного почистим файлы. Давайте удалим файлы logo.svg, App.css и App.test.tsx из папки src/. Из файла App.tsx давайте удалим все, что находится внутри div с className App, а также удалим строки номер 2 и 3 с импортом логотипа. Вот так:


```машинопись


импортировать React из «реагировать»;


приложение функции () {


возврат (



Мы наполним этот раздел позже



экспортировать приложение по умолчанию;


Создание стиля и базовой структуры


В папке src/ создадим еще одну папку с именем Terminal/, а внутри нее создадим файл index.tsx с именем terminal.css. Это будет наша основная структура.


Окно терминала состоит из двух элементов: вывода и ввода. На выходе мы будем писать ответ, а на входе мы будем писать наши команды.


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


В index.tsx у нас будет следующий код:


```машинопись


импортировать './terminal.css';


экспортировать константу Терминал = () => {


возврат (



Конечная линия


александру.тасика:


<тип ввода = "текст" />





Наш стиль terminal.css будет следующим:


```машинопись


.Терминал {


высота: 500 пикселей;


переполнение-у: авто;


цвет фона: #3C3C3C;


цвет: #C4C4C4;


отступ: 35px 45px;


размер шрифта: 14px;


высота строки: 1,42;


семейство шрифтов: «IBM Plex Mono», Consolas, Menlo, Monaco, «Courier New», Courier,


моноширинный;


тень текста: 0 4px 4px rgba (0, 0, 0, 0,25);


.terminal__line {


высота строки: 2;


пробел: предварительная обертка;


.terminal__prompt {


дисплей: гибкий;


выравнивание элементов: по центру;


.terminal__prompt__label {


гибкий: 0 0 авто;


цвет: #F9EF00;


.terminal__prompt__input {


гибкий: 1;


поле слева: 1re;


дисплей: гибкий;


выравнивание элементов: по центру;


белый цвет;


.terminal__prompt__input ввод {


гибкий: 1;


ширина: 100%;


цвет фона: прозрачный;


белый цвет;


граница: 0;


контур: нет;


размер шрифта: 14px;


высота строки: 1,42;


семейство шрифтов: «IBM Plex Mono», Consolas, Menlo, Monaco, «Courier New», Courier,


моноширинный;


А также давайте импортируем это в наш App.tsx, чтобы просмотреть его в нашем браузере:


```машинопись


импортировать React из «реагировать»;


импортировать {Терминал} из "./Терминал";


приложение функции () {


возврат (



<Терминал />



экспортировать приложение по умолчанию;


И вот результат, 🥁 барабаны пожалуйста:



Хорошо, значит, вы поняли, куда мы направляемся.


Базовое управление состоянием


Потрясающий! Итак, у нас есть основная структура. Теперь нам нужно заставить пользователей взаимодействовать с ним, вести историю сообщений и добавлять новые сообщения в терминал.


Для этого мы напишем специальный хук под названием useTerminal, который будет идти рука об руку с нашим терминалом. Давайте посмотрим на базовую структуру хуков, которую мы собираемся использовать:


```машинопись


импортировать {useCallback, useEffect, useState} из «реагировать»;


импортировать {TerminalHistory, TerminalHistoryItem, TerminalPushToHistoryWithDelayProps} из "./types";


экспортировать const useTerminal = () => {


const [terminalRef, setDomNode] = useState();


const setTerminalRef = useCallback((узел: HTMLDivElement) => setDomNode(узел), []);


const [история, setHistory] = useState<История Терминала>([]);


  • Прокрутите вниз терминала при изменении размера окна

использоватьЭффект(() => {


const windowResizeEvent = () => {


terminalRef?.scrollTo({


вверху: terminalRef?.scrollHeight ?? 99999,


поведение: «гладкое»,


window.addEventListener('изменить размер', windowResizeEvent);


возврат () => {


window.removeEventListener('resize', windowResizeEvent);


}, [терминальная ссылка]);


  • Прокрутите вниз терминала для каждого нового элемента истории

использоватьЭффект(() => {


terminalRef?.scrollTo({


вверху: terminalRef?.scrollHeight ?? 99999,


поведение: «гладкое»,


}, [история, TerminalRef]);


const pushToHistory = useCallback((item: TerminalHistoryItem) => {


setHistory((старый) => [...старый, элемент]);


  • Написать текст в терминал

  • @param content Текст, который будет напечатан в терминале

  • @param delay Задержка в мс перед печатью текста

  • @param executeBefore Функция, которая должна быть выполнена до того, как текст будет напечатан

  • @param executeAfter Функция, которая будет выполнена после вывода текста

константа pushToHistoryWithDelay = useCallback(


задержка = 0,


содержание,


}: TerminalPushToHistoryWithDelayProps) =>


новое обещание ((разрешить) => {


setTimeout(() => {


pushToHistory (контент);


вернуть решение (содержание);


}, задерживать);


[нажать на историю]


  • Сбросить окно терминала

const resetTerminal = useCallback(() => {


установитьИсторию([]);


возврат {


история,


pushToHistory,


pushToHistoryWithDelay,


терминалRef,


setTerminalRef,


сброситьтерминал,


И давайте также создадим отдельный файл, который будет содержать тип реквизита и все, что мы хотим ввести строго, например, «История терминала». Давайте создадим файл с именем types.ts с определениями, которые у нас есть прямо сейчас.


```машинопись


импортировать {ReactNode} из "реагировать";


тип экспорта TerminalHistoryItem = ReactNode | нить;


тип экспорта TerminalHistory = TerminalHistoryItem[];


тип экспорта TerminalPushToHistoryWithDelayProps = {


содержимое: TerminalHistoryItem;


задержка?: номер;


Теперь, когда мы определили типы, давайте посмотрим краткое описание того, что делает каждая функция в нашем хуке:


  • Первое состояние, terminalRef, сохранит нашу ссылку на контейнер терминала. Мы будем ссылаться на него позже в нашем файле terminal/index.tsx. Первая функция — это вспомогательная функция, которая устанавливает ссылку на этот div.

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

  • pushToHistory отправит новое сообщение в историю нашего терминала

  • pushToHistoryWithDelay отправит новое сообщение в историю нашего терминала с определенной задержкой. Мы возвращаем обещание вместо того, чтобы обрабатывать его как функцию, поэтому мы можем связать несколько нажатий в виде анимации.

  • Наконец, у нас есть resetTerminal, который сбрасывает историю терминала. Работает больше как функция очистки из классического терминала.

Потрясающий! Теперь, когда у нас есть способ хранить и отправлять сообщения, давайте интегрируем этот хук со структурой. В файле Terminal/index.tsx у нас будут некоторые реквизиты, которые будут связаны с историей терминала, а также будут обрабатывать фокус в строке ввода нашего терминала.


```машинопись


импортировать './terminal.css';


импортировать {ForwardedRef, forwardRef, useCallback, useEffect, useRef} из «реакции»;


импортировать {TerminalHistory, TerminalHistoryItem} из "./types";


тип экспорта TerminalProps = {


история: TerminalHistory;


promptLabel?: TerminalHistoryItem;


экспортировать const Terminal = forwardRef(


(реквизит: TerminalProps, ссылка: ForwardedRef) => {


константа {


история = [],


подсказка = '>',


} = реквизит;


  • Сосредоточьтесь на вводе всякий раз, когда мы визуализируем терминал или нажимаем в терминале

const inputRef = useRef();


использоватьЭффект(() => {


inputRef.current?.focus();


const focusInput = useCallback(() => {


inputRef.current?.focus();


возврат (



{history.map((строка, индекс) => (


terminal-line-${index}-${line}}>

{линия}




{promptLabel}


<ввод


тип = "текст"


// @ts-игнорировать


ссылка = {inputRef}





А нет, давайте соединим точки и впихнем что-нибудь в терминал. Поскольку мы принимаем любой ReactNode как строку, мы можем отправить любой HTML, какой захотим.


Наш App.tsx будет оберткой, здесь будут происходить все действия. Здесь мы установим сообщения, которые мы хотим показать. Вот рабочий пример:


```машинопись


импортировать React, {useEffect} из 'реагировать';


импортировать {Терминал} из "./Терминал";


импортировать {useTerminal} из "./Terminal/hooks";


приложение функции () {


константа {


история,


pushToHistory,


setTerminalRef,


сброситьтерминал,


} = использоватьтерминал();


использоватьЭффект(() => {


сброситьтерминал();


pushToHistory(<>


Добро пожаловать! в терминал.

Он содержит HTML. Потрясающе, правда?

возврат (



<Терминал


история={история}


ссылка = {setTerminalRef}


promptLabel={<>Напишите что-нибудь классное:}



экспортировать приложение по умолчанию;


И пока результат такой:



При монтировании мы просто нажимаем все, что хотим, например, этот крутой большой желтый HTML-текст.


Реализация команд


Потрясающий! Вы дошли почти до конца. Теперь давайте добавим возможность пользователю взаимодействовать с нашим терминалом в письменной форме.


В наш Terminal/index.tsx добавится возможность обновить ввод, а также команды, которые будет выполнять каждое слово. Это самые важные вещи в терминале, верно?


Давайте обработаем пользовательский ввод в файле «Terminal/index.tsx» и прослушаем его, когда пользователь нажмет клавишу «Enter». После этого выполняем ту функцию, которую он хочет.


Файл Terminal/index.tsx преобразуется в:


```javascript


импортировать './terminal.css';


импортировать {ForwardedRef, forwardRef, useCallback, useEffect, useRef, useState} из «реакции»;


импортировать {TerminalProps} из "./types";


экспортировать const Terminal = forwardRef(


(реквизит: TerminalProps, ссылка: ForwardedRef) => {


константа {


история = [],


подсказка = '>',


команды = {},


} = реквизит;


const inputRef = useRef();


const [ввод, setInputValue] = useState('');


  • Сосредоточьтесь на вводе всякий раз, когда мы визуализируем терминал или нажимаем в терминале

использоватьЭффект(() => {


inputRef.current?.focus();


const focusInput = useCallback(() => {


inputRef.current?.focus();


  • Когда пользователь что-то вводит, мы обновляем введенное значение

константа handleInputChange = useCallback(


(e: React.ChangeEvent) => {


setInputValue (e.target.value);


  • Когда пользователь нажимает ввод, мы выполняем команду

const handleInputKeyDown = useCallback(


(e: React.KeyboardEvent) => {


если (e.key === 'Ввод') {


const commandToExecute = команды?.[input.toLowerCase()];


если (командаToExecute) {


командаВыполнить?.();


установить значение ввода ('');


[команды, ввод]


возврат (



{history.map((строка, индекс) => (


terminal-line-${index}-${line}}>

{линия}




{promptLabel}


<ввод


тип = "текст"


значение = {ввод}


onKeyDown={handleInputKeyDown}


onChange={handleInputChange}


// @ts-игнорировать


ссылка = {inputRef}





Как видите, мы переместили определенный здесь тип в отдельный созданный нами файл с именем types.ts. Давайте изучим этот файл и посмотрим, какие новые вещи мы определили.


```машинопись


импортировать {ReactNode} из "реагировать";


тип экспорта TerminalHistoryItem = ReactNode | нить;


тип экспорта TerminalHistory = TerminalHistoryItem[];


тип экспорта TerminalPushToHistoryWithDelayProps = {


содержимое: TerminalHistoryItem;


задержка?: номер;


тип экспорта TerminalCommands = {


[команда: строка]: () => недействительным;


тип экспорта TerminalProps = {


история: TerminalHistory;


promptLabel?: TerminalHistoryItem;


команды: TerminalCommands;


Хорошо, теперь, когда мы можем выполнять команды из терминала, давайте определим некоторые из них в App.tsx. Мы создадим всего два простых, а вы дайте волю своему воображению!


```машинопись


импортировать React, {useEffect, useMemo} из 'реагировать';


импортировать {Терминал} из "./Терминал";


импортировать {useTerminal} из "./Terminal/hooks";


приложение функции () {


константа {


история,


pushToHistory,


setTerminalRef,


сброситьтерминал,


} = использоватьтерминал();


использоватьЭффект(() => {


сброситьтерминал();


pushToHistory(<>


Добро пожаловать! в терминал.

Он содержит HTML. Потрясающе, правда?



Вы можете написать: start или alert , чтобы выполнить некоторые команды.

константные команды = useMemo(() => ({


'старт': асинхронный () => {


ожидание pushToHistory(<>


<дел>


Запуск сервера... Готово



'предупреждение': асинхронный () => {


Сообщить('Привет!');


ожидание pushToHistory(<>


<дел>


Предупреждение



Отображается в браузере




}), [pushToHistory]);


возврат (



<Терминал


история={история}


ссылка = {setTerminalRef}


promptLabel={<>Напишите что-нибудь классное:}


команды = {команды}



экспортировать приложение по умолчанию;


Константа commands определяет команду, которую должен ввести пользователь. Поэтому, если пользователь введет «start» и нажмет «Enter», он увидит текст «Запуск сервера… Готово».


Что мы построили


Спасибо, что добрались до конца, вы потрясающие!


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



Что дальше?


Что дальше спросите вы? Терминал сейчас довольно прост. Вы не видите ранее введенные команды. Если пользователь введет неверную команду, он ничего не увидит. И, конечно же, анимации!


До встречи во второй части, посвященной этим новым функциям.


А пока вы можете просмотреть код на GitHub или увидеть его в действии здесь.



Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE