nodejs API 学习1:ANSI 字符

12 阅读2分钟

我们每天都在用 console.log 打印日志,一般都是从上到下依次打印的。但很多 Node.js 工具可以在任意的位置打印内容:

image.png

比如 npkill 这个包,它就可以把文件夹大小异步的打印在对应行的右边。

而且还能显示颜色。

都是 console.log,咋做到的呢?

这就涉及到 ANSI 字符了。

其实终端比图形界面出现的早得多,那时候所有的交互都是在终端上进行的。

如果只能从上到下依次打印,还不支持颜色,想象一下,得多难用。

所以 1970 年左右,就出现了 ANSI 标准。

那个年代就是靠这种 ANSI 控制字符移动光标。

它并不像 aaa、bbb 这样会展示对应的字符,而是遇到就会解释为指令,执行相应的代码。

Unicode 字符 和 ASCII 字符

ASCII:美国信息交换标准代码,诞生于 1960 年代,只给英语用

一共 128 个字符,范围:0 ~ 127

  • 大小写英文字母:A-Z, a-z
  • 数字:0-9
  • 常见符号:! @ # $ % ^ & * ( )
  • 控制字符:回车、换行、ESC。

它体积小,只支持英文,不支持中文、日文、阿拉伯文等任何非英语文字

ASCII 字符中每一个字符都对应一个数字,数字可以用01这样的二进制来表示,这样字符就可以用二进制表示了。

A -> 65 
B -> 66 
\n -> 10 
ESC-> 27

Unicode:统一码、万国码,为了解决:世界上所有语言在电脑里乱码的问题。

它把地球上所有文字、符号、表情都编了一个唯一编号。

ASCII 完全包含在 Unicode 里,所有 ASCII 字符,都是 Unicode 字符。

JS中如何表示这两种字符呢?

\x## :表示 ASCII 字符,只能写 2 位十六进制,范围:\x00 ~ \xFF

'\x1B' // 正确,表示 ESC(ASCII 27) 
'\x41' // A

\u#### :表示 Unicode 字符,必须写 4 位十六进制,范围:全世界所有文字。

'\u001B' // ESC(和 \x1B 完全一样) 
'\u0041' // A 
'\u4E2D' // 中 
'\u6587' // 文

为什么字符前面必须加 \ 斜杠?

斜杠 \ 是转义开始标记,它告诉 JS,后面的字符不是普通文字,是特殊指令!

不加 \ 会发生什么?

console.log( 'x1B' );  输出:x1B (普通文字)
console.log( 'u001B' );  输出:u001B (普通文字)

加了 \ 就完全变了:

console.log('\u6587') // 输出:文

JS 看到 \ 立刻明白:哦!后面不是普通文字,是转义序列。

转义 = 改变原来的意思

  • 普通 x = 字母 x
  • 加斜杠 \x = 十六进制标记
  • 普通 n = 字母 n
  • 加斜杠 \n = 换行
  • 普通 u = 字母 u
  • 加斜杠 \u = Unicode 编码

为什么 console.log("\x1B"); 什么都不显示?

因为 \x1B = ESC 字符 = 控制字符 = 没有形状、没有图案、看不见!

它不是字母 A、不是数字 1、不是符号。

它是给终端看的指令不是给人看的文字

怎么证明它真的存在?

很简单:给它加一个 ANSI 指令,它就会干活:

// 只打印 ESC,看不见 
console.log("直接打印:", "\x1B"); 
// 打印 ESC + [32m(绿色指令),立刻生效! 
console.log("带指令打印:", "\x1B[32m我变成绿色了\x1B[0m");

\x1B 真的在里面,只是它隐身了。

同理,\n 你也看不见,但它会换行

\r 就是光标回到当前行的最开头,但不换行。

ANSI 控制指令

