哇擦!他竟然把 React 組件渲染到了命令行終端窗口裏面

也許你以前據說過前端組件代碼能夠運行在瀏覽器,運行在移動端 App 裏面,甚至能夠直接在各類設備當中,但你有沒有見過: 前端組件直接跑在命令行窗口裏面,讓前端代碼構建出終端窗口的 GUI 界面和交互邏輯?前端

今天, 給你們分享一個很是有意思的開源項目: ink。它的做用就是將 React 組件渲染在終端窗口中,呈現出最後的命令行界面。node

本文偏重實戰,前面會帶你們熟悉基本使用,而後會作一個基於實際場景的練手項目。react

上手初體驗

剛開始上手時,推薦使用官方的腳手架建立項目,省時省心。webpack

npx create-ink-app --typescript
複製代碼

而後運行這樣一段代碼:git

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 />);
複製代碼

會出現以下的界面:github

而且數字一直遞增! demo 雖小,但足以說明問題:web

  1. 首先,這些文本輸出都不是直接 console 出來的,而是經過 React 組件渲染出來的。typescript

  2. React 組件的狀態管理以及hooks 邏輯放到命令行的 GUI 當中仍然是生效的。瀏覽器

也就是說,前端的能力以及擴展到了命令行窗口當中了,這無疑是一項很是可怕的能力。著名的文檔生成工具 Gatsby,包管理工具yarn2都使用了這項能力來完成終端 GUI 的搭建。微信

命令行工具項目實戰

可能你們剛剛瞭解到這個工具,知道它的用途,但對於具體如何使用仍是比較陌生。接下來讓咱們以一個實際的例子來進行實戰,快速熟悉。代碼倉庫已經上傳到 git,你們能夠這個地址下面 fork 代碼: github.com/sanyuan0704…

下面咱們就來從頭至尾開發這個項目。

項目背景

首先說一說項目的產生背景,在一個 TS 的業務項目當中,咱們曾經碰到了一個問題:因爲production模式下面,咱們是採用先 tsc,拿到 js 產物代碼,再用webpack打包這些產物。

但構建的時候直接報錯了,緣由就是 tsc 沒法將 ts(x) 之外的資源文件移動到產物目錄,以致於 webpack 在對於產物進行打包的時候,發現有些資源文件根本找不到!好比之前有這樣一張圖片的路徑是這樣—— src/asset/1.png,但這些在產物目錄dist卻沒還有,所以 webpack 在打包 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這個庫才搜索文件,很是好用的一個庫,寫法上也很優雅,推薦你們使用。

咱們執行這段邏輯,成功將資源文件轉移到到了產物目錄中。

問題是解決掉了,但咱們能不能封裝一下這個邏輯,讓它可以更方便地在其它項目當中複用,甚至直接提供給其餘人複用呢?

接着,我想到了命令行工具。

2、命令行 GUI 搭建

接着咱們使用 ink,也就是用 React 組件的方式來搭建命令行 GUI,根組件代碼以下:

// 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;
複製代碼

能夠看到,主要包含兩大組件: StateLog,分別對應兩個 Tab 欄。具體的代碼你們去參考倉庫便可,下面放出效果圖:

3. GUI 如何實時展現業務狀態?

如今問題就來了,文件操做的邏輯開發完了,GUI 界面也搭建好了。那麼如今如何將二者結合起來呢,也就是 GUI 如何實時地展現文件操做的狀態呢?

對此,咱們須要引入第三方,來進行這兩個模塊的通訊。具體來說,咱們在文件操做的邏輯中維護一個 EventBus 對象,而後在 React 組件當中,經過 Context 的方式傳入這個 EventBus。 從而完成 UI 和文件操做模塊的通訊。

如今咱們開發一下這個 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))
  }
}
複製代碼

接着在文件操做模塊和 UI 模塊當中,都須要作響應的適配,首先看看文件操做模塊,咱們作一下封裝。

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代碼)
}
複製代碼

這樣,咱們就成功地將 UI 和文件操做邏輯串聯了起來。固然,篇幅所限,還有一些代碼並無展現出來,完整的代碼都在 git 倉庫當中。但願你們能 fork 下來好好體會一下整個項目的設計。

整體來講,React 組件代碼可以跑在命令行終端,確實是一件激動人心的事情,給前端釋放了更多想象的空間。本文對於這個能力的使用也只是冰山一角,更多使用姿式等待你去解鎖,趕忙去玩一玩吧!

本文首發於公衆號《前端三元同窗》歡迎你們關注, 原文連接:哇擦!他竟然把 React 組件渲染到了命令行終端窗口裏面!

字節跳動 IES 前端架構團隊急缺人才(p5/p6/p7大量HC),歡迎加我微信 sanyuan0704 交流,也歡迎你們一塊兒來搞事情。

相關文章
相關標籤/搜索