前言
前段时间用 Travis CI 发现它的部署日志包含了许多彩色日志。
而且我们知道这些可爱的颜色在使用命令行终端时也会出现。
当然,我不是不是为了吹它。它有实际效果,可以帮助我们快速定位问题!
我对此很好奇,Travis CI 如何将这些彩色日志转移到浏览器上?
我猜肯定不是通过识别关键词特征来做的,因为太 了low 了。
进行了查询后,查到了一个终于查到了关键词,它就是 ANSI escape sequences。
ANSI转义序列是控制终端和终端仿真器上光标位置、颜色等选项的标准。--维基百科
一般来说,在终端输出的彩色文本中包含一些转义序列字符,但我们看不到它们,并被终端分析。然后终端将这些字符分析成我们现在看到的彩色日志(包括一些颜色、下划线、粗体等)。
例如,我们在终端上npm 的安装,git 分支切换,包括操作报错时可见。
正是这些颜色大大提高了我们的调试效率,一眼就能看到哪些命令出了问题,如何解决。
我们现在要做的是如何将这些彩色日志输出到浏览器端。在这一步之前,我们必须知道这些ANSI转义序列的形式是什么?
根据wiki了解 ANSI 转义序列可以操作许多功能,如光标位置、颜色、下划线和其他选项。让我们解释一下 的颜色部分。
ANSI 转义序列
ANSI 转义序列 也随着终端的发展而发展,颜色规格也随着设备的不同而不同。例如,早期设备只支持 3 / 4 Bit ,支持的颜色是 8 / 16 。
ANSI 转义序列大多是 ESC 和‘开头嵌入文本,终端会找到并解释为命令,而不是字符串。
ESC 的 ANSI 为 27 ,8进制为 \033 ,16进制为 \u001B。
3/4 bit
原规格只有 8/16 色。
比如ESC[30;47m 它是以 ESC[ 开头 m 结束,中间是code码,按分号分割。
color 取值为30-37,background 取值为 40-47。例如 :
echo-e"\u001B[31mhello"(如果你想去除颜色,你需要使用 ESC [39;49m(有些终端不支持) 或ESC[0m )
后来终端增加了直接指定 90-97 和 100-107 的“明亮”颜色能力。
效果如下:
以下是其颜色对照表:
8-bit
后来,由于256种颜色在显卡上很常见,从预定义的256种颜色中添加了转义序列,即在原来的写作方法中添加了一个新的来代表更多的颜色。
ESC[38;5;<n>m//设置字体颜色ESC[48;5;<n>m//设置背景色0-7:standardcolors(asinESC[30–37m)8-15:highintensitycolors(asinESC[90–97m)16-231:6×6×6cube(216colors):16 36×r 6×g b(0≤r,g,b≤5)232-255:grayscalefromblacktowhitein24steps例如:
echo-e"\u001B[38;5;11mhello"代表输出黄色字体。
echo-e"\u001B[48;5;14;38;5;13mhello"代表蓝色背景和粉色字体的输出。
以下是其颜色对照表:
24-bit
未来的发展是支持 24 位真彩显卡,Xterm,KDE 的Konsole,以及所有的基础 libvte 终端(包括GNOME支持24位前景和背景色设置。
ESC[38;2;<r>;<g>;<b>m//前景色ESC[48;2;<r>;<g>;<b>m//背景色例如:
echo-e"\u001B[38;2;100;228;75mhello"输出绿色字体代表 rgb(100,228,75)。
解析工具
在我们知道了转义的规范之后,我们需要 ANSI 分析字符。
因为规范多,我们先调查一下 js 常用的色库,进行小探索。
因为 3 / 4bit 兼容性更好,大多数工具(如chalk)会采用这 8 / 16 色来做高亮,因此我们先实现一个 8 / 16 色的解析。
这里参考 ansiparse 这个分析库:
核心思想如下:
constansiparse=require('ansiparse')constansiStr="\u001B[34mHello\u001B[39mWorld\u001B[31m!\u001B[39m"constjson=ansiparse(ansiStr)console.log(json)//json输出如下:[{foreground:'blue',text:'Hello'},{text:'World'},{foreground:'red',text:'!'}]然后我们可以写一个函数个函数来分析 JSON数组,输出 HTML。
functioncreateHtml(ansiList,wrap=''){lethtml='';for(leti=0;i<ansiList.length;i ){consthtmlFrame=ansiList[i];const{background='',text,foreground=''}=htmlFrame;if(background&&foreground){if(text.includes('\n')){html =wrap;continue;}html =fontBgCode(text,foreground,background);continue;}if(background||foreground){constcolor=background?`bg-${background}`:foreground;lettextColor=bgCode(text,color);textColor=textColor.replace(/\n/g,wrap);html =textColor;continue;}if(text.includes('\n')){consttextColor=text.replace(/\n/g,wrap);html =textColor;continue;}html =singleCode(text);}html =''returnhtml;}functionfontBgCode(value,color,bgColor){return`<spanclass="${color}bg-${bgColor}">${value}</span>`}functionbgCode(value,color){return`<spanclass="${color}">${value}</span>`}functionsingleCode(value){return`<span>${value}</span>`}使用示例如下:
conststr="\u001B[34mHello\u001B[39mWorld\u001B[31m!\u001B[39m";console.log(createHtml(parseAnsi(str)));//<spanclass="blue">Hello</span><span>World</span><spanclass="red">!</span>部署实战
有了以上部分,我们将使用一个简单的部分demo实际演示部署日志吧!
///项目录结构demo|-package.json|-index.html|-webpack.config.js|-/src|-index.jsindex.jsbuild.sh我们在 index.js 中启动一个 build 脚本,模拟我们的真实部署场景。
const{spawn}=require('child_process');constcmd=spawn('sh',['build.sh']);cmd.stdout.on('data',(data)=>{console.log(`stdout:${data}`);});cmd.stderr.on('data',(data)=>{console.log(`stderr:${data}`);});cmd.on('close',(code)=>{console.log(`childprocessexitedwithcode${code}`);});//build.shcddemonpxwebpack我们试着在终端上输入控制台node index.js
在输出日志中没有看到相应的颜色。
为什么从 child_process 为什么我们不能输出颜色,如果我们直接在终端上打包项目,我们可以输出颜色?
Why?
第一反应是找到根源,即使用频率最高的颜色输出库。
以简单的方式标记控制台输出的颜色。
https://github.com/Marak/colors.js
https://github.com/chalk/chalk
在看了webpack-cli发现源码后,使用了它colorette作为色彩输出库。
让我们来看看colorette探索源码。
在入口文件的开头看到一个变量isColorSupported判断颜色输出是否支持。
https://github.com/jorgebucaran/colorette/blob/main/index.js#L17
//colorette/index.jsimport*asttyfrom"tty"constenv=process.env||{}constargv=process.argv||[]constisDisabled="NO_COLOR"inenv||argv.includes("--no-color")constisForced="FORCE_COLOR"inenv||argv.includes("--color")constisWindows=process.platform==="win32"constisCompatibleTerminal=tty&&tty.isatty&&tty.isatty(1)&&env.TERM&&env.TERM!=="dumb"constisCI="CI"inenv&&("GITHUB_ACTIONS"inenv||"GITLAB_CI"inenv||"CIRCLECI"inenv)exportconstisColorSupported=!isDisabled&&(isForced||isWindows||isCompatibleTerminal||isCI)可以看出,这种工具判断了很多条件来处理我们的输出流。
只有建立上述条件,才能输出 ANSI 日志。在不符合上述条件的情况下,输出更容易分析。
const isWindows = process.platform === "win32"
参考:https://stackoverflow.com/questions/8683895/how-do-i-determine-the-current-operating-system-with-node-js
dumb: "哑终端"
哑终端不能执行,如“删行”、“清屏”或“控制光标位置”的一些特殊ANSI计算机终端计算机终端
参考:https://zh.wikipedia.org/wiki/哑终端
也就是说,我们的 child_process 的输出流关闭了终端模式(TTY),以上四种情况都不满意。所以我们得不到 ANSI 色彩日志。
How?
我们可以显示进入环境的变量 FORCE_COLOR=1 或命令带上参数 --color 强制启动颜色来解决这个问题。
就这样,我们得到了带 的ANSI 颜色信息的输出文本,最终解析得到 HTML。
<div>asset<spanclass="green">main.js</span><span>132bytes</span><spanclass="yellow">[comparedforemit]</span><span></span><spanclass="green">[minimized]</span>(name:main)</div><div><span>./src/index.js</span><span>289bytes</span><spanclass="yellow">[built]</span><span></span><spanclass="yellow">[codegenerated]</span></div><div></div><div><spanclass="yellow">WARNING</span><span>in</span>configuration</div><div>The<spanclass="red">'mode'optionhasnotbeenset</span>,webpackwillfallbackto'production'forthisvalue.</div><div><spanclass="green">Set'mode'optionto'development'or'production'</span>toenabledefaultsforeachenvironment.</div><div>Youcanalsosetitto'none'todisableanydefaultbehavior.Learnmore:https://webpack.js.org/configuration/mode/</div><div></div><div>webpack5.53.0compiledwith<spanclass="yellow">1warning</span>in201ms</div><div></div>然后我们可以在浏览器中显示与终端输出一致的彩色输出日志。
参考
https://www.twilio.com/blog/guide-node-js-logging
https://github.com/jorgebucaran/colorette/blob/main/index.js#L17
https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
https://stackoverflow.com/questions/15011478/ansi-questions-x1b25h-and-x1be
https://bluesock.org/~willg/dev/ansi.html
https://www.cnblogs.com/gamesky/archive/2012/07/28/2613264.html
https://github.com/mmalecki/ansiparse