chalk 这个包相信很多人都用过,一个简单实用的控制台格式渲染工具,可以自由定义颜色、背景色以及格式等。开源在 chalk/chalk (github.com):
基本框架
这个包的代码量并不大,总共就只有三个文件:
在 index.js 做的事情就是引入了 ansistyles 这个核心依赖并构建了 chalk
对象。如果你之前了解过控制台颜色实现原理的话,就应该知道可以通过在一段文本前后添加特殊字符的形式来对其进行颜色或格式自定义,而这些特殊字符的来源就是 ansiStyles。chalk 做的事情就是对其进行封装来让使用更加方便。
而 templates.js 里实现了 chalk 的模板字符串功能,下面会详细讲解。
utils.js 就比较简单了,包含了两个函数,分别是重复渲染时避免再次添加颜色字符串,另一个则是用于修复 一个小 bug。
实现细节
1、功能懒加载
代码所在位置:chalk/index.js:63
在 chalk 上有 很多颜色方法 是可以直接调用的,但是实际上我们一般只会用到其中的某几个颜色,那在一开始就加载全部的功能岂不是有点浪费?
chalk 就使用 getter 属性实现了“功能懒加载”,具体代码如下:
for (const [styleName, style] of Object.entries(ansiStyles)) {
styles[styleName] = {
get() {
const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
Object.defineProperty(this, styleName, {value: builder});
return builder;
}
};
}
注意 get 方法里的操作,首先创建了一个 builder(实际的功能函数),然后使用 Object.defineProperty
把本属性的值重定向到这个 builder,最后返回 builder 完成 getter 的使命。这样之后再访问本方法就会直接调用功能,这个占位用的 getter 也就被丢弃了。
所以,如果我们没有调用 chalk 时,它上面的所有的功能方法都仅仅只是一个 getter 访问器,这样不仅提高了运行速度,也降低了内存占用。
2、标签模板字符串
代码所在位置:chalk/index.js:47
js 中的模板字符串可以说是非常常用,例如:
`hello ${customValue}`
但是你有想过这样的用法么?
String.raw`hello ${customValue}`
chalk`There are {bold ${aNumber} feet} in a mile.`
这种用法被称为 标签模板字符串,在模板字符串前面的东西就是标签。这种操作说白了就是 使用你提供的“标签”作为模板的解析方法,由此来实现自定义模板解析。下面简单的介绍一下用法:
首先,提供的“标签”必须是一个函数(或者给一个对象指定 template
属性,就可以直接使用这个对象进行模板解析,就像上面 chalk
做的这样),在调用之后,这个函数就会接受到如下参数:
/**
* @param {string[]} strings 被分割的模板字符串,分隔符就是模板中的 ${}
* @param {...any} keys 每个 ${} 中包含的值
*/
function template(strings, ...keys) {
// ...
}
其中第一个参数 strings 还包含一个特殊数组属性 raw
,可以访问到没有经过转义的原始字符串(会保留诸如 \n\r
之类的特殊字符)。
这个函数返回的值就会被当作模板字符串的最终值,值得注意的是,这个返回值没必要是字符串,你可以返回任何东西,例如:
function template () {
return { message: '大明天下无敌' };
}
const result = template`这个模板字符串里输入啥都没用`;
console.log(result.message); // 大明天下无敌
3、String.replace 接受函数参数
代码所在位置:chalk/templates.js:104
字符串的 replace 函数大家都用过,但是有多少人用过 replace 的回调参数呢,从 MDN 文档 里可以看到,replace 的第二个参数可以接受一个回调函数,这个函数将会在匹配成功时调用。比较特殊的是这个回调函数的参数列表:
- 参数1
match
:正则匹配到的字串 - 参数2 - 参数n:如果 replace 第一个参数是个正则表达式的话,这里的参数数量和值是相关于你正则里的捕获组的。
- 倒数第二个参数
offset
:匹配到的字符串(的开始字符)在母字符串中的索引值 - 倒数第一个参数
string
:被匹配的母字符串
而 chalk 就是在这个回调中进行了自定义模板的解析操作。
4、Object.entries()
代码所在位置:chalk/index.js:63
一个实用方法,但是我平时用的比较少,所以这里记录下,这个方法可以把一个对象转化为一个二维数组,例如:
const obj = { foo: 'bar', baz: 42, 1: 888 };
console.log(Object.entries(obj));
// [ ['foo', 'bar'], ['baz', 42], ['1', 888] ]
说白了其实是一个元组数组,元组的第一个值是属性的键,而第二个值是属性的值。这样转一下之后就可以接数组迭代器了,注意这个也是浅拷贝哦。
详细文档在 Object.entries() - JavaScript | MDN (mozilla.org),感兴趣的可以看一下。
运行流程
下面介绍一下大致的流程,总体可以分为两部分:渲染流程 和 模板字符串渲染。
1、渲染流程
这种是指通过函数方式进行的调用,例如 chalk.blue('Hello')
或者 chalk.hex('#DEADED').bold('Bold gray!')
。主要代码都在 source/index.js 里。
首先就是原型生成,经过一大堆操作之后制作了一个名为 proto
的对象,这个对象就是 chalk 的核心原型。上文中提到的一大堆懒加载的 getter 就放在这上面。在调用颜色渲染方法后首先会来到 107行。在这里调用 getModelAnsi
获取到对应颜色的特殊字符(起始字符和结束字符),然后传入 createStyler
格式化一下。最后调用 createBuilder
创建最后的渲染函数。
在 createBuilder 里创建的 builder 函数就是实际的渲染函数,其参数列表 ...arguments_
就是咱们想要渲染的内容。注意创建完成后使用了 Object.setPrototypeOf
把主原型 proto
挂了上去,由此实现了链式调用。
在渲染函数 builder 中会调用 applyStyle
函数来执行最后的字符串拼接,这个函数就比较简单了,就是使用之前 getModelAnsi
里获取的样式字符添加到内容字符串的前后就可以了。
至此,渲染流程结束,applyStyle
返回的字符串就是最终的结果。总结一下就是下面这样:
- 使用 getModelAnsi 和 createStyler 生成前后颜色字符
- 调用 createBuilder 生成渲染函数 builder
- builder 执行,触发 applyStyle 函数把颜色字符拼接到内容字符串前后。
2、模板字符串渲染
在上面的渲染函数 builder 执行过程中,会先对入参进行判断,如果发现入参格式符合模板字符串的传参,就会调起 chalkTag
函数先进行模板合成。(如果不是的话就直接 join 拼接字符串了。)
chalkTag 这个函数做的事情其实就是标准的模板字符串执行的功能,把 ${}
里的数值塞到模板里,然后再把生成的纯字符串(包含 chalk 的特殊模板语法)传递给 template 方法,接下来就是重头戏了,不过在讲解怎么进行模板合成之前,咱们先来复习一下 chalk 的模板语法:
- 使用
{}
括起来的内容将会进行颜色渲染,在大括号后要跟渲染成的内容,如:There are {bold 5280 feet} in a mile.
将把5280 feet
渲染为红色。 - 可以通过分隔符
.
指定多个格式,如there are {green.bold 5280 feet}
将会渲染成绿色粗体。
现在我们再来看怎么实现的渲染,它用的正则很复杂,咱们就不看了,直接讲流程,由于模板里可能会有多个渲染内容(每个 {} 都是一个要添加颜色的内容),所以这个渲染其实是一个如下形式的循环:
相关代码在 source/templates.js:104,也就是下面这段,可以参考下面的注释进行理解:
export default function template(chalk, temporary) {
/**
* 这个数组代表每个循环中匹配到的样式
* 例如 green.bold,这个数组就会包含这两个样式
* 每次遇到闭合标签 } 时就会从中取出样式进行渲染
*/
const styles = [];
/**
* 渲染完成的字符串数组
* 里边的元素就是 {} 中的渲染字符串和不同 {} 直接的普通字符串
*/
const chunks = [];
/**
* 当前循环正在遍历的字符
* 当遇到起始标签 { 时(不用渲染颜色的内容)或遇到闭合标签 } 时(需要渲染颜色的内容)
* 就会取出、拼接、添加样式(如果有的话)然后存入 chunks
*/
let chunk = [];
// eslint-disable-next-line max-params
temporary.replace(TEMPLATE_REGEX, (m, escapeCharacter, inverse, style, close, character) => {
// 转义特殊字符
if (escapeCharacter) {
chunk.push(unescape(escapeCharacter));
}
// 遇到样式文本
else if (style) {
const string = chunk.join('');
chunk = [];
// 把之前的内容添加样式(如果有的话)
chunks.push(styles.length === 0 ? string : buildStyle(chalk, styles)(string));
// 把样式文本转换为对象
styles.push({inverse, styles: parseStyle(style)});
}
// 遇到了结束标签 }
else if (close) {
if (styles.length === 0) {
throw new Error('Found extraneous } in Chalk template literal');
}
// 把内容应用颜色
chunks.push(buildStyle(chalk, styles)(chunk.join('')));
chunk = [];
// 移除样式
styles.pop();
}
// 内容字符,直接推进待渲染数组
else {
chunk.push(character);
}
});
chunks.push(chunk.join(''));
if (styles.length > 0) {
const errorMessage = `Chalk template literal is missing ${styles.length} closing bracket${styles.length === 1 ? '' : 's'} (\`}\`)`;
throw new Error(errorMessage);
}
return chunks.join('');
}
总结
chalk 算是一个比较典型的封装包,做的工作就是从另一个包里拿到特殊的颜色字符,然后拼接到目标字符串前后。不过在易用性方面还是下了不少努力的。
其他方面,注释比较少,只在一些 bug 修复的位置添加了注释;不过测试用例还是比较全面的。框架风格比较混沌,可以学习细节,但是整体风格不宜参考。