博客 / 詳情

返回

在命令行裏也能用 React

用過 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 工具需要實現,可以考慮這款工具,至少不必煩惱如何在命令行進行文本對齊。

user avatar xiangjiaochihuanggua 頭像 peter-wilson 頭像 coderleo 頭像 _raymond 頭像 sunhengzhe 頭像 zhangxishuo 頭像 columsys 頭像 buxia97 頭像 jianqiangdepaobuxie 頭像 suporka 頭像 gaoming13 頭像 pugongyingxiangyanghua 頭像
69 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.