PDF.js 从 demo 深入浅出 3: getPage 方法全貌
2026年10月27日 · StreamPDF Team

前言
在前一篇文章中, 我们通过 await loadingTask.promise 拿到了 pdf 变量, 它是一个 PDFDocumentProxy 实例.
回到第一篇的 demo, 第 8-10 行:
const page = await pdf.getPage(1);传入数字参数:
- pageNumber: 页码, 第一页是 1. 值为
1
getPage 返回一个 Promise, resolve 出一个 PDFPageProxy 对象. 本篇跟随这次调用进入源码.
getPage 调用
依据 api.js 中 PDFDocumentProxy 的定义, 代码执行流程如下.
PDFDocumentProxy.getPage: 一行委托
getPage(pageNumber) {
return this._transport.getPage(pageNumber);
}pdf 对象本身只是个门面, 真正的逻辑在 this._transport 上 — 即第二篇中创建的 WorkerTransport 实例 (task._transport). 以下全部流程都在 WorkerTransport.getPage 中.
第 1 步, 参数校验
if (
!Number.isInteger(pageNumber) ||
pageNumber <= 0 ||
pageNumber > this.pagesMapper.pagesNumber
) {
return Promise.reject(new Error("Invalid page request."));
}代入 demo 的值:
Number.isInteger(1)为true1 > 0this.pagesMapper.pagesNumber为756— 文档就绪时由pdfInfo.numPages写入. 本文的 demo PDF 共 756 页,1 <= 756
校验通过. 若传入 0, 负数, 小数或超出页数, 这里直接返回 rejected promise.
Q: 为什么用
Promise.reject而不是throw? A: 保持异步语义一致.getPage的调用方总是await一个 promise, 参数错误也走同一条错误通道, 调用方不需要同时处理同步 throw 和异步 reject 两种情况.
第 2 步, 页码换算
const pageIndex = pageNumber - 1; // 1 - 1 = 0
const newPageIndex = this.pagesMapper.getPageId(pageNumber) - 1; // 1 - 1 = 0这里出现了两套编号:
pageNumber: 用户视角的页码, 1-based, 符合直觉pageIndex: 内部视角的页码, 0-based, 用作数组下标和缓存 key
pagesMapper.getPageId(1) 返回 1. PagesMapper 是第 2 篇参数解析时 new PagesMapper() 创建的, 它维护「用户看到的页码」与「原始文档页 ID」之间的映射. demo 没有对页面做过移动/删除/复制, 映射是恒等的 (#pageNumberToId 为 null, getPageId 原样返回入参), 因此 newPageIndex === pageIndex === 0.
Q: 为什么需要 PagesMapper 绕一圈? A: 为了支持页面重排. 在编辑器场景下, 用户可以移动, 删除, 复制页面, 此后「第 3 页」未必还是原始文档的第 3 页. worker 端只认原始文档的页序, 因此发送给 worker 的是
newPageIndex; 而主线程缓存以用户视角的pageIndex为 key. 恒等映射下两者相同, 可以略过不计.
第 3 步, 查询缓存
const cachedPromise = this.#pagePromises.get(0);
if (cachedPromise) {
return cachedPromise;
}#pagePromises 是 Map<pageIndex, Promise<PDFPageProxy>>. 首次调用未命中, 继续往下.
Q: 缓存的效果是什么? A: 实测连续两次
await pdf.getPage(1), 返回的是同一个PDFPageProxy对象 (page === page2为true). 因为第二次调用直接命中缓存的 promise, 不会再向 worker 发消息.
第 4 步, 向 worker 发送 GetPage 消息
const promise = this.messageHandler
.sendWithPromise("GetPage", {
pageIndex: 0, // newPageIndex
})
.then(pageInfo => { ... });
this.#pagePromises.set(0, promise);
return promise;messageHandler 是第 2 篇创建的 MessageHandler, sendWithPromise 的通讯机制另文展开. 这里只需知道: 它把消息发给 worker 线程, 并返回一个 promise, 等待 worker 的回执.
worker 端收到 GetPage 后 (core/worker.js):
handler.on("GetPage", async function ({ pageIndex }) {
const page = await pdfManager.getPage(pageIndex);
const [rotate, ref, userUnit, view] = await Promise.all([
pdfManager.ensure(page, "rotate"),
pdfManager.ensure(page, "ref"),
pdfManager.ensure(page, "userUnit"),
pdfManager.ensure(page, "view"),
]);
return {
rotate,
ref,
refStr: ref?.toString() ?? null,
userUnit,
view,
};
});worker 内部如何解析页面对象, 另文展开. demo 第一页的回执为:
pageInfo = {
rotate: 0,
ref: { num: 4, gen: 0 }, // 跨线程结构化克隆后, ref 退化为普通对象
refStr: "4 0 R",
userUnit: 1,
view: [0, 0, 595, 842],
}Q: 为什么只取这 4 个属性? A: 它们全是页面的元数据: 旋转角度, 对象引用, 单位换算因子, 可见区域. 读取它们只需解析页面对象字典, 不碰页面内容流. 真正的内容解析 (绘制指令) 被推迟到
render时的GetOperatorList消息. 这是getPage能快速返回的原因.这 4 个属性随后一一对应
PDFPageProxy上的 getter:rotate,ref,userUnit,view. 下一篇的getViewport会用到其中两个.
第 5 步, 创建 PDFPageProxy
回执到达后, 进入 .then 回调:
.then(pageInfo => {
if (this.destroyed) {
throw new Error("Transport destroyed");
}
if (pageInfo.refStr) {
this.#pageRefCache.set("4 0 R", 0);
}
const page = new PDFPageProxy(0, pageInfo, this, this.pagesMapper, false);
this.#pageCache.set(0, page);
return page;
});依次做了三件事:
destroyed检查: 等待回执期间文档可能已被销毁 (loadingTask.destroy()), 是则抛错;#pageRefCache反向缓存:"4 0 R"→0. 供getPageIndex(ref)反查 — 例如大纲跳转时, PDF 内部用对象引用 (4 0 R) 定位页面, 需要反查出页码;- 创建
PDFPageProxy并写入#pageCache.
PDFPageProxy 的构造函数只做字段赋值, 没有任何解析:
this._pageIndex = 0;
this._pageInfo = pageInfo;
this._transport = transport;
this._stats = null; // pdfBug 为 false, 不启用性能统计
this.commonObjs = transport.commonObjs; // 全文档共享的资源容器 (字体等)
this.objs = new PDFObjects(); // 本页私有的资源容器
this._intentStates = new Map(); // 渲染意图状态缓存, 第 5 篇 render 会用到
this.destroyed = false;commonObjs 与 objs 是后续渲染时的资源容器, PDFObjects 另文展开.
至此, getPage(1) 的 promise resolve 出 page, demo 中 await 拿到它.
总结
本节跟随 demo 走完了 getPage(1) 的完整调用:
PDFDocumentProxy.getPage一行委托给WorkerTransport.getPage;- 参数校验:
1是合法页码 (总页数 756); - 页码换算:
pageNumber(1)→pageIndex(0)→newPageIndex(0), 恒等映射下三者一致; - 查询
#pagePromises缓存, 首次未命中; - 向 worker 发送
GetPage消息, 收到 4 个页面元数据; - 写入
#pageRefCache, 创建PDFPageProxy, 写入#pageCache与#pagePromises; - 返回 promise, demo
await得到page.
过程中刻意略过了两个「重角色」: MessageHandler.sendWithPromise 的跨线程通讯, 以及 worker 端 pdfManager.getPage 的页面解析. 它们各自独立成文.
拿到 page 之后, demo 的下一步是 page.getViewport({ scale: 0.5 }) — 它会用到本篇 pageInfo 里的 view 和 userUnit. 下一篇展开.
相关文章
- PDF.js 从 demo 深入浅出 10: CanvasGraphics 与 operatorList 绘制2026年10月27日
- PDF.js 从 demo 深入浅出 9: 网络流与 Range 请求2026年10月27日
- PDF.js 从 demo 深入浅出 8: PDFWorker.create 与真假 Worker 握手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 深入浅出 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日