哇擦!他竟然把 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 交流,也欢迎你们一块儿来搞事情。

相关文章
相关标签/搜索