TypeScript 配置

浏览器形态的 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.createObjectURLblob: 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 读取
evaltrue将构造函数第一个参数作为 worker 源码运行,而非将其解析为 URL
execArgv字符串数组worker 的 Node 标志;合并到承载 Nub 预加载的标志上
env对象worker 线程的环境

消息传递

两个线程通过 postMessageonmessage(或 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 中抛出的错误在主线程上作为携带 messageerror 和从 worker 堆栈读取的源位置的 ErrorEvent 出现:

worker.onerror = (ev) => console.error(ev.message, ev.filename, ev.lineno);

载荷通过 Node 的结构化序列化器克隆。将转移列表作为第二个参数传递以移动 ArrayBufferMessagePort 的所有权而非复制——转移在发送方分离它:

const buf = new ArrayBuffer(8);
worker.postMessage(buf, [buf]); // buf.byteLength 在此侧现在为 0
类别成员
克隆普通对象和数组、Map / Set / Date、类型化数组
转移ArrayBufferMessagePortFileHandle — Node 的可转移集合
共享SharedArrayBuffer(按引用共享,不复制)
不可用ImageBitmapOffscreenCanvas、流转移 — 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(); // 从内部停止此 worker

Node 的 worker 全局变量不是 EventTarget,不暴露这些,所以 polyfill 提供整个作用域。标准消息类型——MessageEventMessageChannelMessagePort——是 Node 自身的全局变量,在 worker 内部不变可用。

模块 worker 是默认的。经典 worker({ type: "classic" })获得同步 importScripts() 加载器;模块 worker 获得抛出的 importScripts 并使用 importtype 选项只选择作用域暴露哪种加载器——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 方法、onlineexit 生命周期事件、返回 Promiseterminate(),以及用于内联源码字符串的 { 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 作用域,而非主线程的——selfpostMessageonmessage 在那里,@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.postMessageself.onmessage。(self.close()importScripts() 是 worker 作用域独有的;如果你使用它们,请使用 lib: ["webworker"]。)

工作原理

polyfill 是预加载(runtime/worker-polyfill.mjs),不是标志注入——Node 没有暴露浏览器 Worker 的标志。它将 globalThis.Worker 定义为包装 node:worker_threads.WorkerEventTarget 子类,用 typeof 功能检测,所以仅在不存在原生全局变量时安装。为保护冷启动,它在首次 new Worker(...) 时懒安装。

规范一致性

Nub 的 Worker 在每个支持的 Node 层级上运行 web-platform-tests 的一个内嵌切片(tests/worker-wpt/)——webmessaging 套件、通过 MessageChannel 的结构化克隆,以及 worker 作用域事件测试——对照签入的期望文件绿色通过。消息传递和结构化克隆通过;文档记录的例外是 Node 没有基质的浏览器 DOM 类型,如 ImageBitmapOffscreenCanvas

差异

浏览器的 Worker 和 Nub 的共享相同形态。两个差异值得了解:

  • SharedWorker — 它需要浏览器文档和 origin 模型,没有服务器端等价物。使用带消息传递的单个 worker。
  • Worker 是真实线程 — 每个是带自己 V8 隔离和模块图的 OS 线程,比浏览器 worker 重。为热路径池化它们而非每个任务生成一个。