也許你以前據說過前端組件代碼能夠運行在瀏覽器,運行在移動端 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
首先,這些文本輸出都不是直接 console 出來的,而是經過 React 組件
渲染出來的。typescript
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
目錄,這樣就能解決資源沒法找到的問題。
肯定了解決思路以後,咱們寫下這樣一段 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 組件的方式來搭建命令行 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;
複製代碼
能夠看到,主要包含兩大組件: State
和Log
,分別對應兩個 Tab 欄。具體的代碼你們去參考倉庫便可,下面放出效果圖:
如今問題就來了,文件操做的邏輯開發完了,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 交流,也歡迎你們一塊兒來搞事情。