首页
/ webpack 实战:用 Web Worker 与 SharedWorker 实现后台计算与多页面共享——examples/worker 完整源码解析

webpack 实战:用 Web Worker 与 SharedWorker 实现后台计算与多页面共享——examples/worker 完整源码解析

2026-09-07 09:29:41作者:何将鹤

本篇文章以 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.jsfib-worker.jsfibonacci.js new Worker(new URL(...), { name, type }) worker 作为独立 entry bundle,内部可继续动态 import()
多页面/多连接共享聊天记录 chat-worker.jschat-module.js new SharedWorker(new URL(...)) + port.onmessage SharedWorker 单实例多端口通信
主线程内直接计算作对照 fibonacci.js import() 动态导入 与 worker 内 import() 形成对照,验证 chunk 共享

运行入口是 index.html,它只加载编译产物 ./dist/main.js;编译配置为 webpack.config.js。由于页面逻辑中用到了 SharedWorkerimport.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.jsfib-worker.js 都不是手动声明的入口,而是 webpack 在解析到 new Worker(...) / new SharedWorker(...) 时自动生成的"worker 入口 chunk";
  • output.filename / chunkFilename 使用 [name].jschunkIds: "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 的三个关键决策:

  1. worker 拥有独立运行时fibonaccichat 在编译统计中各自以 chunk (...runtime: <hash>) 形式出现,说明 webpack 会为每个 worker 分配独立 runtime,使其能脱离主 bundle 独立启动;
  2. worker 内部可继续代码分割129.js 这一异步 chunk 同时被 ./example.js 的第 70 行 import()./fib-worker.js 第 2 行的 import() 引用,即主线程与 worker 共享同一份公共模块 chunk
  3. SharedWorker 的依赖模块被就地合并chat-module.jschat-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.jsonmessage 接收消息,并在 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.mdchat.js 的产物可以看到,webpack 将 chat-module.jschat-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.jsdist/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.mdmain.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 层注册了钩子。它的整体工作流程如下:

  1. 语法匹配:把 new Workernew SharedWorkernavigator.serviceWorker.register() 等(源码中的 WORKER_DEFAULT_SYNTAX)注册进 parser。默认识别 Worker / SharedWorkernew 表达式;parser.worker 配置项可自定义/关闭匹配语法集;
  2. URL 解析resolveWorkerUrl 分析第一参数,确认它是 new URL("./xxx.js", import.meta.url) 形式,从而得到模块相对路径与代码区间;同时也支持把"纯 import.meta.url"等形态识别为 worker 引用点;
  3. 选项提取parseObjectExpression 从第二参数对象字面量中静态提取 name 等可编译期求值的字段;parseEntryOptions 读取魔术注释 webpackEntryOptions / webpackChunkName(这也是示例中 { filename: "workers/[name].js" } 生效的入口),并防御性地过滤 __proto__ 等危险键;
  4. 生成 worker entry:构造 AsyncDependenciesBlock,其 entryOptionsworker: true;同时 ensureRuntime 会为每个 worker 生成一段基于模块文件名的唯一 runtime 摘要,保证每个 worker 拥有自己独立的 runtime chunk;
  5. 改写调用点handleNewWorker 还会处理第二参数里的 type 字段——当输出不是 ESM 模块时,把 type: "module"ConstDependency 改写为 undefined,从而让产物中的 worker 以经典脚本方式被浏览器加载(对应第四节观察到的产物形态);
  6. 生成依赖WorkerDependency(type 为 "new Worker()",category 为 "worker")被放入该 block,经 NormalModuleFactory 解析出真正的 worker 模块,最后交由 WorkerDependency.Template 在代码生成阶段完成 URL 替换。

该插件同时管理 WorkletDependencyaudioWorkletCSS.paintWorkletaddModule 语法,WORKLET_DEFAULT_SYNTAX),因此 Worker 与 Worklet 使用同一套解析基础设施,行为统一。

七、worker 打包的实践建议与限制

综合示例与实现,落地到自己项目中时建议注意以下几点:

  1. 始终使用 new URL(...) + import.meta.url 的静态形式引用 worker 文件,而不是字符串拼接路径——这是 webpack 能识别依赖、参与 tree-shaking 与内容哈希的前提;
  2. 善用 name 选项与魔术注释控制产物:name: "chat" 决定 chunk 语义名,webpackEntryOptions 可注入 filename 等 entry 级选项,从而把 worker 产物组织到独立子目录(本示例的 dist/workers/);
  3. worker 内也可以做代码分割fib-worker.js 中的 import("./fibonacci") 展示 worker 与主线程可共享异步 chunk。不过请确认目标环境的 worker 支持动态加载(经典脚本路线使用 importScripts),或者将输出切换为真正的 module 输出让 worker 走原生 import
  4. 正确配置 publicPath:worker chunk 的 URL 由 __webpack_require__.p + __webpack_require__.u(chunkId) 拼接而来,部署到 CDN 或子路径时需要让 output.publicPath(或 output.workerPublicPath)与实际资源路径一致;
  5. 运行环境要求SharedWorker、module worker 与 import.meta.url 均属较新的浏览器能力,且 worker 脚本需通过 HTTP(S) 加载,本地调试建议使用 dev server 或任意静态服务器从示例目录起服务。

八、延伸阅读:测试与同族示例

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
899
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
532
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
521
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
392