Web Workers
原生 Node 上的浏览器形态 Worker 全局变量——其构造函数、消息传递、可转移对象和事件接口。
浏览器形态的 Worker 全局变量由 Nub 自身的类型包定义——@types/node 只声明 node:worker_threads。将 @nubjs/types 作为 devDependency 与 @types/node 26 一起安装。
# 安装类型包
npm i -D @nubjs/types @types/node@25
# tsconfig.json
{ "compilerOptions": { "types": ["node", "@nubjs/types"] } }浏览器暴露 Worker 全局变量;Node 只提供 node:worker_threads.Worker,一个不同的类和不同的 API,从不是浏览器形态的全局变量。Nub 通过在每个支持的 Node(18.19+)上预加载的 polyfill 弥合差距,所以浏览器构造函数和消息形态无需修改即可工作。
const worker = new Worker(import.meta.resolve("./worker.ts"), { type: "module" });
worker.onmessage = (ev) => {
console.log(ev.data); // → 42
worker.terminate();
};
worker.postMessage({ n: 41 });// worker.ts
self.onmessage = (ev) => {
self.postMessage(ev.data.n + 1);
};Worker 入口免费获得 TypeScript——Nub 像任何其他文件一样转译它。一个监听中的 worker 保持进程存活,所以主线程在获得回复后调用 worker.terminate()(或 worker 可以自行 self.close())。
创建 worker
构造函数接受 WHATWG 脚本 URL 输入——文件路径或 URL(包括 file://)、data: URL 或 blob: URL。对于基于路径的 worker,默认使用 import.meta.resolve:
new Worker(import.meta.resolve("./worker.ts")); // 模块相对,无需构建,TypeScript 和全部import.meta.resolve 调用相对于当前模块解析说明符——从不 process.cwd()——并返回其 URL。这是应该使用的简洁写法。
使用打包器?Vite、webpack 和 esbuild 追踪 new URL(...) 形式——将 worker 拉入其自己的块并重写路径——但它们尚不追踪 import.meta.resolve。当管道中有打包器时切换形式:
new Worker(new URL("./worker.ts", import.meta.url)); // 打包器可追踪,运行时也正确裸相对字符串相对于 process.cwd() 解析,而非调用模块——与 node:worker_threads 和 Bun 一致。当启动目录固定时有效,但从其他位置运行嵌套文件时会中断:
new Worker("./worker.ts"); // cwd 相对——除非启动目录已知,否则避免使用内联源码跳过解析。data: URL 直接运行,来自 URL.createObjectURL 的 blob: URL 被同步快照并生成:
const src = "self.postMessage('ready')";
new Worker("data:text/javascript," + encodeURIComponent(src));
new Worker(URL.createObjectURL(new Blob([src], { type: "text/javascript" })));对于原始源码字符串,传递 { eval: true }——Node 的内联形式,在 Node worker_threads 兼容性下涵盖。
第二个参数是 WorkerOptions 对象:
| 选项 | 值 | 效果 |
|---|---|---|
type | "module"(默认)、"classic" | worker 作用域暴露哪种 importScripts 形式——classic 获得同步加载器,module 获得抛出形式 |
name | 字符串 | 在 worker 内部可作为 self.name 读取 |
eval | true | 将构造函数第一个参数作为 worker 源码运行,而非将其解析为 URL |
execArgv | 字符串数组 | worker 的 Node 标志;合并到承载 Nub 预加载的标志上 |
env | 对象 | worker 线程的环境 |
消息传递
两个线程通过 postMessage 和 onmessage(或 addEventListener("message"))交换消息。入站消息作为真实的 MessageEvent 到达,所以载荷在 ev.data 上:
worker.postMessage({ n: 41 });
worker.onmessage = (ev) => console.log(ev.data); // MessageEvent — 载荷在 .data 上
worker.addEventListener("message", (ev) => console.log(ev.data)); // 相同事件,EventTarget 形式worker 中抛出的错误在主线程上作为携带 message、error 和从 worker 堆栈读取的源位置的 ErrorEvent 出现:
worker.onerror = (ev) => console.error(ev.message, ev.filename, ev.lineno);载荷通过 Node 的结构化序列化器克隆。将转移列表作为第二个参数传递以移动 ArrayBuffer 或 MessagePort 的所有权而非复制——转移在发送方分离它:
const buf = new ArrayBuffer(8);
worker.postMessage(buf, [buf]); // buf.byteLength 在此侧现在为 0| 类别 | 成员 |
|---|---|
| 克隆 | 普通对象和数组、Map / Set / Date、类型化数组 |
| 转移 | ArrayBuffer、MessagePort、FileHandle — Node 的可转移集合 |
| 共享 | SharedArrayBuffer(按引用共享,不复制) |
| 不可用 | ImageBitmap、OffscreenCanvas、流转移 — Node 上无 DOM 基质 |
Worker 内部
Nub 在 node:worker_threads.parentPort 之上安装 WHATWG 专用 worker 作用域。全局 self(=== globalThis)承载消息和生命周期接口:
// worker.ts
self.name; // { name } 构造选项
self.onmessage = (ev) => self.postMessage(ev.data);
self.addEventListener("message", (ev) => {});
self.close(); // 从内部停止此 workerNode 的 worker 全局变量不是 EventTarget,不暴露这些,所以 polyfill 提供整个作用域。标准消息类型——MessageEvent、MessageChannel、MessagePort——是 Node 自身的全局变量,在 worker 内部不变可用。
模块 worker 是默认的。经典 worker({ type: "classic" })获得同步 importScripts() 加载器;模块 worker 获得抛出的 importScripts 并使用 import。type 选项只选择作用域暴露哪种加载器——Node 仍通过文件扩展名和最近的 package.json "type" 决定入口的模块 vs CommonJS 解析,与 Nub 应用于主入口的规则相同。
// worker.cjs — 经典 worker
importScripts("./setup.cjs"); // 同步获取并运行,按顺序
self.onmessage = (ev) => self.postMessage(ev.data);在经典 worker 中,importScripts() 同步按顺序执行本地文件和 data: URL。远程(http: / https:)URL 不支持——Node 上没有同步网络。
Node worker_threads 兼容性
上方的 web 形态是主要接口。同一个句柄也为 Node 风格代码镜像 node:worker_threads.Worker:其 EventEmitter 方法、online 和 exit 生命周期事件、返回 Promise 的 terminate(),以及用于内联源码字符串的 { eval: true }。
const worker = new Worker(
`const { parentPort } = require("node:worker_threads");
parentPort.on("message", (n) => parentPort.postMessage(n * 2));`,
{ eval: true },
);
worker.on("online", () => console.log("started"));
worker.on("error", (err) => console.error(err.stack)); // 裸 Error
worker.on("exit", (code) => console.log("exited", code));
worker.on("message", async (value) => {
console.log(value); // 42 — 裸值,不是 MessageEvent
const code = await worker.terminate(); // Promise<number>
});
worker.postMessage(21);在 node 通道上,message 监听器获得裸投递值,error 监听器获得裸 Error——Node 的形态,而非 web 通道保持的 MessageEvent / ErrorEvent。两个通道存在于一个句柄上;选择适合代码的那个。
TypeScript
主线程的 Worker 全局变量和 WorkerOptions 由 @nubjs/types 定义(仅类型的 devDependency)。当 tsconfig.json 中有 lib: ["dom"] 时声明退到一边——DOM 自身的 Worker 类型在那里优先——所以两者从不冲突。
在 worker 文件内部,全局作用域是专用 worker 作用域,而非主线程的——self、postMessage 和 onmessage 在那里,@types/node 和 @nubjs/types 都不声明它们。设置 lib: ["webworker"] 获得完整的 worker 作用域类型,或在 worker 顶部放一行 shim 用于常见的处理程序:
// worker.ts
declare var self: Worker;
self.onmessage = (ev: MessageEvent) => {
self.postMessage(ev.data + 1);
};这是 Bun 文档化的模式——它无需 tsconfig 更改即可类型化 self.postMessage 和 self.onmessage。(self.close() 和 importScripts() 是 worker 作用域独有的;如果你使用它们,请使用 lib: ["webworker"]。)
工作原理
polyfill 是预加载(runtime/worker-polyfill.mjs),不是标志注入——Node 没有暴露浏览器 Worker 的标志。它将 globalThis.Worker 定义为包装 node:worker_threads.Worker 的 EventTarget 子类,用 typeof 功能检测,所以仅在不存在原生全局变量时安装。为保护冷启动,它在首次 new Worker(...) 时懒安装。
规范一致性
Nub 的 Worker 在每个支持的 Node 层级上运行 web-platform-tests 的一个内嵌切片(tests/worker-wpt/)——webmessaging 套件、通过 MessageChannel 的结构化克隆,以及 worker 作用域事件测试——对照签入的期望文件绿色通过。消息传递和结构化克隆通过;文档记录的例外是 Node 没有基质的浏览器 DOM 类型,如 ImageBitmap 和 OffscreenCanvas。
差异
浏览器的 Worker 和 Nub 的共享相同形态。两个差异值得了解:
- 无
SharedWorker— 它需要浏览器文档和 origin 模型,没有服务器端等价物。使用带消息传递的单个 worker。 - Worker 是真实线程 — 每个是带自己 V8 隔离和模块图的 OS 线程,比浏览器 worker 重。为热路径池化它们而非每个任务生成一个。