在命令行里也能用 React

Shenfq
English

用过 React 的同学都知道,React 作为一个视图库,在进行 Web 开发的时候需要安装两个模块。

npm install react --save
npm install react-dom --save

react 模块主要提供了组件的生命周期、虚拟 DOM Diff、Hooks 等能力,以及将 JSX 转换为虚拟 DOM 的 h 方法。而 react-dom 主要对外暴露一个 render 方法,将虚拟 DOM 转化为真实 DOM。

import React from 'react'
import ReactDOM from 'react-dom'
/* import ReactDOM from 'react-dom/server' //服务的渲染 */

class Hello extends React.component {
  render() {
    return <h1>Hello, world!</h1>,
  }
}

ReactDOM.render(
  <Hello />,
  document.getElementById('root')
)

如果我们将 react-dom 换成 react-native 就可以将虚拟 DOM 转换为安卓或 iOS 的原生组件。我在之前的文章中介绍过,虚拟 DOM 最大的优势并不是其 Diff 算法,而是将 JSX 转换为统一的 DSL,通过其抽象能力实现了跨平台的能力。除了官方提供的 react-domreact-native ,甚至可以渲染到命令行上,这也是我们今天介绍的 ink

🔗 npm ink: https://www.npmjs.com/package...

Ink

ink 内部使用 facebook 基于 C++ 开发的一款跨平台渲染引擎 yoga,支持 Flex 布局,功能十分强大。另外,React Native 内部使用了该引擎。

初始化

这里有一个官方提供的脚手架,我们可以直接通过这个脚手架来创建一个项目。

$ mkdir ink-app
$ cd ink-app
$ npx create-ink-app

如果你想使用 TypeScript 来编写项目,你也可以使用如下命令:

$ npx create-ink-app --typescript

生成的代码如下:

// src/cli.js
#!/usr/bin/env node
const ink = require('ink')
const meow = require('meow')
const React = require('react')
const importJsx = require('import-jsx')

const ui = importJsx('./ui')

const cli = meow(`
    Usage
      $ ink-cli
    Options
        --name  Your name
`)

ink.render(React.createElement(ui, cli.flags))
// src/ui.js
const App = (props) => (
  <Text>
    Hello, <Text color = "green">
          { props.name || 'UserName' }
      </Text>
  </Text>
)

module.exports = App;

除了 inkreact,脚手架项目还引入了 meowimport-jsx 两个库。

meow 的主要作用是运行命令时,对参数进行解析,将解析的参数放到 flags 属性中,其作用与 yargscommander 一样,是构建 CLI 工具的必备利器。

const meow = require('meow')
// 传入的字符串,作为 help 信息。
const cli = meow(`
    Options
        --name  Your name
        --age   Your age
`)
console.log('flags: ', cli.flags)

另一个 import-jsx 的主要作用,就是将 jsx 字符串转化为 createElement 方法的形式。

// ui.js
const component = (props) => (
  <Text>
    Hello, <Text color = "green">
          { props.name || 'UserName' }
      </Text>
  </Text>
)

// cli.js
const importJsx = require('import-jsx')
const ui = importJsx('./ui')

console.log(ui.toString()) // 输出转化后的结果
// 转化结果:
props => /*#__PURE__*/React.createElement(
  Text,
  null,
  "Hello, ",
  /*#__PURE__*/React.createElement(
    Text, {
      color: "green"
    },
    props.name || 'UserName'
     )
)

这一步的工作一般由 babel 完成,如果我们没有通过 babel 转义 jsx,使用 import-jsx 就相当于是运行时转义,对性能会有损耗。但是,在 CLI 项目中,本身对性能要求也没那么高,通过这种方式,也能更快速的进行项目搭建。

内置组件

由于是非浏览器的运行环境,inkreact-native 一样提供了内置的一些组件,用于渲染终端中的特定元素。

\<Text\>

<Text> 组件用于在终端渲染文字,可以为文字指定特定的颜色、加粗、斜体、下划线、删除线等等。

DEMO:

