前言
在日志系统或者工程化打包插件中我们时常会看到控制台输出五颜六色的字体以及符号,这些字体大多是使用ANSI转义序列来实现的,ANSI转义序列(或ANSI转义码)是一种用于控制文本输出格式的标准化方法,通常用于终端和控制台应用程序的样式或者输出,比如:粗体、斜体、颜色、背景,光标控制等,本篇文章将通过JS工具库的形式与大家分享常用的ANSI指令及进阶用法
基础知识
先来了解一下ANSI的基础用法,以node控制台为例,当我们使用console.log或print输出字符串时,在字符串中插入一些特殊字符,比如:
const logAndReset = (...args: string[]) => console.log(...args.map(it => it + `\x1b[0m`)); logAndReset(`普通字体`, `\x1b[1m加粗`, `\x1b[3m斜体`, `\x1b[36m青色`, `\x1b[42m绿色背景`);
效果如下:
上文中的`\x1b[变量m`就是通过ANSI转义码的形式输出对应指令,这些序列以Escape字符(ASCII码27,也就是Esc)开头,后面跟着一些特定的字符,用于控制终端的颜色、样式和其他属性。其中x1b代表16进制的17,此外,在node中我们还可以使用Unicode中的\u001b表示:`\u001b[1m`。m表示设置文本样式的参数,除了m外,还可以通过其他参数控制其他功能:A表示光标向上移动;2J表示清除整个屏幕等等。
进阶实践
有了上面的概念,我们可以尝试使用代码实现一个工具,将ANSI语义化,使用参数控制其样式和行为
ANSI参数
首先是收集基础的指令的文件static,这里面存放的是需要用到的ANSI参数
/** 0:重置所有样式 1:粗体(高亮) 2:暗淡(降低亮度) 3:斜体 4:下划线 5:闪烁 7:反显(交换前景色和背景色) 8:隐藏(不可见) 9:划掉(删除线) 21:关闭粗体(粗体关闭) 22:正常颜色和粗体(关闭粗体和暗淡) 23:关闭斜体 24:关闭下划线 25:关闭闪烁 27:关闭反显 28:关闭隐藏 29:关闭删除线 30 到 37:设置前景色(文本颜色) 38:设置前景色为RGB颜色 39:重置前景色为默认颜色 40 到 47:设置背景色 48:设置背景色为RGB颜色 49:重置背景色为默认颜色 90 到 97:设置高亮前景色 100 到 107:设置高亮背景色 **/ export const ANSIParams = { // 2:真彩色模式; 5:兼容模式 colorMode: { trueColor: "2", compatibility: "5", }, // 48:背景颜色,38:字体颜色 colorType: { foreground: "38", background: "48" }, color: { black: 30, red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37, bgBlack: 40, bgRed: 41, bgGreen: 42, bgYellow: 43, bgBlue: 44, bgMagenta: 45, bgCyan: 46, bgWhite: 47, brightBlack: 90, brightRed: 91, brightGreen: 92, brightYellow: 93, brightBlue: 94, brightMagenta: 95, brightCyan: 96, brightWhite: 97, bgBrightBlack: 100, bgBrightRed: 101, bgBrightGreen: 102, bgBrightYellow: 103, bgBrightBlue: 104, bgBrightMagenta: 105, bgBrightCyan: 106, bgBrightWhite: 107, }, textStyle: { reset: 0,// 重置所有样式 bold: 1,// 粗体 dim: 2,// 暗淡(降低亮度) italic: 3,// 斜体 underline: 4,// 下划线 inverse: 7,// 闪烁 hidden: 8,// 隐藏 strikethrough: 9,// 删除线 noBold: 21, // 关闭粗体 noItalic: 23, // 关闭斜体 noUnderline: 24, // 关闭下划线 noBlink: 25, // 关闭闪烁 noInverse: 27, // 关闭反显 noHidden: 28, // 关闭隐藏 noStrikethrough: 29, // 关闭删除线 }, cursor: { savePosition: `s`, // 保存光标位置 restorePosition: `u`, // 恢复光标位置 reportPosition: `6n`, // 获取光标位置 toStart: `H`, // 将光标移动到屏幕的左上角 toLineStart: `E`, // 将光标移动到当前行的开头 toLineEnd: `F`, // 将光标移动到当前行的末尾 eraseDisplay: `2J`, // 清除整个屏幕 eraseLine: `K`, // 清除从光标位置到行尾的内容 direct: { custom: `H`,// 光标移动到指定行列 up: `A`,// 光标向上移动 down: `B`,// 光标向下移动 right: `C`,// 光标向右移动 left: `D`,// 光标向左移动 } }, scroll: { up: "S", down: "T" } }
通过这些参数也可以直接使用console.log的形式打印出效果,可以看到,我将指令分成了六类:
colorMode:显示灰度色或RGB颜色,前者兼容性更好,后者颜色更丰富(具体用法在后文会讲到)
colorType:当使用了调色板时,设置前景色还是背景色
color:默认的颜色指令
textStyle:字体样式
cursor:光标控制相关的
scroll:滚动条相关
为了方便使用,我使用下面的ANSI类对上述指令做了规范化处理
ANSI类
围绕上面的基础指令,我们可以将对应的参数传入函数中达到效果
// ANSI 类,用于生成 ANSI 控制码 class ANSI { // moveTo 用于生成移动光标的 ANSI 控制码 moveTo = (opts: IMoveParams) => { const { direct, position } = opts ?? {} if (!direct || !position) return `` let _p = `` if (direct === "custom" && typeof position === "object") { const { col, row } = position _p = `${row};${col}` } else if (typeof position !== "object") { _p = `${position}` } if (_p) return `${_p}${ANSIParams.cursor.direct[direct] ?? ""}` return `` } // scrollTo 方法,用于生成滚动屏幕的 ANSI 控制码 scrollTo = (row: number) => { if (!row) return `` // 滚动条向下滚动,代码对应向上移动一行,相当于删除(backspace)row行 let direct = ANSIParams.scroll.down if (row < 0) { // 滚动条向上滚动,代码对应向下移动一行,相当于回车(enter)row行 direct = ANSIParams.scroll.up } return `${Math.abs(row)}${direct}` } // 生成光标相关的 ANSI 控制码 getCursor = (cursor: ICursor) => { if (cursor) return `${ANSIParams.cursor[cursor] ?? ""}` else return `` } // 生成颜色和样式相关的 ANSI 控制码 getColorMode = (mode: IColorMode) => `${ANSIParams.colorMode[mode] ?? "2"}` getColorType = (type: IColorType) => `${ANSIParams.colorType[type] ?? "38"}` getRGB = (options: Partial<IRGB>) => { const { type = "foreground", mode = "trueColor", red = 0, green = 0, blue = 0, color = 0 } = options const cType = this.getColorType(type), cMode = this.getColorMode(mode) if (cMode === ANSIParams.colorMode.compatibility) return `${cType};${cMode};${color}` return `${cType};${cMode};${red};${green};${blue}` } getColor = (color: IColorStr) => { if (color) return `${ANSIParams.color[color] ?? ""}` else return `` } getTextStyle = (textStyle: ITextStyle) => { if (textStyle) return `${ANSIParams.textStyle[textStyle] ?? ""}` else return `` } // 生成最终的 ANSI 控制码字符 getANSI = (params: string, isStyle: boolean = true) => params ? `\x1B[${params}${isStyle ? "m" : ""}` : "" // 重置所有样式 reset = () => this.getANSI(`0`) }
其中TS类型代码如下:
import { ANSIParams } from "./static.js" // 颜色字符串的索引 export type IColorStr = keyof typeof ANSIParams.color // 前(后)景色可以是 RGB 对象或颜色字符串 export type IColor = IRGB | IColorStr // 文本样式 export type ITextStyle = keyof typeof ANSIParams.textStyle // 光标的方向 export type IDirect = keyof typeof ANSIParams.cursor.direct // 光标操作的索引 export type ICursor = keyof Omit<typeof ANSIParams.cursor, "direct"> export type IGlobalOpts = Partial<{ text: string; // 要打印的文本 reset: boolean; // 是否在末尾重置样式 type: string; // console的类型 split: boolean; // 是否拆分显示,在node中可以在一个console.log中分开显示,比如:console.log("1","2","3")和console.log("123"),前者在字符之间会有空格,split为true表示使用前者显示,反之使用后者,在浏览器环境下只能合并显示 color: IColor[] | IColor; // 前(后)景色或其数组,方便传入多个颜色参数,但是有些控制台似乎不兼容 textStyle: ITextStyle[] | ITextStyle; // 文本样式或样式数组 cursor: ICursor; // 光标控制 move: IMoveParams; // 光标移动参数 scroll: number; // 滚动条行数 style: Partial<CSSStyleDeclaration>; // CSS样式对象,仅支持浏览器环境 }> export type IOpts = Omit<IGlobalOpts, "type" | "split"> // 颜色模式,2:真彩色模式; 5:兼容模式; null:默认颜色模式 export type IColorMode = keyof typeof ANSIParams.colorMode // 颜色类型,48:背景颜色,38:字体颜色 export type IColorType = keyof typeof ANSIParams.colorType export type IRGB = { red?: string | number green?: string | number blue?: string | number type?: IColorType // 字体颜色或背景颜色 mode?: IColorMode color?: string | number// 兼容模式下,256色调色板的颜色索引 } export type IPosition = { col: number | string // 列 row: number | string // 行 } export type IMoveParams = { direct: IDirect// 移动方向 position: IPosition | number | string// 移动到的位置,可以是坐标对象或数字 }
我们可以直接使用上面的类配合log输出样式到控制台
const ansi = new ANSI(); const { getANSI, getTextStyle, reset } = ansi; console.log(getANSI(getTextStyle("bold")), "加粗", reset());
实际上只使用ANSI操作控制台,有一个ANSI类就够了,但是我在工具中使用了JSLog对ANSI进行了拓展
JSLog类
实现JSLog类的目的有以下几点
- 日志格式化:根据传入参数的不同,使用不同的ANSI控制码和样式信息
- 环境适配:通过判断是在Node环境还是浏览器环境,做出相应的处理
- 全局选项配置:日志默认使用全局的配置,每个日志函数可以通过传入配置达到封装目的
- 可读性和维护性:相对于ANSI类,更易读,方便拓展并提升维护性
下面是JSLog的代码
const defaultOpts: IGlobalOpts = { reset: true, type: "log", split: true } // JSLog 类,继承 ANSI 类,用于生成日志,并根据环境打印到控制台或执行其他操作 export class JSLog extends ANSI { readonly stylePlaceholder = "%c"; // 浏览器环境下字符串占位符:%c isNode: boolean = typeof process !== "undefined"; private globalOpts: IGlobalOpts = defaultOpts // 全局选项作为参数 constructor(globalOpts?: IGlobalOpts) { super() this.checkOptions(globalOpts) this.mixins(globalOpts) } // 合并全局选项 mixins(opts: IGlobalOpts) { this.globalOpts = emptyObject({ ...this.globalOpts, ...opts }) return this.globalOpts } // 清空全局样式及行为 clear() { this.globalOpts = emptyObject(defaultOpts) } // 生成日志 log = (...args: IOpts[]) => { if (args.length <= 0) return this const { logQueue, styleQueue } = this.createQueue(args) this.logFn(logQueue, styleQueue) return this } // 生成日志队列 private createQueue = (args: IOpts[]) => { const styleQueue = [], logQueue: string[] = [] const { isNode, stylePlaceholder, globalOpts } = this args.forEach(it => { this.checkOptions(it) const { text = "" } = it const _opts = emptyObject({ ...globalOpts, ...it }) const { reset, style = {} } = _opts const hasStyle = !isEmptyObject(style) logQueue.push(`${this.formate(_opts)}${hasStyle ? stylePlaceholder : ""}${text}${reset ? this.reset() : ""}`) if (hasStyle) { const _style = this.formatStyle(style) _style && styleQueue.push(_style) } }) if (isNode && globalOpts.split) { return { logQueue, styleQueue } } return { logQueue: logQueue.join(""), styleQueue } } // 格式化选项生成 ANSI 控制码 private formate(opts: IOpts) { const { color, textStyle, cursor, move, scroll } = opts // TODO:控制渲染执行顺序 const params = this.getANSI(`${this.formatCursor(cursor)}${this.moveTo(move)}${this.scrollTo(scroll)}`, false) + this.getANSI(`${this.formatColor(color)}${this.formatText(textStyle)}`) return params } // formatParams、formatText、formatColor方法,格式化样式和颜色选项 private formatCursor(cursor: ICursor) { if (typeof cursor === "string") { return this.getCursor(cursor) } return `` } private formatText(textStyle: ITextStyle[] | ITextStyle) { if (typeof textStyle === "string") { return this.getTextStyle(textStyle) } else if (typeof textStyle === "object") { let __temp = ``; textStyle.forEach(it => __temp += this.getTextStyle(it)) return __temp } return `` } private formatColor(color: IColor[] | IColor) { if (typeof color === "string") { return this.getColor(color) } else if (typeof color === "object") { if (getType(color) === "array") { let __temp = ``; (color as IColor[]).forEach(it => __temp += this.formatColor(it)) return __temp } else { return this.getRGB(color as IRGB) } } return `` } // 格式化浏览器端的style样式属性 private formatStyle(styles: Partial<CSSStyleDeclaration>) { let styleStr = `` Reflect.ownKeys(styles).forEach(k => { if (typeof k === "string") styleStr += `${toKebabCase(k)}:${styles[k]};` }) return styleStr } // 执行打印操作 private logFn(logQueue: string[] | string, styleQueue: string[]) { const { globalOpts } = this const { type } = globalOpts if (typeof logQueue === "string") { return console[type](logQueue, ...styleQueue) } return console[type](...logQueue, ...styleQueue) } // 校验options参数 checkOptions(opts: IGlobalOpts = {}) { const { isNode } = this const { cursor, move, scroll, style, color, textStyle } = opts if (isNode && style) throw Error("node环境下无法使用style相关属性") if (!isNode && (cursor || move || scroll)) throw Error("window环境下无法使用光标相关属性") if (!isNode && style && (color || textStyle)) console.warn("请注意:样式可能会冲突") } }
工具实现完毕,来看看下面的功能介绍
工具的使用说明
配置相关
全局配置项
参照 types.ts 中的 IGlobalOpts 类型,全局配置项可以传入以下属性
type IGlobalOpts = Partial<{ text: string; // 要打印的文本 reset: boolean; // 是否在末尾重置样式 type: string; // console的类型 split: boolean; // 是否拆分显示,在node中可以在一个console.log中分开显示,比如:console.log("1","2","3")和console.log("123"),前者在字符之间会有空格,split为true表示使用前者显示,反之使用后者,在浏览器环境下只能合并显示 color: IColor[] | IColor; // 前(后)景色或其数组,方便传入多个颜色参数,但是有些控制台似乎不兼容 textStyle: ITextStyle[] | ITextStyle; // 文本样式或样式数组 cursor: ICursor; // 光标控制 move: IMoveParams; // 光标移动参数 scroll: number; // 滚动条行数 style: Partial<CSSStyleDeclaration>; // CSS样式对象,仅支持浏览器环境 }>;
默认配置
在index.ts中defaultOpts是默认配置,当开发者没有设置option时,会使用这里的样式和设置
基本用法
打印字符
const logger = new JSLog(); logger.log({ text: "阿宇的编程之旅" }); // 阿宇的编程之旅
添加全局配置项
全局配置的增加方式有两种,一是在实例化对象时传入构造函数,第二种是通过 mixins 添加
const logger = new JSLog({ type: "error", color: "red" }); logger.log({ text: "我是个错误提示" }); logger.mixins({ type: "info", color: "green" }); logger.log({ text: "我是个成功提示" });
清空所有样式及操作行为
const logger = new JSLog({ type: "error", color: "red" }); logger.log({ text: "阿宇" }); logger.clear(); logger.log({ text: "阿宇" });
校验传入的参数是否正确
const logger = new JSLog(); // node中 logger.checkOptions({ style: {} }); // node环境下无法使用style相关属性
样式控制
Node环境
在node环境下使用color控制文字的前(后)景色,使用textStyle控制文字形态样式,如加粗,斜体等(不同的控制台对样式兼容性不相同,可能会导致冲突或失效问题)
const logger = new JSLog(); logger.log({ text: "hello world", color: "cyan", // 字体青色 textStyle: "bold", // 加粗 });
浏览器中
浏览器环境下也可以使用color,textStyle控制文字样式,但是使用style属性可以支持更多的样式设置,具体可以参考CSSStyleDeclaration类型,如果style与上述的color,textStyle同时设置了,可能会导致样式冲突
logger.log({ text: "hello world", style: { color: "lightblue", background: "#333", margin: "10px" }, });
光标控制指令
光标控制只支持在Node环境下使用,和样式调整类似,其原理也是使用ANSI编码在控制台输出对应指令来完成的
const logger = new JSLog(); logger.log({ text: "i m here" }); logger.log({ text: "i m here" }); logger.log({ text: "i m here", cursor: "eraseDisplay" }); // 清除屏幕
光标位置偏移
光标移动与上述的光标指令相同,只不过在指令中传入了移动距离
const logger = new JSLog(); logger.log( { move: { direct: "right", position: 5, }, text: "编程之旅", // 先打印后面的字符 }, { move: { direct: "left", position: 14, }, text: "阿宇的", // 将前面的字符插入到左边 } );
滚动条控制
借助之前写的TimerManager定时器,或者直接使用setinterval实现一个向上滑动滚动条的效果,每500毫秒打印一次行数
import { TimerManager } from "utils-lib-js"; const logger = new JSLog(); const timerManager = new TimerManager(); let scroll = 0; const timer = timerManager.add(() => { if (scroll <= -5) return timerManager.delete(timer); scroll--; logger.log({ text: scroll.toString(), scroll }); }, 500);
其他用法
除了上述核心用法之外,JSLog还支持下面的进阶用法
log函数的链式调用
由于log函数返回了当前类,这就使开发者可以直接调用本身的其他函数进行操作
const logger = new JSLog(); logger.log({ text: "hello" }).log({ text: "world" });
同一行打印多种log
const logger = new JSLog({ split: false }); logger.log( { text: "阿宇", color: "red" }, { text: "的编程", color: "brightCyan" }, { text: "之旅", color: "bgBrightMagenta" } );
属性的指令集
上面我们提到了options可以传入一种指令控制样式或者行为,某些属性支持传入一个数组,批量设置属性
logger.log({ text: "hello world", color: ["red", "bgBrightGreen"], // 红色字体,亮绿背景 textStyle: ["bold", "italic", "strikethrough"], // 加粗,斜体,删除线 });
ANSI相关的操作
上面我们说到JSLog类继承于ANSI类,所以一些ANSI指令操作在JSLog中也是支持的,下面就举例说说常用的指令函数
ANSI编码指令拼接
通过getANSI和ANSI编码达到控制样式的效果,使用console或者直接使用JSLog直接输出getANSI的转移字符可以设置对应的指令
const logger = new JSLog(); const { getANSI } = logger; console.log(getANSI(`35`), "hello world"); // 输出紫色的字符串 logger.log({ text: getANSI(`34`) + "hello world", }); // 输出蓝色的字符串
颜色设置
使用下面的getColor函数可以设置对应的颜色样式
const logger = new JSLog(); const { getANSI, getColor } = logger; logger.log({ text: getANSI(getColor("bgBlue")) + "hello world", }); // 输出蓝色背景的字符串
字体样式
使用getTextStyle函数可以设置对应的字体样式
const logger = new JSLog(); const { getANSI, getTextStyle } = logger; logger.log({ text: getANSI(getTextStyle("bold")) + "hello world", }); // 输出加粗效果的字符串
光标操作
如果你想直接操作光标,不妨直接使用getCursor函数
const logger = new JSLog(); const { getANSI, getCursor } = logger; logger.log( { text: "----------------------------" }, { text: getANSI(getCursor("toLineStart"), false) + "hello world", } ); //移动到行首并输出字符
光标移动
使用moveTo函数可以直接移动光标
const logger = new JSLog(); const { getANSI, moveTo } = logger; logger.log( { text: "hello" }, { text: getANSI(moveTo({ direct: "right", position: 5 }), false) + "world", } ); //向右移动5次光标
滚动条移动
使用scrollTo函数可以移动滚动条
const logger = new JSLog(); const { getANSI, scrollTo } = logger; logger.log({ text: getANSI(scrollTo(-2), false) + "hello world", }); //向上移动2行滚动条
重置样式
通过运行reset函数可以重置样式,由于jslog默认配置了reset参数,所以样式只会在一个log队列中生效,要取消重置可以在实例化时传入reset: false,这里我直接使用console.log展示效果
const logger = new JSLog(); const { getANSI, getColor, reset } = logger; console.log( getANSI(getColor("bgBrightGreen")), "i m bgBrightGreen", reset(), "i m reset" );
ANSI颜色指令的进阶用法
上面说到了前后景色的设置,使用color属性可以对log的字符设置对应的颜色属性,然而由于颜色的指令数有限,有许多其他颜色无法表示,所以ANSI提供了两种设置颜色参数的方式
兼容模式
第一种是256种色调色板的形式,又叫做兼容模式,设置该模式需要将ANSI的第二个参数 mode 设置为5:
const ANSI = `\x1B[38;5;<color>m`;
它支持设置数字0-255:
0-15是标准颜色,也就是之前用到的颜色变量
16-231有216种颜色,这些颜色按照六阶的RGB立方体规则排列,提供了各种颜色的组合
232-255是灰度颜色,这些颜色表示不同灰度级别,从黑到白。232表示最暗的灰色,255表示最亮的灰色。
通过下面的代码我们可以打印出全部256种色阶:
const logger = new JSLog(); const { getANSI, getColor, reset } = logger; console.log( getANSI(getColor("bgBrightGreen")), "i m bgBrightGreen", reset(), "i m reset" );
真彩模式
另一种设置颜色的方式是使用TrueColor(真彩色)模式。在TrueColor模式下,可以通过指定RGB(红绿蓝)值来准确设置颜色,而不仅仅依赖于预定义的颜色索引,就像是css中的rgb方法。该模式需要将ANSI的第二个参数mode设置为2:
const ANSI = `\x1B[38;2;<r>;<g>;<b>m`;
它支持设置数字 256^3 种(16777216 种颜色),在真彩模式下可以通过指定每个颜色通道的具体强度值来准确定义颜色
const logger = new JSLog(); logger.log( { text: "hello", color: { red: 255 } }, { text: "阿宇的", color: { green: 255 }, }, { text: "编程之旅", color: { blue: 255, type: "background" } } );
下面是一个使用rgb实现的简易调色板,这里我将步长设置为26
const logger = new JSLog(); const { getANSI, getRGB, reset } = logger; let color = `` for (let r = 0; r <= 255; r += 26) { for (let g = 0; g <= 255; g += 26) { for (let b = 0; b <= 255; b += 26) { color += `${getANSI(getRGB({ red: r, green: g, blue: b, mode: "trueColor", type: "background" }))} ${reset()}` } } } console.log(color);
光标坐标轴移动
通过配置move参数我们还可以实现指针坐标位移的功能
下面的代码中我们实现了一个斜着输出hello world字符串的功能
import { TimerManager } from "utils-lib-js"; const logger = new JSLog(); const timer = new TimerManager(); // 创建定时器 const str = "hello world"; const position = { row: 0, // 行 col: 0, // 列 }; logger.log({ cursor: "eraseDisplay" }); // 清屏 timer.add(() => { const _str = str[position.col]; if (!_str) timer.clear(); position.col++; position.row++; move(_str); }, 100); const move = (str) => logger.log({ text: str, move: { direct: "custom", position, }, });
写在最后
本篇文章通过ANSI控制码入手,由浅及深的介绍了其用法,在控制台中我们可以使用ANSI来设置字符样式,光标移动等等,接着我实现了一个浏览器和服务端共用的JS工具库,帮助大家更好的操控log以及理解ANSI操作样式的原理。
以上就是文章的全部内容了,感谢你看到了最后,如果觉得文章不错的话,还望三连支持一下!谢谢~