chalk.js源码分析

nodejs如何在终端输出带颜色的命令行

参考:
colors源码
ansi-styles源码
chalk源码
ANSI转义字符
通过控制台输出各种颜色的字符——ANSIConsole、JANSI
知乎 ~ Node.js Color 模块实现入门浅析

已有实现方案

colorschalkcli-color

ANSI控制码(ANSI escape code

ANSI 具体介绍可查看维基百科)的解释。

如果要实现控制终端输出文字颜色或者背景色等,首先需要了解ANSI控制码。

ANSI控制码用于在字符显示系统中控制光标移动和字符色彩等,常用于BBS系统中。

ANSI ESCAPE SEQUENCES又称为VT100系列控制码,国内译为ANSI控制码。顾名思义,需要VT100系列终端的支持,当然现在已经不在局限于VT100了,包括xterm,linux都能很好完成。

ANSI控制码开始的标志都为ESC[,ESC对应ASCII码表的033(八进制),linux命令echo用-e启用转义,\033来输入ESC,\033[31m即为ESC[31m。

但是,通常在使用时通常用十六进制来表示ESC,即\u001b。

注意:不同的终端对ANSI控制码的支持度不一样,所以不能保证所有系统上都可以实现效果。

ASCII控制字符
二进制 十进制 十六进制 缩写 可以显示的表示法 名称/意义
0000 0000 0 00 NUL 空字符(Null)
0000 0001 1 01 SOH 标题开始
0000 0010 2 02 STX 本文开始
0000 0011 3 03 ETX 本文结束
0000 0100 4 04 EOT 传输结束
0000 0101 5 05 ENQ 请求
0000 0110 6 06 ACK 确认回应
0000 0111 7 07 BEL 响铃
0000 1000 8 08 BS 退格
0000 1001 9 09 HT 水平定位符号
0000 1010 10 0A LF 换行键
0000 1011 11 0B VT 垂直定位符号
0000 1100 12 0C FF 换页键
0000 1101 13 0D CR 归位键
0000 1110 14 0E SO 取消变换(Shift out)
0000 1111 15 0F SI 启用变换(Shift in)
0001 0000 16 10 DLE 跳出数据通讯
0001 0001 17 11 DC1 设备控制一(XON 启用软件速度控制)
0001 0010 18 12 DC2 设备控制二
0001 0011 19 13 DC3 设备控制三(XOFF 停用软件速度控制)
0001 0100 20 14 DC4 设备控制四
0001 0101 21 15 NAK 确认失败回应
0001 0110 22 16 SYN 同步用暂停
0001 0111 23 17 ETB 区块传输结束
0001 1000 24 18 CAN 取消
0001 1001 25 19 EM 连接介质中断
0001 1010 26 1A SUB 替换
0001 1011 27 1B ESC 跳出
0001 1100 28 1C FS 文件分割符
0001 1101 29 1D GS 组群分隔符
0001 1110 30 1E RS 记录分隔符
0001 1111 31 1F US 单元分隔符
0111 1111 127 7F DEL 删除

通过阅读下面的文章可以知道,其实就是通过控制码进行样式渲染。

可参阅: 通过Ansi Escape Codes酷炫玩转命令行!
原文地址:http://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html

编码延伸阅读

chalk源码分析

版本:2.4.1

为什么要解读chalk,通过查看上面介绍的几个实现方案,chalk是有对ansi颜色输出支持的判断,所以兼容性处理更好。

chalk包含了几个重要依赖:

  • escape-string-regexp — 字符串转义功能
    对特殊字符进行转义,e.g. ‘$’ => ‘\$’
  • supports-color — 判断当前系统终端支持的颜色(8色、16色、256色)
  • ansi-styles — ANSI控制码对应的颜色值

先对chalk的整体架构进行了整理,源码很少,但是涉及到原型的东西蛮多的,容易弄混,所以整理成图:
chalk原型架构

现在分析下源码:

1. module.exports
1
2
3
4
// 实际输出的是chalk.template,看第2点
module.exports = Chalk(); // eslint-disable-line new-cap
module.exports.supportsColor = stdoutColor;
module.exports.default = module.exports; // For TypeScript
2. Chalk定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 定义Chalk, 生成chalk与chalk.template
// 定义原型指向,如上图左侧部分
function Chalk(options) {
// We check for this.template here since calling `chalk.constructor()`
// by itself will have a `this` of a previously constructed chalk object
if (!this || !(this instanceof Chalk) || this.template) {
const chalk = {};
applyOptions(chalk, options);

chalk.template = function () {
const args = [].slice.call(arguments);
return chalkTag.apply(null, [chalk.template].concat(args));
};

// 修改原型指向
Object.setPrototypeOf(chalk, Chalk.prototype);
Object.setPrototypeOf(chalk.template, chalk);

// 定义constructor,用于生成新的Chalk实例
// e.g. const ctx = new chalk.constructor({enabled: false});
chalk.template.constructor = Chalk;

return chalk.template;
}

applyOptions(this, options);
}
3. styles

styles可以说是所有使用方法的集合,因为所有的使用方法都是通过Object.defineProperties(xxx, styles)生成的。

styles到底长什么样呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// styles结构示例
{
red: {
get() {
return build.call(...);
}
},
black: {
get() {
return build.call(...);
}
},
// 颜色转换方法与直接取颜色值使用方法不同,结构也有所差异
ansi: {
get() {
return function() {
return build.call(...);
}
}
}
...
}

下图是在vsCode中断点截取的styles的构造
styles

我们在使用chalk时的一个例子就是console.log(clalk.red('Hello')),通过上面的原型结构我们知道chalk.red其实就是调用了Chalk.prototype.red,那具体做了什么呢,接着看源码第224行:

1
Object.defineProperties(Chalk.prototype, styles);

其实为了将styles的所有方法实现在原型上,所以作者才用get(){}属性的,如果不懂的话去查defineProperties方法。

4.styles由来

从源码55 ~ 115行,可以看出,ansi-styles模块就是其由来(也就是源码开头定义的ansiStyles)。
这个模块定义了每个颜色对应的ANSI控制码,并从color-convert模块生成了一系列颜色转换工具函数,也就是chalk中的hex()、rbg()等方法的由来;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ansiStyles结构
{
// 颜色
bgBlack: {
open: '\u001b[40m',
close: '\u001b[49m',
closeRe: /\\u001b\[49m/g // 这个是chalk源码94行添加的,不属于ansi-styles模块自有
},
...
// color模块 指ANSI控制码 31-37
color: {
...
},
// bgColor模块 指ANSI控制码 40-47
bgColor: {
...
},
// modifier模块 指ANSI控制码 0-8
modifier: {
bold: {
...
},
...
}
6. 链式操作的实现

console.log(chalk.red.underline('Hello'))
这种操作是如何实现的呢
可以看下最初的图,其实里面已经描述的很清楚了,下面从源码讲解下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 1. styles每个方法的定义
styles[key] = {
get() {
const codes = ansiStyles[key];
// 每个方法其实都返回了build函数的执行结果,即builder
return build.call(this, this._styles ? this._styles.concat(codes) : [codes], this._empty, key);
}
};

// 2. 定义builder的原型

const proto = Object.defineProperties(() => {}, styles);

function build(_styles, _empty, key) {
const builder = function () {
return applyStyle.apply(builder, arguments);
};
...
// `__proto__` is used because we must return a function, but there is
// no way to create a function with a different prototype
// builder的原型指向为proto
builder.__proto__ = proto; // eslint-disable-line no-proto

return builder;
}

// 3. 生成Chalk原型
// 可以看出proto和Chalk其实一模一样
Object.defineProperties(Chalk.prototype, styles);

// 总结
// 因为每个builder方法原型都指向了proto,proto中的方法又都是builder函数,即原型又指向proto,构成无限循环,所以可以无限链式调用
chalk.red.underline -> Chalk.prototype.red -> builder方法(即red方法) -> builder原型指向proto -> builder.__proto__.underline

7. 返回

最终调用返回结果就是返回了一个包含ANSI控制码的字符串
e.g. "\u001b[31m\u001b[4m Hello \u001b[24m\u001b[39m"(红色带下划线)

其实就是在每次调用后再this._styles结构中增加了每种修饰的open与close