// ui.js
const React = require('react')
const { Text } = require('ink')
moudle.exports = () => (<>
  <Text>I am text</Text>
  <Text bold>I am bold</Text>
  <Text italic>I am italic</Text>
  <Text underline>I am underline</Text>
  <Text strikethrough>I am strikethrough</Text>
  <Text color="green">I am green</Text>
  <Text color="blue" backgroundColor="gray">I am blue on gray</Text>
</>)

// cli.js
const React = require('react')
const importJsx = require('import-jsx')
const { render } = require('ink')

const ui = importJsx('./ui')
render(React.createElement(ui))

其主要作用就是设置渲染到终端上的文本样式,有点类似于 HTML 中的 <font> 标签。

除了这种常见的 HTML 相关的文本属性,还支持比较特殊的 wrap 属性,用于将溢出的文本进行截断。

长文本在超出终端的长度时,默认会进行换行处理。

<Text>loooooooooooooooooooooooooooooooooooooooong text</Text>

如果加上 wrap 属性,会对长文本进行截断。

<Text wrap="truncate">
  loooooooooooooooooooooooooooooooooooooooong text
</Text>

除了从尾部截断文本,还支持从文本中间和文本开始处进行截断。

<Text wrap="truncate">
  loooooooooooooooooooooooooooooooooooooooong text
</Text>
<Text wrap="truncate-middle">
  loooooooooooooooooooooooooooooooooooooooong text
</Text>
<Text wrap="truncate-start">
  loooooooooooooooooooooooooooooooooooooooong text
</Text>

\<Box\>

<Box> 组件用于布局,除了支持类似 CSS 中 marginpaddingborder 属性外,还能支持 flex 布局,可以将 <Box> 理解为 HTML 中设置了 flex 布局的 div ( <div style="display: flex;">)。

下面我们先给一个 <Box> 组件设置高度为 10,然后主轴方向让元素两端对齐,交叉轴方向让元素位于底部对齐。

然后在给内部的两个 <Box> 组件设置一个 padding 和一个不同样式的边框。

const App = () => <Box
  height={10}
  alignItems="flex-end"
  justifyContent="space-between"
>
    <Box borderStyle="double" borderColor="blue" padding={1} >
    <Text>Hello</Text>
  </Box>
    <Box borderStyle="classic"  borderColor="red" padding={1} >
      <Text>World</Text>
  </Box>
</Box>

最终效果如下:

比较特殊的属性是边框的样式: borderStyle,和 CSS 提供的边框样式有点出入。

<Box borderStyle="single">
  <Text>single</Text>
</Box>
<Box borderStyle="double">
  <Text>double</Text>
</Box>
<Box borderStyle="round">
  <Text>round</Text>
</Box>
<Box borderStyle="bold">
  <Text>bold</Text>
</Box>
<Box borderStyle="singleDouble">
  <Text>singleDouble</Text>
</Box>
<Box borderStyle="doubleSingle">
  <Text>doubleSingle</Text>
</Box>
<Box borderStyle="classic">
  <Text>classic</Text>
</Box>

<Box> 组件提供的其他属性和原生的 CSS 基本一致,详细介绍可以查阅其文档:

🔗 ink#Box:https://www.npmjs.com/package/ink#box

\<Newline\>

<NewLine> 组件相当于直接在终端中添加一个 \n 字符,用于换行(PS:只支持插入在 <Text> 元素之间);

const App = () => (<>
  <Text>Hello</Text>
  <Text>World</Text>
</>)

const App = () => (<>
  <Text>Hello</Text>
  <Newline />
  <Text>World</Text>
</>)

\<Spacer\>

