PDF.js 从 demo 深入浅出 8: PDFWorker.create 与真假 Worker 握手
2026年10月27日 · StreamPDF Team

前言
第 7 篇已经从 WorkerTransport 走到了 GetDoc. 本篇回到更早的位置, 补齐第 2 篇第 26-33 行刻意略过的调用:
worker = PDFWorker.create({
verbosity: undefined,
port: undefined, // GlobalWorkerOptions.workerPort, demo 未设置
});
task._worker = worker;此时第一篇已经配置:
pdfjsLib.GlobalWorkerOptions.workerSrc =
"https://cdn.jsdelivr.net/npm/pdfjs-dist@6.3.289/build/pdf.worker.mjs";本篇跟随这两个具体配置, 回答四个问题:
PDFWorker.create是同步返回, 还是等 worker 启动后才返回?- CDN 上的
workerSrc如何变成浏览器的Worker? ready与test两条消息如何构成启动握手?- 真 worker 不可用时, 假 worker 如何让后续代码不变?
本篇只跟随 worker 的创建与就绪路径.
GetDocRequest是握手成功之后的下一条消息, 文档级MessageHandler与WorkerTransport分别见第 6、7 篇. 完整类结构可对照 api.js 源码解读.
PDFWorker.create 调用
第 1 步, create 先查共享 port 缓存
PDFWorker.create 本身很短:
static create(params) {
const cachedPort = this.#workerPorts.get(params?.port);
if (cachedPort) {
if (cachedPort._pendingDestroy) {
throw new Error("PDFWorker.create - the worker is being destroyed...");
}
return cachedPort;
}
return new PDFWorker(params);
}它是一个同步工厂, 只有两种结果:
- 用户提供了可复用的
port, 且缓存中已有PDFWorker: 直接返回旧实例; - 否则: 创建新实例.
demo 的 params.port = undefined, 没有可复用端口, 因此进入 new PDFWorker(params). task._worker = worker 保存这个新实例, 供加载任务销毁时清理.
create不是 async 函数, 也不返回 promise. 新实例会同步返回, 启动结果则挂在实例的worker.promise上.
第 2 步, 构造函数给 undefined 应用默认值
构造参数经过解构:
constructor({
name = null,
port = null,
verbosity = getVerbosityLevel(),
} = {}) {
this.name = name;
this.destroyed = false;
this.verbosity = verbosity;
if (port) {
// 用户提供的共享 worker 分支
} else {
this.#initialize();
}
}JavaScript 的默认值会在属性值为 undefined 时生效, 所以 demo 实际得到:
| 字段 | 值 | 来源 |
|---|---|---|
name | null | 未传入 |
port | null | 显式传入 undefined, 触发默认值 |
verbosity | 1 | getVerbosityLevel(), 默认 VerbosityLevel.WARNINGS |
destroyed | false | 构造时初始化 |
port 为假, demo 进入 #initialize(), 尝试创建真 worker.
第 3 步, 检查是否可以直接回退
#initialize() 开头先看两个条件:
if (
PDFWorker.#isWorkerDisabled ||
PDFWorker.#mainThreadWorkerMessageHandler
) {
this.#setupFakeWorker();
return;
}#isWorkerDisabled: Node 环境会在类初始化时置为true; 浏览器里此前如果回退过, 也会置为true, 后续文档不再重复尝试真 worker;#mainThreadWorkerMessageHandler: 如果主线程已经通过globalThis.pdfjsWorker提供 worker 实现, 直接使用它.
demo 在普通浏览器中首次创建, 两个条件都不满足, 继续真 worker 路径.
workerSrc 到 new Worker
第 4 步, 读取 GlobalWorkerOptions.workerSrc
let { workerSrc } = PDFWorker;这里的 getter 不是简单返回空值:
static get workerSrc() {
if (GlobalWorkerOptions.workerSrc) {
return GlobalWorkerOptions.workerSrc;
}
throw new Error('No "GlobalWorkerOptions.workerSrc" specified.');
}demo 已在第一篇设置 CDN 地址, 因此得到完整的 https://cdn.jsdelivr.net/.../pdf.worker.mjs. 如果没有设置, 异常会被 #initialize() 外层 try...catch 捕获, 随后尝试假 worker; 但假 worker 同样需要加载这份代码, 所以官方建议始终显式配置 workerSrc.
第 5 步, demo 命中 CDN Blob wrapper
demo 页面位于 streampdf.com, worker 文件位于 cdn.jsdelivr.net, 两者不同源. 浏览器不允许直接把跨域 URL 交给 new Worker(...), PDF.js 因此先判断:
if (!PDFWorker._isSameOrigin(window.location, workerSrc)) {
workerSrc = PDFWorker._createCDNWrapper(
new URL(workerSrc, window.location).href
);
}_createCDNWrapper 不下载源码, 只生成一个极小的模块包装器:
const wrapper = `await import("${url}");`;
return URL.createObjectURL(
new Blob([wrapper], { type: "text/javascript" })
);于是 workerSrc 从 CDN URL 变为同源 blob: URL. 包装模块里再通过 import() 加载 CDN 上的 ES module.
直接 new Worker("https://cdn.jsdelivr.net/...") // 跨域, 会被拒绝
new Worker("blob:...", { type: "module" })
→ blob 模块执行 await import("https://cdn.jsdelivr.net/...")
→ CDN 返回支持跨域加载的 worker 模块第 6 步, 创建 Worker 与全局 MessageHandler
const worker = new Worker(workerSrc, { type: "module" });
const messageHandler = new MessageHandler("main", "worker", worker);这里的 MessageHandler 名称是 main ↔ worker, 服务的是整个 worker 的全局通道, 目前只注册 test、configure、GetDocRequest 等全局消息.
第 6 篇的 d1 ↔ d1_worker 则要在之后处理 GetDocRequest 时才创建, 服务某一份文档. 两层通道不要混淆:
| 通道 | 创建时机 | 典型消息 |
|---|---|---|
main ↔ worker | 本篇创建 Worker 后 | ready、test、configure、GetDocRequest |
d1 ↔ d1_worker | worker 处理 GetDocRequest 时 | Ready、GetDoc、GetPage、GetOperatorList |
主线程接下来注册 error、test、ready 三个事件处理器, 为握手做准备.
ready/test 双向握手
第 7 步, 主线程立即发送第一次 test
sendTest 的定义与第一次调用如下:
const sendTest = () => {
const testObj = new Uint8Array();
messageHandler.send("test", testObj, [testObj.buffer]);
};
sendTest();发送内容是一个空的 Uint8Array, 它的 ArrayBuffer 被列入 transfer 列表. 测试目的不是传输 PDF 数据, 而是验证两件基础能力:
- 主线程与 worker 的消息通道能够工作;
- TypedArray 经过
postMessage后, 在 worker 侧仍然保持正确的类型.
PDF 字节、字体和图像数据随后都依赖这条二进制通道, 所以启动时先做一次最小成本的能力探测.
第 8 步, worker 启动并发送 ready
pdf.worker.mjs 加载 WorkerMessageHandler. 它的静态块检测到当前环境是 worker 后, 执行:
this.initializeFromPort(self);初始化函数建立另一侧的全局通道:
static initializeFromPort(port) {
const handler = new MessageHandler("worker", "main", port);
this.setup(handler, port);
handler.send("ready", null);
}setup 注册三个与本文相关的处理器:
handler.on("test", ...);
handler.on("configure", ...);
handler.on("GetDocRequest", ...);随后 worker 主动发送 ready. 这条消息只表示「worker 代码已加载, 全局消息路由已注册」, 不代表 PDF 文档已经加载——此时 worker 甚至连 docParams 都还没收到.
第 9 步, ready 触发第二次 test
主线程的 ready 处理器再次调用 sendTest():
messageHandler.on("ready", data => {
ac.abort();
if (this.destroyed) {
terminateEarly();
return;
}
try {
sendTest();
} catch {
this.#setupFakeWorker();
}
});因此一次正常启动中, 主线程可能发送两次 test:
- 创建
Worker后立即发送一次; - 收到
ready后再发送一次.
worker 用闭包变量保证只处理第一条:
let testMessageProcessed = false;
handler.on("test", data => {
if (testMessageProcessed) {
return;
}
testMessageProcessed = true;
handler.send("test", data instanceof Uint8Array);
});如果第一条 test 在 worker 初始化期间被排队, ready 后的第二次发送提供兜底; 如果第一条已经处理, 第二条直接忽略. 两次发送消除了脚本加载速度不同带来的时序竞争.
ready的方向是 worker → 主线程, 表示 worker 脚本就绪; 第 7 篇的Ready方向是主线程 → worker, 表示文档级主线程路由就绪. 大小写不同, 语义与发生阶段也不同.
第 10 步, test 回包让 worker.promise 就绪
worker 处理第一条 test 后, 回发一个布尔值:
handler.send("test", data instanceof Uint8Array);主线程收到 true 后:
messageHandler.on("test", data => {
ac.abort();
if (this.destroyed || !data) {
terminateEarly();
return;
}
this.#messageHandler = messageHandler;
this.#port = worker;
this.#webWorker = worker;
this.#resolve();
});直到这一刻, 三个字段才挂到 PDFWorker 实例上. 此前的 new PDFWorker(params) 虽然已经同步返回, 但启动仍未宣告成功.
#resolve() 做两件事:
#resolve() {
this.#capability.resolve();
this.#messageHandler.send("configure", {
verbosity: this.verbosity,
});
}- resolve
worker.promise, 其值是undefined(Promise<void>), 不是另一个 worker 对象; - 发送
configure, demo 中把verbosity = 1同步给 worker, worker 调用setVerbosityLevel(1).
如果 test 回包为 false, 或等待期间任务已销毁, 主线程不会 resolve, 而是进入清理或回退路径.
第 11 步, 与 gpuPromise 汇合
真 worker 握手成功后, 第 2 篇的等待条件才成立:
Promise.all([worker.promise, gpuPromise]).then(([, hasGPU]) => {
// 回填 hasGPU, 然后发送 GetDocRequest
});demo 的 gpuPromise = Promise.resolve(false) 早已完成, 实际等待的是 worker.promise.
因此完整顺序是:
PDFWorker.create 同步返回
→ new Worker / ready / test 在后台推进
→ test 回包为 true
→ worker.promise resolve
→ Promise.all 继续
→ 发送 GetDocRequest
→ 创建文档级 d1 ↔ d1_worker 通道PDFWorker.create 的返回值与 worker.promise 分别回答「实例是否已创建」和「实例是否可工作」, 两者不能混为一谈.
假 worker: 同一协议, 主线程运行
哪些情况会回退
真 worker 路径的任何启动失败, 都可能进入 #setupFakeWorker():
| 情况 | 处理方式 |
|---|---|
| Node 环境或此前已禁用 worker | 直接回退 |
主线程已有 globalThis.pdfjsWorker | 直接使用该实现 |
new Worker(...) 或早期 sendTest() 抛错 | 捕获后回退 |
worker 初始化期间触发 error | 清理端口与 Worker 后回退 |
test 回包为 false | 清理后回退 |
等待期间 PDFWorker.destroy() | 不降级, reject worker.promise |
terminateEarly 会中止事件监听、销毁全局 MessageHandler 并调用 worker.terminate(), 避免失败的真实 worker 留在后台.
LoopbackPort 替换线程边界
假 worker 先通过动态 import(this.workerSrc) 取得 WorkerMessageHandler, 然后建立本地回环端口:
const port = new LoopbackPort();
const id = `fake${PDFWorker.#fakeWorkerId++}`; // 首次为 fake0
const workerHandler = new MessageHandler("fake0_worker", "fake0", port);
WorkerMessageHandler.setup(workerHandler, port);
this.#port = port;
this.#messageHandler = new MessageHandler("fake0", "fake0_worker", port);
this.#resolve();它不再创建 Worker, 也不执行 ready/test 握手. WorkerMessageHandler.setup 直接在主线程注册同一组 test、configure、GetDocRequest 处理器, 随后 worker.promise 就绪.
LoopbackPort 与真实 Worker 提供相同的 postMessage / 消息监听接口, 所以后续调用不变:
worker.messageHandler.sendWithPromise("GetDocRequest", docParams);区别只在执行位置: 真 worker 把解析放到另一个线程, 假 worker 在主线程执行相同代码. 第 6、7 篇的 MessageHandler 与 WorkerTransport 不需要知道当前是哪一种.
demo 未走: 共享 workerPort 分支
如果用户设置的是:
pdfjsLib.GlobalWorkerOptions.workerPort = myWorker;PDFWorker.create({ port: myWorker }) 会优先查 #workerPorts:
- 已缓存且没有
_pendingDestroy: 返回已有PDFWorker, 多份文档共享同一个 worker; - 已缓存但正在销毁: 立即抛错, 防止拿到半销毁实例;
- 未缓存:
new PDFWorker({ port })进入#initializeFromPort(port).
#initializeFromPort 不再读取 workerSrc, 也不发送 test:
this.#port = port;
this.#messageHandler = new MessageHandler("main", "worker", port);
this.#messageHandler.on("ready", () => {
// 外部 port 应已经完成初始化, 忽略 ready
});
this.#resolve();这是因为调用方提供的是一个已经初始化、可由 PDF.js 直接使用的 Worker. demo 没有设置 workerPort, 所以走的是本文前面的完整创建与测试路径.
总结
本篇跟随 demo 走完了 PDFWorker.create 的完整启动链:
create同步查询共享 port 缓存, demo 未命中, 创建新PDFWorker;- 构造参数中的
undefined触发默认值:port = null,verbosity = 1; - 读取第一篇配置的
GlobalWorkerOptions.workerSrc; - demo 的 CDN 地址跨域, 因此生成同源 Blob module wrapper, 再创建
Worker; - 主线程建立
main ↔ worker全局MessageHandler, 立即发送第一次test; - worker 脚本初始化后注册全局处理器并发送
ready, 主线程再补发一次test; - worker 只处理第一条
test, 检查Uint8Array类型并回发布尔值; - 主线程收到
true, 保存 port 与 handler, resolveworker.promise, 再发送configure; Promise.all([worker.promise, gpuPromise])继续, 后续才发送GetDocRequest;- 真 worker 启动失败时, 用动态 import +
LoopbackPort在主线程运行同一套 worker 协议, 上层无需改变.
所以 PDFWorker 并不是简单的 new Worker 包装: 它同时负责启动时序、二进制能力探测、共享端口复用与透明降级. 这也是第 2 篇只凭 worker.promise 就能等待后续握手的原因.
第 9 篇回到第 7 篇留下的 GetReader 钩子, 展开网络流的 range 能力探测与 GetRangeReader 请求策略.
StreamPDF 的设计原则: 能前置处理的绝对不放到解析的时候才做, 由服务端一次解决所有性能问题. 浏览器只获取&渲染必须数据.
相关文章
- PDF.js 从 demo 深入浅出 10: CanvasGraphics 与 operatorList 绘制2026年10月27日
- PDF.js 从 demo 深入浅出 9: 网络流与 Range 请求2026年10月27日
- PDF.js 从 demo 深入浅出 7: WorkerTransport 初始化2026年10月27日
- PDF.js 从 demo 深入浅出 6: MessageHandler 跨线程通讯2026年10月27日
- PDF.js 从 demo 深入浅出 5: render 方法全貌2026年10月27日
- PDF.js 从 demo 深入浅出 4: getViewport 方法全貌2026年10月27日
- PDF.js 从 demo 深入浅出 3: getPage 方法全貌2026年10月27日
- PDF.js 从 demo 深入浅出 2: getDocument 方法全貌2026年10月27日
- PDF.js 源码解读: TextLayer2026年10月27日
- PDF.js 源码解读: font_loader2026年10月27日
- PDF.js 源码解读: obj_bin_transform_display2026年10月27日
- PDF.js 源码解读: PDFObjects2026年10月27日
- PDF.js 源码解读: 网络流2026年10月27日
- PDF.js 源码解读: internal_evt.js2026年10月27日
- PDF.js 源码解读: math_clamp.js2026年10月27日
- PDF.js 源码解读: css_utils.js2026年10月27日
- PDF.js 源码解读: scripting_utils2026年10月27日
- PDF.js 源码解读: MurmurHash3_642026年10月27日
- PDF.js 源码解读: obj_bin_transform_utils2026年10月27日
- PDF.js 源码解读: image_utils.js2026年10月27日
- PDF.js 源码解读: BasePDFStream2026年10月27日
- PDF.js 源码解读: 共享工具层2026年10月27日
- PDF.js 源码解读: MessageHandler2026年10月27日
- PDF.js 源码解读: api2026年10月27日
- PDF.js 源码解读: fonts2026年10月27日
- PDF.js 源码解读: glyphlist2026年10月27日
- PDF.js 源码解读: binary_cmap2026年10月27日
- PDF.js 源码解读: cmap2026年10月27日
- PDF.js 源码解读: encodings2026年10月27日
- PDF.js 源码解读: metrics2026年10月27日
- PDF.js 源码解读: standard_fonts2026年10月27日
- PDF.js 源码解读: fonts_utils2026年10月27日
- PDF.js 源码解读: default_appearance2026年10月27日
- PDF.js 源码解读: metadata_parser2026年10月27日
- PDF.js 源码解读: name_number_tree2026年10月27日
- PDF.js 源码解读: struct_tree2026年10月27日
- PDF.js 源码解读: catalog2026年10月27日
- PDF.js 源码解读: pdf_manager2026年10月27日
- PDF.js 源码解读: object_loader2026年10月27日
- PDF.js 源码解读: obj_bin_transform_core2026年10月27日
- PDF.js 源码解读: crypto2026年10月27日
- PDF.js 源码解读: core_utils2026年10月27日
- PDF.js 源码解读: primitives2026年10月27日
- PDF.js 源码解读: worker_stream2026年10月27日
- PDF.js 源码解读: ccitt_stream2026年10月27日
- PDF.js 源码解读: jpx_stream2026年10月27日
- PDF.js 源码解读: jbig2_stream2026年10月27日
- PDF.js 源码解读: jpeg_stream2026年10月27日
- PDF.js 源码解读: decrypt_stream2026年10月27日
- PDF.js 源码解读: brotli_stream2026年10月27日
- PDF.js 源码解读: run_length_stream2026年10月27日
- PDF.js 源码解读: ascii_hex_stream2026年10月27日
- PDF.js 源码解读: ascii_85_stream2026年10月27日
- PDF.js 源码解读: lzw_stream2026年10月27日
- PDF.js 源码解读: predictor_stream2026年10月27日
- PDF.js 源码解读: flate_stream2026年10月27日
- PDF.js 源码解读: decode_stream2026年10月27日
- PDF.js 源码解读: chunked_stream2026年10月27日
- PDF.js 源码解读: stream2026年10月27日
- PDF.js 源码解读: base_stream2026年10月27日
- PDF.js 深入浅出 11: Parser2026年10月26日
- PDF.js 深入浅出 10: XRef2026年10月26日
- PDF.js 深入浅出 9: CanvasGraphics2026年10月26日
- PDF.js 深入浅出 8: Document 与 Page2026年10月26日
- AI 时代, 我们到底应该做什么才能活下去(StreamPDF 创立有感)2026年10月26日
- PDF.js 深入浅出 7: PartialEvaluator2026年10月25日
- PDF.js 深入浅出 6: PDFManager.initDocument2026年10月25日
- PDF.js 深入浅出 5: PDFFetchStream 和 PDFWorkerStream2026年10月25日
- PDF.js 深入浅出 4: MessageHandler2026年10月22日
- PDF.js 深入浅出 3: getViewport & render2026年10月22日
- PDF.js 从 demo 深入浅出 1: 自定义 viewer 渲染 PDF2026年10月19日
- PDF.js 深入浅出 2: getDocument & getPage2026年10月19日
- PDF.js 深入浅出 1: PDF.js 的架构设计2026年10月16日