他居然把 React 组件跑在命令行终端窗口里面!

开源
今天, 给大家分享一个非常有意思的开源项目: ink。它的作用就是将 React 组件渲染在终端窗口中,呈现出最后的命令行界面。

[[384219]]

 也许你之前听说过前端组件代码可以运行在浏览器,运行在移动端 App 里面,甚至可以直接在各种设备当中,但你有没有见过: 前端组件直接跑在命令行窗口里面,让前端代码构建出终端窗口的 GUI 界面和交互逻辑?

今天, 给大家分享一个非常有意思的开源项目: ink。它的作用就是将 React 组件渲染在终端窗口中,呈现出最后的命令行界面。

本文偏重实战,前面会带大家熟悉基本使用,然后会做一个基于实际场景的实战项目。

上手初体验

刚开始上手时,推荐使用官方的脚手架创建项目,省时省心。

  1. npx create-ink-app --typescript 

然后运行这样一段代码:

  1. import React, { useState, useEffect } from 'react' 
  2. import { render, Text} from 'ink' 
  3.  
  4. const Counter = () => { 
  5.   const [count, setCount] = useState(0) 
  6.   useEffect(() => { 
  7.     const timer = setInterval(() => { 
  8.       setCount(count => ++count
  9.     }, 100) 
  10.     return () => { 
  11.       clearInterval(timer) 
  12.     } 
  13.      
  14.   }) 
  15.  
  16.   return ( 
  17.     <Text color="green"
  18.       {count} tests passed 
  19.     </Text> 
  20.   ) 
  21.  
  22. render(<Counter />); 

会出现如下的界面:


并且数字一直递增!demo 虽小,但足以说明问题:

  1. 首先,这些文本输出都不是直接 console 出来的,而是通过 React 组件渲染出来的。
  2. React 组件的状态管理以及hooks 逻辑放到命令行的 GUI 当中仍然是生效的。

也就是说,前端的能力以及扩展到了命令行窗口当中了,这无疑是一项非常可怕的能力。著名的文档生成工具Gatsby,包管理工具yarn2都使用了这项能力来完成终端 GUI 的搭建。

命令行工具项目实战

可能大家刚刚了解到这个工具,知道它的用途,但对于具体如何使用还是比较陌生。接下来让我们以一个实际的例子来进行实战,快速熟悉。代码仓库已经上传到 git,大家可以这个地址下面 fork 代码: https://github.com/sanyuan0704/ink-copy-command。

下面我们就来从头到尾开发这个项目。

项目背景

首先说一说项目的产生背景,在一个 TS 的业务项目当中,我们曾经碰到了一个问题:由于production模式下面,我们是采用先 tsc,拿到 js 产物代码,再用webpack打包这些产物。

但构建的时候直接报错了,原因就是 tsc 无法将 ts(x) 以外的资源文件移动到产物目录,以至于 webpack 在对于产物进行打包的时候,发现有些资源文件根本找不到!比如以前有这样一张图片的路径是这样—— src/asset/1.png,但这些在产物目录dist却没还有,因此 webpack 在打包 dist 目录下的代码时,会发现这张图片不存在,于是报错了。

解决思路

那如何来解决呢?

很显然,我们很难去扩展 tsc 的能力,现在最好的方式就是写个脚本手动将src下面的所有资源文件一一拷贝到dist目录,这样就能解决资源无法找到的问题。

一、拷贝文件逻辑

确定了解决思路之后,我们写下这样一段 ts 代码:

  1. import { join, parse } from "path"
  2. import { fdir } from 'fdir'
  3. import fse from 'fs-extra' 
  4. const staticFiles = await new fdir()  
  5.   .withFullPaths()    
  6.   // 过滤掉 node_modules、ts、tsx 
  7.   .filter( 
  8.     (p) => 
  9.       !p.includes('node_modules') && 
  10.       !p.endsWith('.ts') && 
  11.       !p.endsWith('.tsx'
  12.   ) 
  13.   // 搜索 src 目录 
  14.   .crawl(srcPath) 
  15.   .withPromise() as string[] 
  16.  
  17. await Promise.all(staticFiles.map(file => { 
  18.   const targetFilePath = file.replace(srcPath, distPath); 
  19.   // 创建目录并拷贝文件 
  20.   return fse.mkdirp(parse(targetFilePath).dir) 
  21.     .then(() => fse.copyFile(file, distPath)) 
  22.    ); 
  23. })) 

代码使用了fdir这个库才搜索文件,非常好用的一个库,写法上也很优雅,推荐大家使用。

我们执行这段逻辑,成功将资源文件转移到到了产物目录中。

问题是解决掉了,但我们能不能封装一下这个逻辑,让它能够更方便地在其它项目当中复用,甚至直接提供给其他人复用呢?

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

二、命令行 GUI 搭建

接着我们使用 ink,也就是用 React 组件的方式来搭建命令行 GUI,根组件代码如下:

  1. // index.tsx 引入代码省略 
  2. interface AppProps { 
  3.  fileConsumer: FileCopyConsumer 
  4.  
  5. const ACTIVE_TAB_NAME = { 
  6.  STATE: "执行状态"
  7.  LOG: "执行日志" 
  8.  
  9. const App: FC<AppProps> = ({ fileConsumer }) => { 
  10.  const [activeTab, setActiveTab] = useState<string>(ACTIVE_TAB_NAME.STATE); 
  11.  const handleTabChange = (name) => { 
  12.   setActiveTab(name
  13.  } 
  14.  const WELCOME_TEXT = dedent` 
  15.   欢迎来到 \`ink-copy\` 控制台!功能概览如下(按 **Tab** 切换): 
  16.  ` 
  17.  
  18.  return <> 
  19.    <FullScreen> 
  20.     <Box> 
  21.      <Markdown>{WELCOME_TEXT}</Markdown> 
  22.     </Box> 
  23.     <Tabs onChange={handleTabChange}> 
  24.      <Tab name={ACTIVE_TAB_NAME.STATE}>{ACTIVE_TAB_NAME.STATE}</Tab> 
  25.      <Tab name={ACTIVE_TAB_NAME.LOG}>{ACTIVE_TAB_NAME.LOG}</Tab> 
  26.     </Tabs> 
  27.     <Box> 
  28.      <Box display={ activeTab === ACTIVE_TAB_NAME.STATE ? 'flex''none'}> 
  29.       <State /> 
  30.      </Box> 
  31.      <Box display={ activeTab === ACTIVE_TAB_NAME.LOG ? 'flex''none'}> 
  32.       <Log /> 
  33.      </Box> 
  34.     </Box> 
  35.    </FullScreen> 
  36.  </> 
  37. }; 
  38.  
  39. export default App; 

可以看到,主要包含两大组件: State和Log,分别对应两个 Tab 栏。具体的代码大家去参考仓库即可,下面放出效果图:



三. GUI 如何实时展示业务状态?

现在问题就来了,文件操作的逻辑开发完了,GUI 界面也搭建好了。那么现在如何将两者结合起来呢,也就是 GUI 如何实时地展示文件操作的状态呢?

对此,我们需要引入第三方,来进行这两个模块的通信。具体来讲,我们在文件操作的逻辑中维护一个 EventBus 对象,然后在 React 组件当中,通过 Context 的方式传入这个 EventBus。从而完成 UI 和文件操作模块的通信。

现在我们开发一下这个 EventBus 对象,也就是下面的FileCopyConsumer:

  1. export interface EventData { 
  2.   kind: string; 
  3.   payload: any
  4.  
  5. export class FileCopyConsumer { 
  6.  
  7.   private callbacks: Function[]; 
  8.   constructor() { 
  9.     this.callbacks = [] 
  10.   } 
  11.   // 供 React 组件绑定回调 
  12.   onEvent(fn: Function) { 
  13.     this.callbacks.push(fn); 
  14.   } 
  15.   // 文件操作完成后调用 
  16.   onDone(event: EventData) { 
  17.     this.callbacks.forEach(callback => callback(event)) 
  18.   } 

接着在文件操作模块和 UI 模块当中,都需要做响应的适配,首先看看文件操作模块,我们做一下封装。

  1. export class FileOperator { 
  2.   fileConsumer: FileCopyConsumer; 
  3.   srcPath: string; 
  4.   targetPath: string; 
  5.   constructor(srcPath ?: string, targetPath ?: string) { 
  6.     // 初始化 EventBus 对象 
  7.     this.fileConsumer = new FileCopyConsumer(); 
  8.     this.srcPath = srcPath ?? join(process.cwd(), 'src'); 
  9.     this.targetPath = targetPath ?? join(process.cwd(), 'dist'); 
  10.   } 
  11.  
  12.   async copyFiles() { 
  13.     // 存储 log 信息 
  14.     const stats = []; 
  15.     // 在 src 中搜索文件 
  16.     const staticFiles = ... 
  17.      
  18.     await Promise.all(staticFiles.map(file => { 
  19.         // ... 
  20.         // 存储 log 
  21.         .then(() => stats.push(`Copied file from [${file}] to [${targetFilePath}]`)); 
  22.     })) 
  23.     // 调用 onDone 
  24.     this.fileConsumer.onDone({ 
  25.       kind: "finish"
  26.       payload: stats 
  27.     }) 
  28.   } 

然后在初始化 FileOperator之后,将 fileConsumer通过 React Context 传入到组件当中,这样组件就能访问到fileConsumer,进而可以进行回调函数的绑定,代码演示如下:

  1. // 组件当中拿到 fileConsumer & 绑定回调 
  2. export const State: FC<{}> = () => { 
  3.   const context = useContext(Context); 
  4.   const [finish, setFinish] = useState(false); 
  5.   context?.fileConsumer.onEvent((data: EventData) => { 
  6.     // 下面的逻辑在文件拷贝完成后执行 
  7.     if (data.kind === 'finish') { 
  8.       setTimeout(() => { 
  9.         setFinish(true
  10.       }, 2000) 
  11.     } 
  12.   }) 
  13.  
  14.   return  
  15.   //(JSX代码) 

这样,我们就成功地将 UI 和文件操作逻辑串联了起来。当然,篇幅所限,还有一些代码并没有展示出来,完整的代码都在 git 仓库当中。希望大家能 fork 下来好好体会一下整个项目的设计。

总体来说,React 组件代码能够跑在命令行终端,确实是一件激动人心的事情,给前端释放了更多想象的空间。本文对于这个能力的使用也只是冰山一角,更多使用姿势等待你去解锁,赶紧去玩一玩吧!

 

责任编辑:姜华 来源: 前端三元同学
相关推荐

2021-07-29 09:07:44

React视图库Web 开发

2022-01-04 09:02:24

浏览器命令行ttyd

2009-03-01 22:09:08

LinuxTerminal命令行终端

2020-07-30 13:34:48

终端命令行Linux

2014-02-12 13:30:16

Linux命令行终端工具

2023-03-28 08:40:22

命令行JSON用法

2020-11-23 05:50:40

浏览器Web浏览器Linux

2011-09-05 15:09:07

Ubuntuw3m

2020-02-17 11:05:27

GitHub代码开发者

2021-07-15 13:32:12

Linux生成密码

2019-12-09 09:23:04

Linux命令sort

2021-07-15 13:25:43

LinuxPDF

2023-02-26 01:28:09

终端命令行工具

2013-01-29 14:08:58

UbuntuUbuntu手机Ubuntu手机操作系

2014-09-11 10:33:06

Linux

2009-02-18 20:20:40

autojump更改目录命令行下

2009-10-26 18:09:31

Oracle用户解锁

2014-02-12 10:11:08

扫描病毒扫描Clam Antivi

2009-05-30 09:26:38

AndroidGoogle移动OS

2020-12-11 06:44:16

命令行工具开发
点赞
收藏

51CTO技术栈公众号