вау руб! Он фактически визуализировал компонент React в окне терминала командной строки.

JavaScript React.js

Возможно, вы слышали, что код внешнего компонента может выполняться в браузере, в мобильном приложении или даже непосредственно на различных устройствах, но видели ли вы это раньше: внешний компонент запускается непосредственно в окне командной строки, что позволяет front-end Код строит графический интерфейс и логику взаимодействия с окном терминала?

Сегодня я хотел бы поделиться с вами очень интересным проектом с открытым исходным кодом: ink. Его роль заключается в отображении компонентов React в окне терминала, представляя окончательный интерфейс командной строки.

Эта статья посвящена реальному бою.В начале я познакомлю вас с основным использованием, а затем я проведу практический учебный проект, основанный на реальных сценариях.

Начиная

При первом запуске рекомендуется использовать официальные леса для создания проекта, чтобы сэкономить время и нервы.

npx create-ink-app --typescript

Затем запустите этот фрагмент кода:

import React, { useState, useEffect } from 'react'
import { render, Text} from 'ink'

const Counter = () => {
  const [count, setCount] = useState(0)
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count => ++count)
    }, 100)
    return () => {
      clearInterval(timer)
    }
    
  })

  return (
    <Text color="green">
      {count} tests passed
    </Text>
  )
}

render(<Counter />);

Появится следующий интерфейс:

И цифры продолжают расти! Демонстрация небольшая, но достаточная, чтобы проиллюстрировать проблему:

  1. Во-первых, эти текстовые выводы не консольны напрямую, а черезReact 组件предоставлено.

  2. Реагировать компоненты状态管理так же какhooks 逻辑Он по-прежнему работает в графическом интерфейсе в командной строке.

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

Инструмент командной строки

Возможно, вы только что узнали об этом инструменте и его назначении, но еще мало знакомы с тем, как его использовать. Далее, давайте воспользуемся практическим примером для ведения реального боя и быстро с ним ознакомимся. Репозиторий кода был загружен на git, вы можете разветвить код по этому адресу:GitHub.com/ternary0704…

Теперь давайте разработаем этот проект от начала до конца.

Предыстория проекта

Прежде всего, поговорим о предыстории проекта.В бизнес-проекте TS мы однажды столкнулись с проблемой: из-заproductionВ режиме мы используем первыйtsc, получите код продукта js, а затем используйтеwebpackУпакуйте эти продукты.

Но ошибка сообщается непосредственно при построении, причина в том, чтоtscневозможноts(x)Другие файлы ресурсов перемещаются в каталог продукта, поэтому, когда веб-пакет упаковывает продукт, он обнаруживает, что некоторые файлы ресурсов вообще не могут быть найдены! Например, путь такой картинки до этого такой -src/asset/1.png, но они есть в каталоге товаровdistНо его нет, поэтому, когда вебпак запаковывает код в каталог dist, он обнаружит, что этого образа не существует, поэтому сообщает об ошибке.

Решения

Как это решить?

Очевидно, что расширить возможности tsc сложно, и сейчас лучше всего написать скрипт вручную.srcСкопируйте все файлы ресурсов ниже вdistкаталог, который решает проблему невозможности найти ресурс.

1. Логика копирования файла

После определения решения записываем следующий ts-код:

import { join, parse } from "path";
import { fdir } from 'fdir';
import fse from 'fs-extra'
const staticFiles = await new fdir() 
  .withFullPaths()   
  // 过滤掉 node_modules、ts、tsx
  .filter(
    (p) =>
      !p.includes('node_modules') &&
      !p.endsWith('.ts') &&
      !p.endsWith('.tsx')
  )
  // 搜索 src 目录
  .crawl(srcPath)
  .withPromise() as string[]

await Promise.all(staticFiles.map(file => {
  const targetFilePath = file.replace(srcPath, distPath);
  // 创建目录并拷贝文件
  return fse.mkdirp(parse(targetFilePath).dir)
    .then(() => fse.copyFile(file, distPath))
   );
}))

код используетfdirЭта библиотека ищет только файлы.Это очень простая в использовании библиотека.Метод написания также очень элегантный.Рекомендуется всем использовать ее.

Мы выполнили эту логику и успешно перенесли файл ресурсов в каталог продукта.

Проблема решается, но мы можем ли мы инкапсулировать эту логику, чтобы облегчить повторное использование в других проектах или даже предоставлять ему непосредственно другим для повторного использования?

Затем я подумал об инструментах командной строки.

Во-вторых, построение графического интерфейса командной строки.

Затем мы используемink, то есть использование компонентов React для создания графического интерфейса командной строки Код корневого компонента выглядит следующим образом:


// index.tsx 引入代码省略
interface AppProps {
 fileConsumer: FileCopyConsumer
}

const ACTIVE_TAB_NAME = {
 STATE: "执行状态",
 LOG: "执行日志"
}

