webpack 实战:用 Web Worker 与 SharedWorker 实现后台计算与多页面共享——examples/worker 完整源码解析
本篇文章以 examples/worker 示例(其正文由 examples/worker/template.md 模板渲染而成,完整渲染产物见 examples/worker/README.md)为核心,系统讲解 webpack 如何通过 new Worker(new URL(...))、new SharedWorker(...) 等原生语法把 Worker 脚本当作一等公民进行模块打包、代码分割与独立运行时分发。读完本文,你将掌握:worker 入口的写法与命名控制、worker 内部再 import() 的动态加载、主线程与 worker 之间共享公共 chunk 的机制,以及 webpack 在 lib/dependencies/WorkerAndWorkletPlugin.js 等源码中的底层实现原理,并能够迁移到自己的项目中。
一、示例概览:一个页面里同时演示三种 Worker 场景
examples/worker 目录把"斐波那契计算 + 聊天室"做成了一张单页,用来对照演示 webpack 对 worker 的三种处理路径:
| 场景 | 关键文件 | 用到的 API | 技术看点 |
|---|---|---|---|
| 后台斐波那契计算(不阻塞 UI) | example.js、fib-worker.js、fibonacci.js | new Worker(new URL(...), { name, type }) |
worker 作为独立 entry bundle,内部可继续动态 import() |
| 多页面/多连接共享聊天记录 | chat-worker.js、chat-module.js | new SharedWorker(new URL(...)) + port.onmessage |
SharedWorker 单实例多端口通信 |
| 主线程内直接计算作对照 | fibonacci.js | import() 动态导入 |
与 worker 内 import() 形成对照,验证 chunk 共享 |
运行入口是 index.html,它只加载编译产物 ./dist/main.js;编译配置为 webpack.config.js。由于页面逻辑中用到了 SharedWorker、import.meta.url 与 ESM 语法,示例必须在支持这些浏览器能力的环境中以 HTTP 方式访问(不能用 file:// 直接打开),随后在输入框中输入斐波那契参数,或在文本框中发送聊天消息即可观察效果。
二、构建配置与产物全貌
2.1 webpack.config.js
本示例的 webpack.config.js 非常简洁,且没有显式声明 worker 相关插件——这正是 webpack 5 的设计:worker 支持已内建,只需写出标准浏览器 API 即可:
"use strict";
const path = require("path");
/** @type {import("webpack").Configuration} */
const config = {
entry: "./example.js",
output: {
path: path.join(__dirname, "dist"),
filename: "[name].js",
chunkFilename: "[name].js",
publicPath: "/dist/"
},
optimization: {
concatenateModules: true,
usedExports: true,
providedExports: true,
chunkIds: "deterministic" // To keep filename consistent between different modes (for example building only)
}
};
module.exports = config;
配置要点:
- 只有主入口
entry: "./example.js":chat-worker.js、fib-worker.js都不是手动声明的入口,而是 webpack 在解析到new Worker(...)/new SharedWorker(...)时自动生成的"worker 入口 chunk"; output.filename/chunkFilename使用[name].js,chunkIds: "deterministic"保证开发与生产两种模式下 chunk 编号一致(对应编译输出的129.js);publicPath: "/dist/"与 index.html 中的src="./dist/main.js"呼应,同时决定运行时拼接 worker 与异步 chunk 的 URL 前缀。
2.2 编译产物清单
按 examples/worker/README.md 中 "Info" 一节的真实编译输出,产物共四个文件:
| 产物 | 开发模式体积 | 生产模式体积 | 类型 | 对应内容 |
|---|---|---|---|---|
dist/main.js |
11.6 KiB | 3.34 KiB | 页面主 entry | example.js + 完整 webpack runtime |
dist/workers/fibonacci.js |
4.74 KiB | 800 B | fibonacci worker entry |
fib-worker.js + 精简 runtime |
dist/chat.js |
839 B | 270 B | chat SharedWorker entry |
chat-worker.js + chat-module.js(被拼接合并) |
dist/129.js |
729 B | 156 B | 异步共享 chunk | fibonacci.js,被 main 与 worker 两侧共同引用 |
从产物结构可以看到 webpack 的三个关键决策:
- worker 拥有独立运行时:
fibonacci与chat在编译统计中各自以chunk (...runtime: <hash>)形式出现,说明 webpack 会为每个 worker 分配独立 runtime,使其能脱离主 bundle 独立启动; - worker 内部可继续代码分割:
129.js这一异步 chunk 同时被./example.js的第 70 行import()和./fib-worker.js第 2 行的import()引用,即主线程与 worker 共享同一份公共模块 chunk; - SharedWorker 的依赖模块被就地合并:
chat-module.js与chat-worker.js被拼接成一个 527 B 的 entry(编译统计显示./chat-worker.js + 1 modules),因为该 worker 不再需要运行时加载别的 chunk。
三、三种 Worker 用法的完整源码
3.1 在"计算密集型任务"上使用 Worker
页面在 example.js 中同时提供了两种计算斐波那契的方式。其一是主线程直接算,它会在 UI 线程上阻塞:
/// FIBONACCI without worker ///
fib1.addEventListener("change", async () => {
try {
const value = parseInt(fib1.value, 10);
const { fibonacci } = await import("./fibonacci");
const result = fibonacci(value);
output1.innerText = `fib(${value}) = ${result}`;
} catch (e) {
output1.innerText = e.message;
}
});
其二是放到 Worker 里算,UI 完全不被阻塞,交互后通过 postMessage 拿结果:
/// FIBONACCI with worker ///
const fibWorker = new Worker(new URL("./fib-worker.js", import.meta.url), {
name: "fibonacci",
type: "module"
/* webpackEntryOptions: { filename: "workers/[name].js" } */
});
fib2.addEventListener("change", () => {
try {
const value = parseInt(fib2.value, 10);
fibWorker.postMessage(`${value}`);
} catch (e) {
output2.innerText = e.message;
}
});
fibWorker.onmessage = event => {
output2.innerText = event.data;
};
关键语法是 new Worker(new URL("./fib-worker.js", import.meta.url), options)。其中:
new URL("./relative-path", import.meta.url)是 webpack 官方推荐、可被静态分析的 worker 引用写法——webpack 的 parser 会把这个 URL 解析成模块依赖(详见下文第四节),而不是当作运行时字符串;- 第二个参数
options.name会被用作 worker chunk 名;示例中name: "fibonacci"决定了编译统计里的(name: fibonacci); options.type: "module"表示按 ES Module 语义编写 worker 源码。需要注意:当输出并非真正的 ESM(本配置未开启output.module)时,webpack 会在产物里把它改写成type: undefined,将 worker 以"经典脚本 + 内置 webpack runtime"的形式发出(这一点从产物代码可验证,第 3.4 节详述);- 对象内的魔法注释
/* webpackEntryOptions: { filename: "workers/[name].js" } */用于注入 entry 级选项:本示例通过它把该 worker 的产物路径定制为workers/[name].js,于是出现dist/workers/fibonacci.js。
worker 侧源码 fib-worker.js 用 onmessage 接收消息,并在 worker 内部通过动态 import() 拉取 fibonacci:
onmessage = async event => {
const { fibonacci } = await import("./fibonacci");
const value = JSON.parse(event.data);
postMessage(`fib(${value}) = ${fibonacci(value)}`);
};
而 fibonacci.js 是一个普普通通的 ESM 模块:
export function fibonacci(n) {
return n < 1 ? 0 : n <= 2 ? 1 : fibonacci(n - 1) + fibonacci(n - 2);
}
fibonacci.js 之所以能同时被主线程的 import() 与 worker 内部的 import() 使用,正是因为 webpack 把它提取成了同一个异步 chunk 129.js,再由两套运行时各自加载(主线程用 JSONP <script>,worker 用 importScripts,见第四节)。
3.2 用 SharedWorker 实现多页面共享聊天记录
example.js 里通过 SharedWorker 建了一个名为 chat 的连接,多个页面/标签页共享同一个 worker 实例与其内存状态:
const chatWorker = new SharedWorker(
new URL("./chat-worker.js", import.meta.url),
{
name: "chat",
type: "module"
}
);
// ... UI 组装:history / message / send / output 等 DOM 元素 ...
const scheduleUpdateHistory = () => {
clearTimeout(historyTimeout);
historyTimeout = setTimeout(() => {
chatWorker.port.postMessage({ type: "history" });
}, 1000);
};
scheduleUpdateHistory();
const from = `User ${Math.floor(Math.random() * 10000)}`;
send.addEventListener("click", e => {
chatWorker.port.postMessage({ type: "message", content: message.value, from });
message.value = "";
message.focus();
e.preventDefault();
});
chatWorker.port.onmessage = event => {
const msg = event.data;
switch (msg.type) {
case "history":
history.innerText = msg.history.join("\n");
scheduleUpdateHistory();
break;
}
};
SharedWorker 与普通 Worker 最大的差异是它通过 port 通信:主线程需在 onconnect 建立连接后用 chatWorker.port.postMessage 收发消息,而不是直接 postMessage。
worker 侧 chat-worker.js 把聊天历史集中在内存数组里,并导出给所有连接共享:
import { history, add } from "./chat-module";
onconnect = function (e) {
for (const port of e.ports) {
port.onmessage = event => {
const msg = event.data;
switch (msg.type) {
case "message":
add(msg.content, msg.from);
// fallthrough
case "history":
port.postMessage({ type: "history", history });
break;
}
};
}
};
共享状态 chat-module.js 维护了一个上限 10 条的环形覆盖列表:
export const history = [];
export const add = (content, from) => {
if (history.length > 10) history.shift();
history.push(`${from}: ${content}`);
};
从 README.md 中 chat.js 的产物可以看到,webpack 将 chat-module.js 与 chat-worker.js 就地拼接为一个文件,且模块代码被作用域隔离改写(history 变成局部 const chat_module_history 等),SharedWorker 无需任何 chunk 加载 runtime——因为它内部没有任何动态 import()。
3.3 生产模式的压缩对比
把同样的源码以 production 模式构建后,README.md 展示了压缩后的形态。例如 chat.js 被压成一行 270 B:
(()=>{"use strict";const s=[];onconnect=function(t){for(const o of t.ports)o.onmessage=t=>{const e=t.data;switch(e.type){case"message":n=e.content,c=e.from,s.length>10&&s.shift(),s.push(`${c}: ${n}`);case"history":o.postMessage({type:"history",history:s})}var n,c}}})();
fib-worker.js(dist/workers/fibonacci.js)压缩为 800 B,其中内嵌了最短化的 webpack runtime 以及 importScripts 的 chunk 加载逻辑。同时对比可见 chunkIds: "deterministic" 保证了两次构建使用相同的 chunk 编号(129),便于缓存与调试。
四、产物剖析:worker URL 如何被解析与替换
webpack 并不会把 new URL("./fib-worker.js", import.meta.url) 原样留在产物里,而是在编译期就确定好 worker chunk,并把 URL 替换为 运行时函数表达式。从 README.md 中 main.js 的产物可以看出两处替换。
首先,主 bundle 的 runtime 里生成了"取 chunk 文件名"的函数 __webpack_require__.u,其中记录了 worker chunk 的映射:
/* webpack/runtime/get javascript chunk filename */
// This function allow to reference async chunks
__webpack_require__.u = (chunkId) => {
// return url for filenames not based on template
if (chunkId === 721) return "workers/fibonacci.js";
// return url for filenames based on template
return (chunkId === 377 ? "chat" : chunkId) + ".js";
};
其次,Worker 构造调用被改写为基于 publicPath + chunk 文件名 + baseURI 的形式:
const chatWorker = new SharedWorker(
new URL(/* worker import */ __webpack_require__.p + __webpack_require__.u(377), __webpack_require__.b),
{
name: "chat",
type: undefined
}
);
const fibWorker = new Worker(new URL(/* worker import */ __webpack_require__.p + __webpack_require__.u(721), __webpack_require__.b), {
name: "fibonacci",
type: undefined
/* webpackEntryOptions: { filename: "workers/[name].js" } */
});
同样,worker 文件内部的 import() 被替换成 __webpack_require__.e(129).then(...) 并真正发起了 chunk 加载:
// 位于 dist/workers/fibonacci.js 中
onmessage = async event => {
const { fibonacci } = await __webpack_require__.e(/*! import() */ 129).then(() => (__webpack_require__(/*! ./fibonacci */ 3)));
const value = JSON.parse(event.data);
postMessage(`fib(${value}) = ${fibonacci(value)}`);
};
上述替换逻辑的落点就是 lib/dependencies/WorkerDependency.js 中的 WorkerDependency.Template#apply:它根据该 worker 依赖所在 chunk 计算 URL(__webpack_require__.p + __webpack_require__.u(chunkId)),再决定是否需要包一层 new URL(...)。同时这个文件在产物中保留 /* worker import */ 注释,标明这是 worker 引用点。
可以看到 type: "module" 在产物中被改写为 undefined。这与解析插件中"非 module 输出则回退为经典脚本"的设计一致(见下一节),worker 文件于是以含完整 webpack runtime 的经典脚本形式发出。
五、worker chunk 的运行时加载:importScripts 路线
dist/workers/fibonacci.js 的产物中还包含了"worker 自己的 chunk 加载 runtime"。由于 worker 里存在 import()(对 fibonacci.js 的异步引用),webpack 需要为它注入一套不依赖 DOM、可在 worker 全局环境运行的加载器。产物代码以 importScripts 为核心(可对照 lib/webworker/ImportScriptsChunkLoadingRuntimeModule.js 的实现):
// 位于 dist/workers/fibonacci.js 的 runtime 中(摘录)
// no baseURI
var installedChunks = { 721: 1 };
// importScripts chunk loading
var installChunk = (data) => {
let [chunkIds, moreModules, runtime] = data;
// ... 把 moreModules 合并进模块表 ...
while(chunkIds.length)
installedChunks[chunkIds.pop()] = 1;
parentChunkLoadingFunction(data);
};
__webpack_require__.f.i = (chunkId, promises) => {
// "1" is the signal for "already loaded"
if(!installedChunks[chunkId]) {
if(true) { // all chunks have JS
importScripts(__webpack_require__.p + __webpack_require__.u(chunkId));
}
}
};
var chunkLoadingGlobal = self["webpackChunk"] = self["webpackChunk"] || [];
而共享的异步 chunk 129.js 被设计成自注册式文件,使其无论被主线程以 <script>(JSONP)加载、还是被 worker 以 importScripts 加载都能正确安装模块:
"use strict";
(self["webpackChunk"] = self["webpackChunk"] || []).push([[129],{
/***/ 3
/*!**********************!*\
!*** ./fibonacci.js ***!
\**********************/
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
fibonacci: () => (/* binding */ fibonacci)
});
function fibonacci(n) {
return n < 1 ? 0 : n <= 2 ? 1 : fibonacci(n - 1) + fibonacci(n - 2);
}
}]);
这解释了编译统计里 chunk (runtime: ..., main) 129.js 与 worker runtime 同时指向 129.js 的现象:主线程 runtime 和 fib-worker 的 runtime 共用同一份代码模块,但各自拥有独立的加载通道。
六、源码侧原理:WorkerAndWorkletPlugin 的解析管线
new Worker(...) 之所以能被"读出来"并自动构建 chunk,是因为 lib/dependencies/WorkerAndWorkletPlugin.js 在 parser 层注册了钩子。它的整体工作流程如下:
- 语法匹配:把
new Worker、new SharedWorker、navigator.serviceWorker.register()等(源码中的WORKER_DEFAULT_SYNTAX)注册进 parser。默认识别Worker/SharedWorker的new表达式;parser.worker配置项可自定义/关闭匹配语法集; - URL 解析:
resolveWorkerUrl分析第一参数,确认它是new URL("./xxx.js", import.meta.url)形式,从而得到模块相对路径与代码区间;同时也支持把"纯import.meta.url"等形态识别为 worker 引用点; - 选项提取:
parseObjectExpression从第二参数对象字面量中静态提取name等可编译期求值的字段;parseEntryOptions读取魔术注释webpackEntryOptions/webpackChunkName(这也是示例中{ filename: "workers/[name].js" }生效的入口),并防御性地过滤__proto__等危险键; - 生成 worker entry:构造
AsyncDependenciesBlock,其entryOptions带worker: true;同时ensureRuntime会为每个 worker 生成一段基于模块文件名的唯一 runtime 摘要,保证每个 worker 拥有自己独立的 runtime chunk; - 改写调用点:
handleNewWorker还会处理第二参数里的type字段——当输出不是 ESM 模块时,把type: "module"用ConstDependency改写为undefined,从而让产物中的 worker 以经典脚本方式被浏览器加载(对应第四节观察到的产物形态); - 生成依赖:
WorkerDependency(type 为"new Worker()",category 为"worker")被放入该 block,经 NormalModuleFactory 解析出真正的 worker 模块,最后交由WorkerDependency.Template在代码生成阶段完成 URL 替换。
该插件同时管理 WorkletDependency(audioWorklet、CSS.paintWorklet 等 addModule 语法,WORKLET_DEFAULT_SYNTAX),因此 Worker 与 Worklet 使用同一套解析基础设施,行为统一。
七、worker 打包的实践建议与限制
综合示例与实现,落地到自己项目中时建议注意以下几点:
- 始终使用
new URL(...)+import.meta.url的静态形式引用 worker 文件,而不是字符串拼接路径——这是 webpack 能识别依赖、参与 tree-shaking 与内容哈希的前提; - 善用
name选项与魔术注释控制产物:name: "chat"决定 chunk 语义名,webpackEntryOptions可注入filename等 entry 级选项,从而把 worker 产物组织到独立子目录(本示例的dist/workers/); - worker 内也可以做代码分割:
fib-worker.js中的import("./fibonacci")展示 worker 与主线程可共享异步 chunk。不过请确认目标环境的 worker 支持动态加载(经典脚本路线使用importScripts),或者将输出切换为真正的 module 输出让 worker 走原生import; - 正确配置
publicPath:worker chunk 的 URL 由__webpack_require__.p + __webpack_require__.u(chunkId)拼接而来,部署到 CDN 或子路径时需要让output.publicPath(或output.workerPublicPath)与实际资源路径一致; - 运行环境要求:
SharedWorker、module worker 与import.meta.url均属较新的浏览器能力,且 worker 脚本需通过 HTTP(S) 加载,本地调试建议使用 dev server 或任意静态服务器从示例目录起服务。
八、延伸阅读:测试与同族示例
- 本示例配套测试集中在 test/configCases/worker 目录(含
blob、custom-worker、entry-options、entry-options-merge、module等 30 余组用例),覆盖了 worker 引用的各种形态与 entry 选项合并规则,可作为验证本文结论的对照材料; - Worker chunk 加载 runtime 的正式实现见 lib/webworker/ImportScriptsChunkLoadingRuntimeModule.js 与 lib/webworker/ImportScriptsChunkLoadingPlugin.js,解析插件见 lib/dependencies/WorkerAndWorkletPlugin.js,依赖与代码生成见 lib/dependencies/WorkerDependency.js;
- 若想把本示例加入批量构建,可参考 examples/buildAll.js 提供的 examples 统一构建入口;单示例调试可在
examples/worker目录下直接以 webpack CLI 构建并把产物交给任意静态服务器,再通过 examples/worker/index.html 页面验证 Worker 与 SharedWorker 的实时行为。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00