12

Anyone who has used React knows that React, as a view library, requires two modules to be installed during web development.

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

react module mainly provides the life cycle of components, virtual DOM Diff, Hooks and other capabilities, as well as the h method of converting JSX to virtual DOM. The react-dom mainly exposes a render method to convert the virtual DOM into the real 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')
)

If we change react-dom to react-native we can convert the virtual DOM into native Android or iOS components. As I introduced in the article , the biggest advantage of virtual DOM is not its Diff algorithm, but the conversion of JSX into a unified DSL, which achieves cross-platform capabilities through its abstraction capabilities. In addition to the official react-dom and react-native , it can even be rendered on the command line. This is also the ink we introduced today.

🔗 npm ink: https://www.npmjs.com/package/react-dom

Ink

ink internal use facebook on the development of a C ++ cross-platform rendering engine yoga , support Flex layout, is very powerful. In addition, React Native uses this engine internally.

initialization

There is an official scaffolding, we can create a project directly through this scaffolding.

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

If you want to use TypeScript to write projects, you can also use the following commands:

$ npx create-ink-app --typescript

The generated code is as follows:

// 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;

In addition to ink and react , scaffolding project also introduces meow , import-jsx two libraries.

meow is to parse the parameters when running the command, and put the parsed parameters into the flags attribute. Its function is the yargs and commander . It is an essential tool for building CLI tools.

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

Another import-jsx principal role is to jsx string into createElement version of the method.

// 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'
     )
)

This step is generally done by babel. If we do not escape import-jsx through babel, using 06108adc0e8917 is equivalent to runtime escaping, which will cause performance loss. However, in the CLI project, the performance requirements are not so high. In this way, the project can be built more quickly.

Built-in components

Because it is a non-browser operating environment, ink and react-native provide some built-in components for rendering specific elements in the terminal.

\<Text\>

<Text> component is used to render text on the terminal. You can specify a specific color, bold, italic, underline, strikethrough, etc. for the 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))

Its main function is to set the style of the text rendered on the terminal, which is a bit similar to the <font> tag in HTML.

In addition to this common HTML-related text attribute, it also supports the special wrap attribute for truncating the overflowing text.

When long text exceeds the length of the terminal, it will wrap by default.

<Text>loooooooooooooooooooooooooooooooooooooooong text</Text>

If the wrap attribute is added, the long text will be truncated.

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

In addition to truncating the text from the end, it also supports truncation from the middle of the text and the beginning of the text.

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

\<Box\>

<Box> component is used for layout. In addition to supporting margin , padding , border CSS, it also supports flex layout. <Box> can be understood as a div with a flex layout in HTML ( <div style="display: flex;"> ).

Below we first <Box> component to 10, and then the main axis direction makes the two ends of the element aligned, and the cross axis direction makes the element aligned at the bottom.

Then set a padding and a different style of border to the two <Box>

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>

The final effect is as follows:

The more special attribute is the style of the border: borderStyle , which is a bit different from the border style provided by 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> component are basically the same as those of the native CSS. For detailed introduction, please refer to its documentation:

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

\<Newline\>

<NewLine> component is equivalent to directly adding a \n character to the terminal for line feed (PS: only supports insertion between <Text> elements);

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

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

\<Spacer\>

<Spacer> component is used to separate two elements. After use, the spaced two elements will be separated to the two sides of the terminal. The effect is somewhat similar to the alignment of the two ends of the flex layout ( 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>;

The expressions of the above two pieces of code are the same:

Built-in Hooks

ink addition to some layout components.

useInput

It can be used to monitor the user's input. useInput accepts a callback function. Each time the user presses a key on the keyboard, it will call useInput and pass in two parameters.

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

The first parameter: input, which represents the character corresponding to the key pressed. The second parameter: key, is an object, corresponding to some function keys pressed.

  • If you press Enter, key.return = true ;
  • If you press the delete key, key.delete = true ;
  • If you press the esc key, key.escape = true ;

For details on which function buttons are supported, please refer to the official documents:

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

Let's use a DEMO to show its specific usage. Record all the user's output on the terminal. If the delete key is pressed, the most recently recorded character will be deleted.

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 a 06108adc0e918a method to log out of the terminal.

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

Used to get the input stream of the command line. Here is a simple case to simulate user login.

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

Used to get the output stream of the command line. Will expose stdout , and also expose a write method for input at the terminal.

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
}

Third-party components

In addition to these built-in components and Hooks, there is also a rich third-party ecosystem . For example: Loading component, hyperlink component, form component, highlight component, multi-select component, picture component...

🔗 ink# Third-party components: https://www.npmjs.com/package/ink#useful-components

ink-spinner

ink-link

ink-table

ink-syntax-highlight

ink-muti-select

Debugging tools

Ink belongs to the React ecosystem and can naturally support the debugging tool React Devtools officially provided by React.

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

Then, when starting the application, set the DEV global variable in front.

DEV=true node src/cli

The effect after running is as follows:

Summarize

React is indeed a powerful tool for view development. Coupled with the blessing of Hooks, its abstraction capabilities have been further improved. A unified DSL plus a virtual DOM, logically speaking, can be rendered on any platform. Even, Microsoft has officially developed a React Native for Windows , the key is that this thing can not only develop Windows desktop software, but also Mac desktop software.

A little beside the point, he said back ink , well-known Gatsby command-line tool is by ink for development. If you have a local CLI tool that needs to be implemented in the future, you can consider this tool, at least you don't have to worry about how to align text on the command line.


Shenfq
4k 声望6.8k 粉丝