const App: FC<AppProps> = ({ fileConsumer }) => {
 const [activeTab, setActiveTab] = useState<string>(ACTIVE_TAB_NAME.STATE);
 const handleTabChange = (name) => {
  setActiveTab(name)
 }
 const WELCOME_TEXT = dedent`
  欢迎来到 \`ink-copy\` 控制台!功能概览如下(按 **Tab** 切换):
 `

 return <>
   <FullScreen>
    <Box>
     <Markdown>{WELCOME_TEXT}</Markdown>
    </Box>
    <Tabs onChange={handleTabChange}>
     <Tab name={ACTIVE_TAB_NAME.STATE}>{ACTIVE_TAB_NAME.STATE}</Tab>
     <Tab name={ACTIVE_TAB_NAME.LOG}>{ACTIVE_TAB_NAME.LOG}</Tab>
    </Tabs>
    <Box>
     <Box display={ activeTab === ACTIVE_TAB_NAME.STATE ? 'flex': 'none'}>
      <State />
     </Box>
     <Box display={ activeTab === ACTIVE_TAB_NAME.LOG ? 'flex': 'none'}>
      <Log />
     </Box>
    </Box>
   </FullScreen>
 </>
};

export default App;

Как видите, есть два основных компонента:Stateа такжеLog, соответствующие двум столбцам Tab соответственно. Для получения конкретного кода вы можете обратиться к складу.Визуализация показана ниже:

3. Как графический интерфейс отображает бизнес-статус в режиме реального времени?

Теперь проблема, логика работы с файлами была разработана, а также построен GUI-интерфейс. Итак, как теперь совместить их, то есть как графический интерфейс отображает состояние операций с файлами в реальном времени?

В связи с этим нам необходимо ввести третью сторону для связи между этими двумя модулями. В частности, мы поддерживаем объект EventBus в логике файловых операций, а затем передаем этот EventBus через Context в компоненте React. На этом связь между пользовательским интерфейсом и модулем файловых операций завершена.

Теперь давайте разработаем этот объект EventBus, который выглядит следующим образом:FileCopyConsumer:

export interface EventData {
  kind: string;
  payload: any;
}

export class FileCopyConsumer {

  private callbacks: Function[];
  constructor() {
    this.callbacks = []
  }
  // 供 React 组件绑定回调
  onEvent(fn: Function) {
    this.callbacks.push(fn);
  }
  // 文件操作完成后调用
  onDone(event: EventData) {
    this.callbacks.forEach(callback => callback(event))
  }
}

Затем в модуле файловых операций и модуле пользовательского интерфейса нам нужно адаптировать ответ.Сначала давайте посмотрим на модуль файловых операций и сделаем инкапсуляцию.

export class FileOperator {
  fileConsumer: FileCopyConsumer;
  srcPath: string;
  targetPath: string;
  constructor(srcPath ?: string, targetPath ?: string) {
    // 初始化 EventBus 对象
    this.fileConsumer = new FileCopyConsumer();
    this.srcPath = srcPath ?? join(process.cwd(), 'src');
    this.targetPath = targetPath ?? join(process.cwd(), 'dist');
  }

  async copyFiles() {
    // 存储 log 信息
    const stats = [];
    // 在 src 中搜索文件
    const staticFiles = ...
    
    await Promise.all(staticFiles.map(file => {
        // ...
        // 存储 log
        .then(() => stats.push(`Copied file from [${file}] to [${targetFilePath}]`));
    }))
    // 调用 onDone
    this.fileConsumer.onDone({
      kind: "finish",
      payload: stats
    })
  }
}

а затем инициализироватьFileOperatorПосле этогоfileConsumerПередайте его в компонент через React Context, чтобы компонент мог получить к нему доступfileConsumer, а затем вы можете привязать функцию обратного вызова.Код демонстрируется следующим образом:

// 组件当中拿到 fileConsumer & 绑定回调
export const State: FC<{}> = () => {
  const context = useContext(Context);
  const [finish, setFinish] = useState(false);
  context?.fileConsumer.onEvent((data: EventData) => {
    // 下面的逻辑在文件拷贝完成后执行
    if (data.kind === 'finish') {
      setTimeout(() => {
        setFinish(true)
      }, 2000)
    }
  })

  return 
  //(JSX代码)
}

Таким образом, мы успешно соединили логику пользовательского интерфейса и работы с файлами. Конечно, из-за нехватки места некоторые коды все еще не показаны, а полные коды находятся в репозитории git. Надеюсь, каждый сможет раскошелиться и испытать на себе дизайн всего проекта.

В целом, тот факт, что код компонента React может работать в терминале командной строки, действительно захватывающая вещь, которая освобождает больше места для воображения для внешнего интерфейса. Использование этой способности в этой статье — лишь верхушка айсберга. Другие позы ждут вас, чтобы разблокировать, так что идите и играйте!

Эта статья была впервые опубликована в публичном аккаунте «Front-end Sanyuan Classmates», и все желающие могут обратить на это внимание.Оригинальная ссылка:вау руб! На самом деле он поместил рендеринг компонента React в окно терминала командной строки внутри!

Команде архитектуры внешнего интерфейса ByteDance IES срочно нужны таланты (p5/p6/p7 много HC), приглашаю добавить меня WeChat sanyuan0704 для общения и приглашаю всех к совместной работе.