我们每天都在用 console.log 打印日志,一般都是从上到下依次打印的。但很多 Node.js 工具可以在任意的位置打印内容:
比如 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 光标位置的方法:
擦除终端内容的方法:
我们用一下:
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 后修改了第二行的内容。
接下来我们再来看 ANSI 支持的颜色打印:
打印颜色是这样的格式:
除了 ESC[ 外,还要加上前景色、背景色、加粗、下划线等的设置,用 ; 分割,最后用 m 表示结束。
console.log('\u001b[36;1;4mguang');
36 表示前景色为青色、1 表示加粗、4 表示下划线。
当然,加了样式还要去掉,再加一个 \u001b[0m 就可以了。
console.log('\u001b[36;1;4mguang\u001b[0m 666');
显然,自己去打印这些颜色也不现实,我们会用一些库来做:
比如 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!'))
比如我们用 eslint 之类的包会打印这样的错误消息:
被高亮过以后的代码是这样的:
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 工具也会大量用到。