☰
wterm如何将终端渲染到DOM:支持原生文本选择的渲染管线与脏行增量更新
2026/9/28 18:41:40 网站建设 项目流程

wterm如何将终端渲染到DOM:支持原生文本选择的渲染管线与脏行增量更新

【免费下载链接】wtermA terminal emulator for the web项目地址: https://gitcode.com/gh_mirrors/wterm1/wterm

wterm 是一款运行在浏览器里的 Web 终端模拟器(terminal emulator for the web),本文将拆解它如何把终端渲染到 DOM:用真实 DOM 文本而不是画布来实现原生文本选择,并靠脏行(dirty row)增量更新让高频输出下的渲染依然流畅。

为什么选 DOM 而不是 canvas?

很多网页终端用<canvas>画字,快,但代价是:选不中、拷不了、读屏软件听不见。wterm 走了另一条路——把每一行终端输出渲染成真实的 HTML 文本节点(<div>行 + 若干<span>字串),由 renderer.ts 负责完成。

这样做带来三个直接好处:

  • 🖱️原生文本选择:鼠标拖选、Cmd/Ctrl+A、右键复制全部走浏览器内置能力,无需自己写选区算法;
  • ♿无障碍友好:屏幕阅读器可以直接读到已挂载的行文本;
  • 🎨样式即 CSS:前景/背景色、粗体、下划线、删除线都编译成行内 CSS 样式,主题只需换 CSS 变量。

渲染结果的行级结构很简单:一行 = 一个div,行内连续样式相同的单元格会被合并成一个<span>字串(run),样式变化处才切分新 span。

渲染管线:写入 → 调度 → 帧内合并

终端输出是高频的(一次ls可能触发几十次写入)。wterm 不在每次write()时立即重绘,而是采用帧内合并策略,核心逻辑在 wterm.ts 的_scheduleRender()中:

  1. 写入阶段:write(data)把字节喂给 WASM 编译的核心(Zig 源码见 terminal.zig),核心解析转义序列、更新字符网格;
  2. 调度阶段:每次写入只调用一次_scheduleRender(),如果已有帧任务在排队就直接跳过;
  3. 绘制阶段:由requestAnimationFrame在下一帧统一渲染——同一帧内的多次写入被合并成一次重绘。

此外还支持终端的同步输出协议(CSI ?2026):处于同步块时画面被"扣住",直到块关闭才原子性绘制,避免用户看到半屏状态;配合renderingPaused选项还能整体暂停非活动窗格的重绘。

脏行增量更新:只重画"变了的行"

如果每帧把 500 行全部重建 DOM,性能会瞬间崩塌。wterm 的答案是脏行标记:

  • 核心侧打标:Zig 核心为网格维护一条每行的脏标记位(grid.dirty[row]),任何修改某行内容的操作(写字符、滚动、清屏、改宽……)都会把该行置 1,见 terminal.zig;
  • 桥接层读取:JS 通过 WASM 内存直接读这个标记数组,isDirtyRow(row)就是一个 O(1) 的数组索引,见 wasm-bridge.ts;
  • 渲染器只碰脏行:每帧渲染时逐行判断resized || core.isDirtyRow(r),只有脏行(以及光标进出影响的行)才进入重建流程,干净行完全跳过,见 renderer.ts。

还有一层更细的保险:行重建前会先把新 HTML 与上次缓存做字符串比较(rowHtml是一个WeakMap),内容没变连innerHTML都不赋值,见 renderer.ts。这避免了无谓的 HTML 解析与 DOM 写操作,也让滚动时复用行元素的成本降到最低。

行元素复用与回滚区虚拟化

光有脏行还不够,wterm 在 DOM 层面做了两件事:

  • 行元素池复用:可见区每一行对应一个常驻div(rowEls数组),更新只改内容不换节点;回滚区(scrollback)滚动时,与新进入窗口的行重叠复用旧节点,而不是销毁重建;
  • 有界挂载窗口:DOM 里只挂载"可见行 + overscan 余量 + 最多 1000 行被选中的历史行",其余历史保持虚拟化,需要时才按需挂载(详见 README.md 的 "Scrollback rendering" 一节)。

这套组合让一个几万行的构建日志在页面上滚动依旧顺滑。

原生选择的终端语义

DOM 文本虽然可以原生选择,但终端有自己的"行/列/宽字符"语义。wterm 通过 selection.ts 在原生选区之上补了一层终端语义:

  • 双击选词、三击选逻辑行(含软换行拼接);
  • 拷贝时保留完整 emoji、CJK、制表符等完整字形,宽字符右半边不产生多余字符;
  • getSelectionText()返回带终端行/列语义的纯文本;selectAll()可一键选中全部保留历史 + 当前屏幕。

由于选区建立在真实文本节点上,浏览器原生的复制、查找、检查元素全部天然可用。

快速上手

git clone https://gitcode.com/gh_mirrors/wterm1/wterm
<script type="module"> import { WTerm } from "@wterm/dom"; const term = new WTerm(document.getElementById("terminal")); await term.init(); term.write("hello from wterm\n"); </script>

更多用法与完整 API 可参考 web/index.html 演示页和 @wterm/dom README。

小结

机制解决的问题关键代码
DOM 文本行渲染原生选择 / 复制 / 读屏renderer.ts
rAF 帧内合并高频写入抖动wterm.ts
脏行标记避免整屏重绘terminal.zig + wasm-bridge.ts
HTML 字符串比对缓存避免无效 DOM 写renderer.ts
行元素复用 + 虚拟化挂载长日志滚动性能renderer.ts

一句话总结:wterm 用"真 DOM 文本"换来了原生的选择与无障碍,用"脏行标记 + 帧合并 + 节点复用"换来了画布级别的性能。

【免费下载链接】wtermA terminal emulator for the web项目地址: https://gitcode.com/gh_mirrors/wterm1/wterm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询