ANSI 控制指令都是 ESC + [ 开头的。

  • ESC(\x1B / \u001B): 告诉终端:准备接收指令!

  • [ : 告诉终端:接下来是 ANSI 控制指令!

最常见的 ANSI 指令:

\x1B[0K 删除到行尾 
\x1B[1K 删除到行首 
\x1B[2K 删除整行 
\x1B[31m 红色文字 
\x1B[32m 绿色文字 
\x1B[0m 恢复默认颜色 
\x1B[10D 光标左移10格 
\x1B[20C 光标右移20

平时我们经常用到 ANSI 控制字符来修改光标位置,但是直接写那些字符很麻烦,我们会用封装好的包来做,ansi-escapes 这个包来做。

它封装了修改 cursor 光标位置的方法:

image.png

擦除终端内容的方法:

image.png

我们用一下:

npm install --save ansi-escapes
import ansiEscapes from 'ansi-escapes'

const log = process.stdout.write.bind(process.stdout)

// cursorTo 第一个参数是列号、第二个参数是行号。
log(ansiEscapes.cursorTo(10, 1) + '111')
log(ansiEscapes.cursorTo(7, 2) + '222')
log(ansiEscapes.cursorTo(5, 3) + '333')

setTimeout(() => {
  log(ansiEscapes.cursorTo(0, 2) + ansiEscapes.eraseEndLine)
  log(ansiEscapes.cursorTo(5, 3) + '444')
}, 1000)

console.log 会打印一个换行符,所以我们用 processs.stdout.write 打印。

cursorTo 第一个参数是列号、第二个参数是行号。

可以看到,在不同位置打印了内容,并且 1s 后修改了第二行的内容。

image.png

接下来我们再来看 ANSI 支持的颜色打印:

打印颜色是这样的格式:

image.png

除了 ESC[ 外,还要加上前景色、背景色、加粗、下划线等的设置,用 ; 分割,最后用 m 表示结束。

console.log('\u001b[36;1;4mguang');

36 表示前景色为青色、1 表示加粗、4 表示下划线。

image.png

当然,加了样式还要去掉,再加一个 \u001b[0m 就可以了。

console.log('\u001b[36;1;4mguang\u001b[0m 666');

image.png

显然,自己去打印这些颜色也不现实,我们会用一些库来做:

比如 chalk

chalk 的不同方法就是封装了这些 ASCII 码的颜色控制字符。

试一下:

npm install --save chalk
import chalk from 'chalk'

const log = console.log

log(chalk.blue('Hello') + ' World' + chalk.red('!'))
log(chalk.blue.bgRed.bold('Hello world!'))
log(chalk.blue('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz'))
log(chalk.red('Hello', chalk.underline.bgBlue('world') + '!'))
log(
  chalk.green(
    'I am a green line ' +
      chalk.blue.underline.bold('with a blue substring') +
      ' that becomes green again!',
  ),
)

log(`
    CPU: ${chalk.red('90%')}
    RAM: ${chalk.green('40%')}
    DISK: ${chalk.yellow('70%')}
`)

log(chalk.rgb(123, 45, 67).underline('Underlined reddish color'))
log(chalk.hex('#DEADED').bold('Bold gray!'))

image.png

比如我们用 eslint 之类的包会打印这样的错误消息:

image.png

被高亮过以后的代码是这样的:

image.png

node:readline API

node 的 readline 包 也能控制光标的位置,和擦除光标处的位置。

import readline from 'node:readline'

// 拿到当前终端能显示的行数 process.stdout.rows
const repeatCount = process.stdout.rows - 2
// 打印这么多空行
console.log(repeatCount)
const blank = repeatCount > 0 ? '\n'.repeat(repeatCount) : ''
console.log(blank)
// 然后移动 cursor 到 0 行 0 列的位置,之后清除下面的内容:
readline.cursorTo(process.stdout, 0, 0)
// readline.clearScreenDown(process.stdout) 会清除光标所在行以下的内容
readline.clearScreenDown(process.stdout)

显然,readline 的底层也是通过 ANSI 的控制字符来修改的光标位置和清除内容的。

总结

我们每天都在用各种 cli 工具,它们打印的内容和我们 console.log 的不一样,可以在各处打印、可以打印各种颜色等。

原理就是 ANSI 的转义字符,这个是 1970 年左右就有的标准,历史悠久。

当然,自己写这种转义字符比较麻烦,我们会用 node 的 readline 包、三方的 ansi-escapes 包来做光标控制,用 chalk 包来实现颜色打印。

ANSI 转义字符我们每天都在用,后面写 Node.js 工具也会大量用到。