一个终端颜色格式化的库| 8月更文挑战

274 阅读1分钟

这是我参与8月更文挑战的第15天,活动详情查看:8月更文挑战

前言

今天要讲的库是 kleur ,号称在颜色格式化终端文本中最快的 Node.js 库,利用 ANSI 码(一种字符码)对文本进行格式化。

先来康康 kleur 的使用方式:

const { bold, green } = require('kleur');
 
console.log(bold().red('this is a bold red message'));
console.log(bold().italic('this is a bold italicized message'));
console.log(bold().yellow().bgRed().italic('this is a bold yellow italicized message'));
console.log(green().bold().underline('this is a bold green underlined message'));

image.png

实现思路

链式调用

  • 创建可链式调用的 对象 $

$ 对象 包括多个 key 属性,其 value 值是一个初始化函数

const $ = {
    enabled: !NODE_DISABLE_COLORS && NO_COLOR == null && TERM !== 'dumb' && (
            FORCE_COLOR != null && FORCE_COLOR !== '0' || isTTY
    ), // 是否可用

    // modifiers
    bold: init(1, 22),
    italic: init(3, 23),
    underline: init(4, 24),

    // colors
    red: init(31, 39),
    green: init(32, 39),
    yellow: init(33, 39),

    // background colors
    bgBlack: init(40, 49),
    bgRed: init(41, 49),
};

export default $;
  • 初始化链式对象方法

  • 利用 ANSI 码 包裹文本即可使得打印出的文本带有某些样式

console.log(`\x1b[1m \x1b[31m hello \x1b[22m \x1b[39m`)

下面看看 init 方法主要做了什么工作。

function init(open, close) { 
    let blk = {              // 
        open: `\x1b[${open}m`, 
        close: `\x1b[${close}m`,
        rgx: new RegExp(`\x1b\[${close}m`, 'g')
    };
    return function (txt) {  // 返回一个函数【闭包】,可接收文本
        if (this !== void 0 && this.has !== void 0) { // this指向调用者
            this.has.includes(open) || (this.has.push(open), this.keys.push(blk));
                return txt === void 0 ? this : $.enabled ? run(this.keys, txt + '') : txt + '';
        }
        // 1. 没有txt,通过chain()函数链式调用
        // 2. 有txt,输出txt
        return txt === void 0 
            ? chain([open], [blk]) 
            : $.enabled ? run([blk], txt + '') : txt + ''; 
    };
}
  • init() 接收两个数字参数:ANSI 码的起止值

  • init() 返回可接收文本参数的函数

首先在最开始创建了 blk 这个在内层函数闭包可访问的变量。

然后返回的函数中,可接收文本参数,通过 chain 函数进行链式调用。

  • 链式调用函数的实现 chain
function chain(has, keys) {
    let ctx = { has, keys };

    ctx.bold = $.bold.bind(ctx);
    ctx.italic = $.italic.bind(ctx);
    ctx.underline = $.underline.bind(ctx);

    ctx.red = $.red.bind(ctx);
    ctx.green = $.green.bind(ctx);
    ctx.yellow = $.yellow.bind(ctx);

    ctx.bgBlack = $.bgBlack.bind(ctx);
    ctx.bgRed = $.bgRed.bind(ctx);

    return ctx;
}
  • 新建 ctx 对象,用于链式调用执行。

注意:

这里不能直接返回,因为, 因为 kleur 库提供的,需要保持纯净,不影响其他人使用; 将 $ 的相关方法通过 bind() 绑定到 ctx 对象,改变 this 指向为 ctx 不使用 for 循环,而是挨个 bind(),避免一些不必要的属性/方法绑定造成副作用

  • 执行函数的实现 run
function run(arr, str) {
	let i=0, tmp, beg='', end=''; // 先声明再使用
	for (; i < arr.length; i++) {
		tmp = arr[i];
		beg += tmp.open;
		end += tmp.close;
		if (str.includes(tmp.close)) {
			str = str.replace(tmp.rgx, tmp.close + tmp.open);
		}
	}
	return beg + str + end; 
}

这里接受 ANSI 的数组和目标文本 str,然后一个字符串可以有多个样式 ANSI 码,循环数组取到每一项进行拼接, 最终输出 ANSI + str + ANSI 带有样式的文本。(输出不限制一定是 console.log

总结

kleur 可借鉴的思路:

  • 功能隔离,工具函数保证代码更清晰
  • 链式调用,bind() 绑定新对象保证原输出对象的纯净

看到这里,相信聪明伶俐的小伙伴儿已经理解了 kleur 源码的思路,现在我们可以自己试着实现一个简单的 kleur :

const $ = {
    bold: init(1, 22),
    red: init(31, 39),
    green: init(32, 39),
    bgRed: init(41, 49),
    bgYellow: init(43, 49),
}

function chain(base) {
    const context = {base}

    context.bold = $.bold.bind(context)
    context.green = $.green.bind(context)
    context.red = $.red.bind(context)
    context.bgRed = $.bgRed.bind(context)
    context.bgYellow = $.bgYellow.bind(context)

    return context
}

function run(base, str) {
    let start = '', end = ''
    base.forEach(item => {
        start += item.start,
        end += item.end
    })

    return start + str + end
}

function init(start, end) {
    const base = {
        start: `\x1b[${start}m`,
        end: `\x1b[${end}m`
    }
    return function (txt) {
        return !txt ? chain(base) : run([base], txt + '')
    }
}

console.log($.bold().green().bgRed('gyn'))