最近在研究脚手架,不可避免的用到了chalk,发现其可以进行链式调用,来组合各种样式:
import chalk from "chalk";
console.log(chalk.red("hello world"));
console.log(chalk.underline("hello world"));
console.log(chalk.red.underline("hello world"));
console.log(chalk.underline.red.bgBlueBright("hello world"));
各种样式属性(red/underline/bgBlueBright等)可以任意组合,顺序也没有要求,可以来回交换。
学习了一下源码,下面来简单实现一个。
首先,需要思考以下几个点:
1. 能作为函数直接执行,那么第一要义肯定是function;
2. 能当作对象那样,访问其的某个“属性“,那么要么是直接在函数上绑定了属性,要么是借助了原型链,在原型链上绑定属性;
3. 可以任意组合,顺序也无所谓,那么第二点的结果肯定是借助了原型链,所有的函数都是由一个函数生成器创建出的,创建出的函数的原型都指向同一个对象,这样创建出的各个函数就可以访问同一个对象。
根据上面的思路,大概能写出下面这样:
function createBuilder(_styler) {
const builder = () => "渲染效果" + _styler;
Object.setPrototypeOf(builder, chalk);
return builder;
}
var chalk = Object.create(null);
chalk["red"] = createBuilder("red");
chalk["underline"] = createBuilder("underline");
console.log(chalk.red()); // 渲染效果red
console.log(chalk.underline()); // 渲染效果underline
console.log(chalk.red.underline()); // 渲染效果underline
console.log(chalk.underline.red()); // 渲染效果red
链式调用倒是实现了,但很明显,单独调用和链式调用,并无任何差别。与单独调用相比,链式调用中,后面调用的肯定得对之前调用过的进行相应的记录,例如记录调用过哪些,或记录经过之前的调用后产生的结果(像装饰器那样,对经过上一个装饰器的结果进行装饰)。
所以,red/underline等并不能像上面那样直接创建,而应该动态创建,在创建的过程中对之前的结果进行记录:
const styles = Object.create(null);
styles["red"] = {
get() {
// this的指向在调用时才确定,期望其在链式调用时,指向上一个调用的结果
const builder = createBuilder("red", this["STYLER"]);
return builder;
},
};
styles["underline"] = {
get() {
// this的指向在调用时才确定,期望其在链式调用时,指向上一个调用的结果
const builder = createBuilder("underline", this["STYLER"]);
return builder;
},
};
const proto = Object.defineProperties({}, styles);
function createBuilder(_styler, parent) {
const tip = parent ? parent + " " + _styler : _styler;
const builder = () => "渲染效果 " + tip;
builder["STYLER"] = tip; // builder即为调用createBuilder时的传递的this
Object.setPrototypeOf(builder, proto); // builder的原型是proto,实现了"我中有你,你中有我"
return builder;
}
const chalk = proto;
console.log(chalk.red()); // 渲染效果 red
console.log(chalk.underline()); // 渲染效果 underline
console.log(chalk.red.underline()); // 渲染效果 red underline
console.log(chalk.underline.red()); // 渲染效果 underline red
基本完成了。作为优化,没必要每次调用都重新执行一遍get函数,只在第一次执行一次,并将执行结果绑定在调用主体上,之后都的调用都是在调用主体上直接调用,如下:
const styles = Object.create(null);
styles["red"] = {
get() {
console.log("创建 red");
const builder = createBuilder("red", this["STYLER"]);
Object.defineProperty(this, "red", { value: builder }); // 这里,绑定至调用主体
return builder;
},
};
styles["underline"] = {
get() {
console.log("创建 underline");
const builder = createBuilder("underline", this["STYLER"]);
Object.defineProperty(this, "underline", { value: builder }); // 这里,绑定至调用主体
return builder;
},
};
const proto = Object.defineProperties({}, styles);
function createBuilder(_styler, parent) {
const tip = parent ? parent + " " + _styler : _styler;
const builder = () => "渲染效果 " + tip;
builder["STYLER"] = tip;
Object.setPrototypeOf(builder, proto);
return builder;
}
const chalk = {};
Object.setPrototypeOf(chalk, proto); // proto作为chalk的原型,而不是直接赋值给chalk
console.log(chalk.red()); // 创建 red + 渲染效果 red
console.log(chalk.red()); // 渲染效果 red
console.log(chalk.red.underline()); // 创建 underline + 渲染效果 red underline
console.log(chalk.red.underline()); // 渲染效果 red underline
也就是说,访问过一次chalk.red后,chalk上就有了red属性;访问过一次chalk.red.underline后,chalk.red上就有了underline属性。
涉及到具体实现(ANSI转义字符序列,更多信息请参考这里),带样式的文本只需要用两个特殊的字符串包裹文本即可。增加一个createStyler函数,其结果作为调用createBuilder的参数,如下:
const styles = Object.create(null);
styles["red"] = {
get() {
const builder = createBuilder(
createStyler("\x1B[31m", "\x1B[39m", this["STYLER"])
);
Object.defineProperty(this, "red", { value: builder });
return builder;
},
};
styles["underline"] = {
get() {
const builder = createBuilder(
createStyler("\x1B[4m", "\x1B[24m", this["STYLER"])
);
Object.defineProperty(this, "underline", { value: builder });
return builder;
},
};
const proto = Object.defineProperties({}, styles);
const createStyler = (open, close, parent) => {
const openAll = parent ? parent.openAll + open : open;
const closeAll = parent ? close + parent.closeAll : close;
return {
open,
close,
openAll,
closeAll,
parent,
};
};
function createBuilder(_styler) {
const builder = (...arguments_) =>
_styler.openAll + arguments_.join(" ") + _styler.closeAll;
builder["STYLER"] = _styler;
Object.setPrototypeOf(builder, proto);
return builder;
}
const chalk = {};
Object.setPrototypeOf(chalk, proto);
console.log(chalk.red("hello world"));
console.log(chalk.underline("hello world"));
console.log(chalk.red.underline("hello world"));
console.log(chalk.underline.red("hello world"));
至此,一个简单的函数间链式调用及实际应用就实现了。 当然,chalk实际的源码要更复杂,chalk是通过一个工厂函数创建的,各函数间不仅可以链式调用,还可以嵌套调用,createStyler也做了相关的容错处理,更多细节请大家自行查看。
共勉。