<Spacer> 组件用于隔开两个元素,使用后,会将间隔开两个元素隔开到终端的两边,效果有点类似于 flex 布局的两端对齐(justify-content: space-between;

const App1 = () => <Box>
  <Text>Left</Text>
  <Spacer />
  <Text>Right</Text>
</Box>;

const App2 = () => <Box justifyContent="space-between">
  <Text>Left</Text>
  <Text>Right</Text>
</Box>;

上面两段代码的表现形式一致:

内置 Hooks

ink 除了提供一些布局用的组件,还提供了一些 Hooks。

useInput

可用于监听用户的输入,useInput 接受一个回调函数,用户每次按下键盘的按键,都会调用 useInput 传入的回调,并传入两个参数。

useInput((input: string, key: Object) => void)

第一个参数:input ,表示按下按键对应的字符。第二个参数: key ,为一个对象,对应按下的一些功能键。

  • 如果按下回车,key.return = true
  • 如果按下删除键,key.delete = true
  • 如果按下esc键,key.escape = true

具体支持哪些功能按键,可以参考官方文档:

🔗ink#useInput:https://www.npmjs.com/package/ink#useinputinputhandler-options

下面通过一个 DEMO,展示其具体的使用方式,在终端上记录用户的所有输出,如果按下的是删除键,则删除最近记录的一个字符。

const React = require('react')
const { useInput, Text } = require('ink')

const { useState } = React
module.exports = () => {
  const [char, setChar] = useState('')
  useInput((input, key) => {
    if (key.delete) {
      // 按下删除键,删除一个字符
      setChar(char.slice(0, -1))
      return
    }
    // 追加最新按下的字符
    setChar(char + input)
  })
  return <Text>input char: {char}</Text>
}

useApp

对外暴露一个 exit 方法,用于退出终端。

const React = require('react')
const { useApp } = require('ink')

const { useEffect } = React
const App = () => {
  const { exit } = useApp()

    // 3s 后退出终端
    useEffect(() => {
        setTimeout(() => {
            exit();
        }, 3000);
    }, []);

    return <Text color="red">3s 后退出终端……</Text>
}

useStdin

用于获取命令行的输入流。这里用一个简单的案例,来模拟用户登录。

const React = require('react')
const { useStdin } = require('ink')
const { useState, useEffect } = React
module.exports = () => {
  const [pwd, setPwd] = useState('')
  const { stdin } = useStdin()
  
  useEffect(() => {
    // 设置密码后,终止输入
    if (pwd) stdin.pause()
    }, [pwd])
  
  stdin.on('data', (data) => {
    // 提取 data,设置到 pwd 变量中
    const value = data.toString().trim()
    setPwd(value)
  })
  // pwd 为空时,提示用户输入密码
  if (!pwd) {
    return <Text backgroundColor="blue">password:</Text>
  }

  return pwd === 'hk01810'
    ? <Text color="green">登录成功</Text>
    : <Text color="red">有内鬼,终止交易</Text>
}

useStdout

用于获取命令行的输出流。会暴露 stdout 的写入流,还会暴露一个 write 方法,用于在终端进行输入。

const React = require('react')
const { useStdout } = require('ink')
const { useEffect } = React
module.exports = () => {
  const { write } = useStdout()
  useEffect(() => {
    // 在终端进行写入
        write('Hello from Ink to stdout')
    }, [])
  return null
}

第三方组件

除了内置的这些组件和 Hooks 外,还有丰富的第三方生态。比如:Loading组件、超链接组件、表格组件、高亮组件、多选组件、图片组件……

🔗 ink#第三方组件:https://www.npmjs.com/package/ink#useful-components

ink-spinner

ink-link

ink-table

ink-syntax-highlight

ink-muti-select

调试工具

ink 属于 React 生态,自然能够支持 React 官方提供的调试工具 React Devtools

$ npm install react-devtools # 安装调试工具
$ npx react-devtools # 启动调试工具

然后,在启动应用时,在前面设置 DEV 全局变量。

DEV=true node src/cli

运行后的效果如下:

总结

React 确实是视图开发的一把利器,再加上 Hooks 的加持,其抽象能力得到了进一步的提升,统一的 DSL 加上 虚拟 DOM,照理来说,是可以在任何平台进行渲染的。甚至,微软官方都开发了一个 React Native for Windows,关键是这个东西不仅仅能开发 Windows 的桌面软件,还可以开发 mac 的桌面软件。

有点跑题,说回 ink,大家熟知的 Gatsby 的命令行工具也是通过 ink 进行开发的。如果大家后续有本地的 CLI 工具需要实现,可以考虑这款工具,至少不必烦恼如何在命令行进行文本对齐。

阅读 658

自然醒的笔记本
学习过程中的一些总结和沉淀,欢迎关注公众号「自然醒的笔记本」
3.6k 声望
4.6k 粉丝
0 条评论
你知道吗?

3.6k 声望
4.6k 粉丝
宣传栏