- 前端
- UI组件
【免费下载链接】xterm.js
A terminal for the web
@xterm/headless是 xterm.js 官方提供的无头(headless)终端组件,可以在 Node.js 环境中运行完整的终端仿真状态机,而无需浏览器 DOM。本文以仓库中的 headless/README.md 为骨架,结合 xterm-headless.d.ts 类型声明、src/headless 源码实现与 headless 测试用例,系统讲解其安装导入、完整 API、终端选项、事件与缓冲区模型,并给出与前端@xterm/xterm配合的典型服务端部署思路。读完本文,你将掌握如何在远程服务器上用一个纯 Node.js 终端实例跟踪真实进程的终端状态,并理解如何为它编写自定义渲染器与解析器扩展。
⚠ 重要提示:该包当前标记为experimental(实验性),这意味着其 API 可能在不同版本间发生较大变化,生产使用前请务必阅读对应版本的发布说明。
一、什么是 @xterm/headless:设计动机与使用场景
@xterm/headless是一个可运行在 Node.js 中的终端模拟器核心。它的核心设计是:在没有浏览器、没有 DOM、没有渲染层的前提下,完整保留 xterm.js 的 VT/ANSI 解析、缓冲区(buffer)、滚动、光标、模式切换等全部仿真逻辑。
从仓库源码结构看,这一思路非常清晰:
- src/headless/Terminal.ts 中的
Terminal类直接继承自CoreTerminal(见 src/common/CoreTerminal.ts),复用了完整的缓冲区、解析器与输入处理管线; - src/headless/public/Terminal.ts 是对外暴露的公开 API 包装层,负责把公共选项、事件和 addon 管理与核心实例衔接起来;
- 与浏览器端
Terminal不同,这里没有open()、attachCustomKeyEventHandler等 DOM 相关 API——你可以把它理解为"只仿真、不渲染"。
典型应用场景
headless 模式最常见的用途,是与前端@xterm/xterm配合:把终端状态跟踪放在托管进程的远程服务器上,浏览器端只负责渲染。例如:
浏览器 (前端 @xterm/xterm) <--WebSocket/Socket.io--> Node.js 服务端 (@xterm/headless) <--> PTY 子进程 渲染画面 维护终端状态、解析数据流 shell / ssh / 应用进程在这种架构中,@xterm/headless承担"状态真源(source of truth)"的角色:即使客户端断线重连,服务端仍保有完整的缓冲区与终端模式状态,重连后可以无缝恢复画面。由于它不依赖任何 DOM API,也能直接跑在纯 Node.js 进程中(不依赖 jsdom 等 polyfill)。
二、快速开始:安装与导入
@xterm/headless仅通过 npm 分发,因此需要先安装 npm,再把它添加为项目依赖:
npm install @xterm/headless安装后,推荐使用 TypeScript 配合 ES6 模块语法导入(这是官方推荐方式):
import { Terminal } from '@xterm/headless';从 headless/package.json 可以看出,该包同时支持 ESM 与 CommonJS 两种加载方式:
{ "name": "@xterm/headless", "version": "6.0.0", "main": "lib-headless/xterm-headless.js", "module": "lib-headless/xterm-headless.mjs", "types": "typings/xterm-headless.d.ts", "exports": { "types": "./typings/xterm-headless.d.ts", "import": "./lib-headless/xterm-headless.mjs", "require": "./lib-headless/xterm-headless.js" } }也就是说:
- 使用
import(ESM)时加载xterm-headless.mjs; - 使用
require(CJS)时加载xterm-headless.js; types字段指向类型声明文件 typings/xterm-headless.d.ts,这正是"完整 API 即类型声明"的来源。
如果你的项目使用 CommonJS,也可以这样引入:
const { Terminal } = require('@xterm/headless');创建实例的默认行为与浏览器端一致——测试用例 src/headless/public/Terminal.test.ts 明确验证了默认终端尺寸为80 列 × 24 行:
const term = new Terminal(); console.log(term.cols); // 80 console.log(term.rows); // 24三、终端选项(ITerminalOptions)详解
@xterm/headless的公开 API 全部定义在类型声明文件 typings/xterm-headless.d.ts 中。由于 headless 不做渲染,部分与绘制相关的选项(如fontSize、fontFamily、letterSpacing)在无头环境中主要用于状态记录,可随options对象读取/写入,但不会产生任何像素输出。
3.1 构造期专属选项(ITerminalInitOnlyOptions)
以下选项只能在构造函数中设置,之后通过term.options修改会直接抛错。实现依据在 src/headless/public/Terminal.ts 的CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'],以及 L51-L58 的_checkReadonlyOptions校验逻辑:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
cols | number | 80 | 终端列数,如new Terminal({ cols: 120 }) |
rows | number | 24 | 终端行数 |
showCursorImmediately | boolean | false | 创建时是否立即显示光标;false时首次聚焦前光标不可见(无头场景下该语义保留在状态中) |
3.2 常用核心选项
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
allowProposedApi | boolean | false | 是否允许使用 experimental/proposed API;为false时使用会抛错。测试用例 src/headless/public/Terminal.test.ts 验证了访问term.unicode时会抛出'You must set the allowProposedApi option to true to use proposed API' |
allowTransparency | boolean | false | 是否支持非不透明背景色(headless 下主要影响状态模型) |
convertEol | boolean | false | 为true时每个\n被当作\r\n处理。通常 PTY 的 termios 已处理该转换,此选项适合非 PTY 数据源 |
cursorBlink/blinkIntervalDuration | boolean / number | false/ 0 | 光标闪烁及闪烁间隔(毫秒) |
cursorStyle/cursorWidth | 'block' \| 'underline' \| 'bar'/ number | 'block'/ 1 | 光标样式;cursorWidth仅在bar样式下生效(CSS 像素) |
disableStdin | boolean | false | 是否禁用输入 |
drawBoldTextInBrightColors | boolean | true | 粗体文本是否用亮色绘制 |
letterSpacing/lineHeight | number | — / 1 | 字符间距(像素)/ 行高 |
logLevel | 'trace' \| 'debug' \| 'info' \| 'warn' \| 'error' \| 'off' | 'info' | 日志级别,按层级包含:trace < debug < info < warn < error < off |
logger | ILogger \| null | null | 自定义 logger 替代console,接口包含trace/debug/info/warn/error五个方法 |
macOptionIsMeta | boolean | false | 是否将 Option 键当作 Meta 键 |
macOptionClickForcesSelection | boolean | false | 按住修饰键时强制普通选择行为(鼠标模式下) |
minimumContrastRatio | number | 1 | 最小对比度,如4.5(WCAG AA)、7(WCAG AAA)、21(黑白) |
mouseEventsRequireAlt | boolean | false | 鼠标事件仅在按住 Alt 时发送给应用 |
reflowCursorLine | boolean | false | 调整大小时是否重排光标所在行 |
rescaleOverlappingGlyphs | boolean | false | 是否水平缩放单格宽但字形重叠的字符(如罗马数字 U+2160+,与 GB18030 合规相关);Emoji、Powerline、Nerd Font 字形不缩放;DOM 渲染器不支持 |
rightClickSelectsWord | boolean | false | 右键是否选中单词 |
screenReaderMode | boolean | false | 是否启用无障碍支持(headless 下状态保留) |
scrollback | number | 1000 | 滚动缓冲区行数,超出视口的行保留于此 |
scrollOnEraseInDisplay | boolean | false | ED2 清屏时是否将擦除内容推入 scrollback(模拟 PuTTY 默认清屏行为) |
scrollSensitivity | number | — | 滚动速度倍率 |
smoothScrollDuration | number | — | 平滑滚动时长(毫秒),0 表示即时滚动 |
tabStopWidth | number | — | 制表位宽度 |
theme | ITheme | — | 颜色主题(见下) |
windowsPty | IWindowsPty | — | Windows ConPTY 兼容信息:backend: 'conpty' \| 'winpty'、buildNumber(如 19045)。启用后根据数值启用相应启发式/兼容处理,例如增行时把增量并入 scrollback、特定版本禁用 reflow |
wordSeparator | string | — | 双击选词时视为分隔符的字符集合 |
windowOptions | IWindowOptions | — | 窗口操作/报告特性开关,出于安全默认全部禁用(见第五节) |
vtExtensions | IVtExtensions | — | 非标准 VT 扩展,如kittyKeyboard、kittySgrBoldFaintControl、win32InputMode、colorSchemeQuery(默认均为true/false的布尔开关,详见 typings/xterm-headless.d.ts) |
3.3 主题(ITheme)
theme选项用于定义终端配色,包含前景/背景/光标色、选择区背景,以及 16 种 ANSI 色和扩展色(typings/xterm-headless.d.ts):
term.options.theme = { foreground: '#ffffff', background: '#1e1e1e', cursor: '#ffffff', cursorAccent: '#1e1e1e', selection: 'rgba(255, 255, 255, 0.3)', black: '#000000', red: '#cd0000', // ... green/yellow/blue/magenta/cyan/white 及 bright* 系列 extendedAnsi: ['#000000', '#cd0000', /* 16-255 色 */] };注意:官方类型注释特别提醒,对于对象类型选项(如theme),必须传入新对象才能生效(因为内部做引用比较),即term.options.theme = { ...newValue }而非直接赋值同一个引用。
3.4 options 的读写语义
在 src/headless/public/Terminal.ts 中,term.options通过Object.defineProperty为每个选项生成 getter/setter,读取实时返回核心实例的值,写入则同步到核心实例;仅cols/rows被强制只读。批量设置也是允许的:
term.options = { scrollback: 5000, convertEol: true }; term.options.scrollback; // 5000四、核心 API:方法、事件与缓冲区
Terminal类的完整公开成员(构造、事件、方法)均声明于 typings/xterm-headless.d.ts,并由 src/headless/public/Terminal.ts 实现。
4.1 数据写入与输入
| 方法 | 签名 | 说明 |
|---|---|---|
write | write(data: string \| Uint8Array, callback?: () => void): void | 向终端写入数据。字符串按 UTF-16 处理,Uint8Array一律按 UTF-8 解码。写入是异步解析的,要确认缓冲区已反映本次写入,必须依赖callback |
writeln | writeln(data, callback?) | 写入数据后追加\r\n(内部实现为两次write,见 src/headless/public/Terminal.ts) |
input | input(data: string, wasUserInput?: boolean) | 模拟用户输入,会触发onData事件;wasUserInput默认true(触发聚焦/选择清除等附加行为),传false可避免这些副作用,例如向应用透传转义序列 |
写入的字节与回调顺序在测试 src/headless/public/Terminal.test.ts 中有直接验证,包括中文字符("文",UTF-8 三字节230 150 135)与回调执行顺序'abc'。
典型 PTY 接线示例(服务端将 PTY 输出喂给 headless,将用户输入回写 PTY):
import { Terminal } from '@xterm/headless'; const term = new Terminal({ cols: 80, rows: 24 }); // PTY -> 终端:原始字节流 pty.onData((data: Uint8Array) => { term.write(data); }); // 终端(用户输入)-> PTY:把键盘输入转发给子进程 term.onData((data: string) => { pty.write(data); }); // 同步回执:写完后回调(适合做节流/确认) term.write('\x1b[31mred\x1b[0m', () => { console.log('parsed'); });4.2 事件系统
headless 版本保留了完整的仿真事件,且onRender语义与浏览器版不同(见下方注释):
| 事件 | 值类型 | 触发时机 |
|---|---|---|
onBell | void | 收到 BEL 响铃 |
onBinary | string | 收到非 UTF-8 二进制数据(如某些鼠标报告),应转为Buffer.from(data, 'binary')传给 PTY |
onCursorMove | void | 光标移动 |
onData | string | 用户输入/粘贴产生数据,典型场景转发给底层 PTY |
onLineFeed | void | 发生换行 |
onRender | { start: number, end: number } | 请求渲染某行区间(0 到rows-1)。注意:headless 并不真正渲染,而是向外部发出渲染请求——这正为自定义渲染器提供了接入点(见第六节) |
onResize | { cols, rows } | 终端尺寸变化 |
onScroll | number | 视口滚动,值为新的视口位置 |
onTitleChange | string | OSC 0 / OSC 2 标题变化 |
onWriteParsed | void | write的数据解析完成(每帧最多一次;数据量大时可能仍有 pending 写入) |
每个事件监听都会返回一个IDisposable,调用dispose()即取消监听。
4.3 缓冲区访问
headless 的价值在于服务端能直接读取缓冲区内容,这是"状态真源"的关键:
// 获取活动缓冲区(normal 或 alternate) const buf = term.buffer.active; // 读取某一行 const line = buf.getLine(0); if (line) { const text = line.translateToString(true); // 去右侧空白 const cell = line.getCell(5); // 第 5 个单元格 console.log(cell?.getChars(), cell?.getWidth()); } // 行级元信息 console.log(line.isWrapped); // 是否因自动换行而续接上一行相关接口定义见 typings/xterm-headless.d.ts:IBufferNamespace(normal/alternate/active/onBufferChange)、IBufferLine(isWrapped、getCell、translateToString)与IBufferCell(getChars、getWidth、getFgColor、getBgColor、isBold、isUnderline等全部 SGR 属性判断)。
4.4 尺寸、滚动与标记
| 方法 | 说明 |
|---|---|
resize(columns, rows) | 调整尺寸,触发onResize;官方建议做debounce 防抖,避免 PTY 响应不及时。实现见 src/headless/Terminal.ts,尺寸未变时直接返回 |
scrollLines(n)/scrollPages(n)/scrollToTop()/scrollToBottom()/scrollToLine(line) | 程序化滚动视口(headless 下滚动状态可读,供自定义渲染器使用) |
registerMarker(cursorYOffset?) | 在缓冲区注册标记,返回IMarker(id/line,dispose 后line为 -1),常用于跟踪指定输出位置 |
clear() | 清空整个缓冲区,使当前提示行成为首行;会同步清空所有 markers(测试 src/headless/public/Terminal.test.ts 验证了 markers 全部被 dispose) |
reset() | 执行完整重置(RIS,'\x1bc');保留当前rows/cols |
五、解析器扩展与窗口选项(parser / windowOptions)
5.1 自定义转义序列处理器
通过term.parser可以为 CSI、DCS、ESC、OSC、APC 注册自定义处理器,这在 headless 场景特别有用——例如服务端自定义协议扩展。接口见 typings/xterm-headless.d.ts:
// 注册 CSI 处理器,例如拦截所有 SGR(final: 'm') const disposable = term.parser.registerCsiHandler( { final: 'm' }, (params) => { console.log('SGR params:', params); return false; // false 表示继续尝试其他处理器,true 表示已消费 } ); // 之后可随时卸载 disposable.dispose();处理器标识IFunctionIdentifier包含prefix(\x3c..\x3f,仅 CSI/DCS)、intermediates(\x20..\x2f)与final三部分;DCS/OSC/APC 的载荷上限为 10 MB。规则上建议使用 ECMA-48 的私有地址空间、最多一个中间字节,并在其他常见终端模拟器上验证兼容性。
5.2 窗口操作选项(IWindowOptions)与安全模型
windowOptions控制CSI Ps t系列的窗口操作/报告特性(typings/xterm-headless.d.ts)。绝大多数选项没有默认实现,因为窗口操作高度依赖宿主环境;且出于安全考虑(防止向终端内程序泄露宿主机信息),所有选项默认关闭。
对于没有默认实现的项,官方给出了通过 CSI hook 自行实现的范式(如报告窗口状态Ps=11):
term.parser.addCsiHandler({ final: 't' }, (params) => { const ps = params[0]; switch (ps) { case 11: // 你的实现:向终端应用回复 "CSI 1 t" term.input('\x1b[1t'); return true; // 标记该 Ps 已被处理 default: return false; // 未处理的 Ps 继续向下传递 } });另有一批选项自带默认实现:refreshWin(Ps=7)、getWinSizePixels(Ps=14)、getCellSizePixels(Ps=16)、getWinSizeChars(Ps=18)、pushTitle(Ps=22)、popTitle(Ps=23)。
六、基于 onRender 构建自定义渲染器
这是 headless 与浏览器版最大的差异点,也是其扩展性的核心:onRender事件并非真正绘制,而是请求渲染指定行区间。也就是说,你可以在 Node.js 端或任意自定义宿主中实现自己的"渲染"逻辑:
term.onRender(({ start, end }) => { // 收到行区间 [start, end] 的渲染请求 // 例如:从缓冲区读出这几行,推送为字符串给前端 for (let y = start; y <= end; y++) { const text = term.buffer.active.getLine(y)?.translateToString(true) ?? ''; ws.send(JSON.stringify({ type: 'render', y, text })); } });事件值范围是0到term.rows - 1,配合 4.3 的缓冲区访问,足以实现一个轻量的"文本流式渲染器"。
七、Addons:机制相同,但必须无 DOM
@xterm/headless的 addon 机制与@xterm/xterm完全一致:addon 实现ITerminalAddon(activate(terminal)+dispose()),通过term.loadAddon(addon)加载(见 src/headless/public/Terminal.ts 与 AddonManager)。测试 src/headless/public/Terminal.test.ts 验证了 addon 在加载时立即拿到终端实例、以及 addon 与终端的双向 dispose 联动。
唯一的 caveat 是:addon 必须被打包为适用于 Node.js 的版本,并且不得使用任何 DOM API。例如仓库 addons 目录下的官方 addons 大多依赖浏览器渲染/DOM 能力(如 webgl、web-links、fit 等),并不直接适用于 headless;README 亦明确说明目前 npm 上尚未打包任何官方 headless addon。这意味着在服务端扩展功能时,优先考虑直接使用parser注册处理器或onRender等原生 API。
八、实验性 API 与版本策略
正如 headless/README.md 强调的,@xterm/headless整体处于 experimental 状态,使用前应关注:
- 标记为experimental的 API(例如
Terminal.unicode接口,见 src/headless/public/Terminal.ts 的 proposed API 校验)是为快速试验新想法而加入的,不承诺像普通 semver API 一样长期稳定; - 这类 API 可能在不同版本间发生激进变更,若计划使用,务必阅读对应版本的 release notes;
- 需要
allowProposedApi: true才能使用 experimental 成员,否则会抛错(测试已验证此行为)。
九、端到端实践:远程终端的完整服务端骨架
综合以上全部能力,一个典型的"远程进程 + headless 状态跟踪"服务端骨架如下:
import { Terminal } from '@xterm/headless'; import { spawn } from 'node:child_process'; const term = new Terminal({ cols: 80, rows: 24, scrollback: 2000, allowProposedApi: true }); // 1. 启动真实 shell 进程 const pty = spawn('bash', [], { env: process.env }); // 2. PTY 输出 -> headless 终端(原始字节,按 UTF-8 解析) pty.stdout.on('data', (chunk: Buffer) => { term.write(new Uint8Array(chunk)); }); // 3. 用户输入 -> PTY term.onData((data) => pty.stdin.write(data)); // 4. 终端尺寸变化 -> 通知 PTY 调整 term.onResize(({ cols, rows }) => pty.stdout.write?.(`\x1b[8;${rows};${cols}t`)); // 5. 标题同步给客户端 term.onTitleChange((title) => ws.send(JSON.stringify({ type: 'title', title }))); // 6. 自定义渲染器:把渲染请求变成文本推送 term.onRender(({ start, end }) => { const lines: string[] = []; for (let y = start; y <= end; y++) { lines.push(term.buffer.active.getLine(y)?.translateToString(true) ?? ''); } ws.send(JSON.stringify({ type: 'render', start, end, lines })); }); // 7. 清理 process.on('SIGINT', () => { term.dispose(); pty.kill(); });配合前端@xterm/xterm实例把onData/write经 WebSocket 双向转发,即可实现"浏览器渲染 + 服务端状态"的完整远程终端。需要特别留意:PTY 数据应使用term.write的Uint8Array形式或 UTF-8 字符串写入,回调参数用于确认解析完成;多个连续write间需处理好节流,避免解析队列堆积。
参考链接(仓库内)
- 包说明文档:headless/README.md
- 包元数据与导出配置:headless/package.json
- 完整公开 API 类型声明:typings/xterm-headless.d.ts
- 核心实现(继承
CoreTerminal):src/headless/Terminal.ts - 公开 API 包装层:src/headless/public/Terminal.ts
- API 行为验证测试:src/headless/public/Terminal.test.ts
- 其余 addon 源码:addons
- 前端
- UI组件
【免费下载链接】xterm.js
A terminal for the web
相关推荐
BleachHack性能优化指南:如何配置模组以获得最佳游戏性能
BleachHack性能优化指南:如何配置模组以获得最佳游戏性能 BleachHack是一款功能强大的Minecraft模组,能为玩家提供多种实用功能。然而,若
Opal与Node.js:如何在服务器端运行Ruby代码的完整指南
Opal与Node.js:如何在服务器端运行Ruby代码的完整指南 Opal是一个强大的Ruby到JavaScript的源到源编译器,它让开发者能够在Node.
编译器编程语言开发工具JavaScript状态机终极指南:Node.js后端开发的完整实践方案
JavaScript状态机终极指南:Node.js后端开发的完整实践方案 JavaScript状态机(javascript state machine)是一个强
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考