将 React 渲染到 console.log
Text.js 每帧通过一次带样式的 console.log 调用,在 DevTools 控制台中绘制 React 应用。
Text.js 在 Chrome DevTools 控制台中渲染 React 应用。除了一个隐藏的输入框,页面没有其他 UI。应用的每一帧都是一次 console.log 调用。
用控制台作显示器
console.log 接受一个格式字符串。每个 %c 将下一个参数中的 CSS 应用于后续文本,直到下一个 %c:
console.log("%c▼ Text.js%c ready", "color:#ededed;font-weight:700", "color:#45a557");这些样式可以设置颜色、背景、粗体、斜体和下划线,控制台使用等宽字体(macOS 上为 Menlo)。借助等宽字体,以及可按连续字符段变化的颜色,控制台就成了由带样式的字符单元格组成的网格。边框、面板、输入框和图表都可以在上面绘制。
一帧就是一个单元格网格
渲染器将每一帧保存在 CellBuffer 中。字符存储在一个类型化数组中,前景色和背景色分别存储在另外两个数组中,粗体和下划线等属性则存储在第四个数组中。对于给定尺寸,这些数组只分配一次,之后每一帧都复用。
输出一帧时,驱动程序逐行遍历,将样式相同的相邻单元格合并为连续段。每个连续段转换为一个 %c span,各行用 \n 连接。我们的 100×32 测试仪表盘共有 3200 个单元格,编码后约为 200 个 span。
每帧一次调用
将每一行作为独立消息输出行不通。在 DevTools 中,每条消息都有自己的边框和源代码链接,而且 DevTools 会将连续的相同消息折叠为一条,并显示重复次数,因此表格中相同的两行会合并为一行。将一帧作为单条消息输出,可以保持帧的完整。
文本中有两处需要先转义。字面量 % 写成 %%,这样 100% 之类的文本就不会消耗样式参数。DevTools 还会将 URL 转成链接,并用自己的颜色重绘,因此要在 :// 和 www. 中插入一个没有宽度的 word joiner(U+2060):
export function escapeConsoleText(text: string, allowLink = false): string { let s = text; if (s.includes("%")) s = s.replaceAll("%", "%%"); if (!allowLink) { if (s.includes(LINK_SCHEME)) s = s.replaceAll(LINK_SCHEME, LINK_SCHEME_SAFE); if (s.includes(WWW) || s.includes("WWW.")) s = s.replace(/www\./gi, (m) => `${m.slice(0, 3)}\u2060.`); } return s;}空闲帧不输出日志
缓冲区的每一行都会计算哈希。如果自上一帧以来没有哈希变化,驱动程序会在构建字符串之前返回,不会操作控制台。等待输入的应用不会发起任何调用。
当某一行发生变化时,驱动程序会清空控制台并输出新帧:
const { frame, dirtyRows } = this.encode(buffer, options);if (frame === null) return SKIPPED;if (this.clearEachFrame) target.clear();target.log(frame.format, ...frame.args);只有哈希发生变化的行才会重新编码。其余行复用上一帧的 span。
输入
控制台不接收按键输入,因此由页面收集。一个隐藏的 <textarea> 接收按键、粘贴和输入法组合输入,并将其转换为应用的按键事件。点击页面会使这个输入框获得焦点。
后续
npm 上的 @textjs/core 0.0.x 原型将所有功能放在一个包中。Text.js 0.1 将本文介绍的渲染器移至 @textjs/core,将 React 协调器移至 @textjs/react,并添加了 App Router。