1. 为什么选择React + pdfjs-dist + pubsub-js?
如果你正在为你的Web应用寻找一个PDF阅读器的解决方案,市面上其实有不少现成的库,比如react-pdf-viewer。但说实话,我踩过不少坑。要么是功能太臃肿,引入了大量你用不上的代码;要么是定制化程度太低,想改个样式或者加个交互都束手束脚。最后你会发现,自己动手,丰衣足食。
这就是为什么我最终选择了 React + pdfjs-dist + pubsub-js 这个组合。我来给你拆解一下这个“黄金三角”各自扮演的角色:
- React:负责搭建整个应用的骨架和界面。我们用它来创建组件,管理状态(比如当前缩放比例、总页数),响应用户的点击、输入等操作。它的组件化思想让我们的代码结构非常清晰,主展示区、侧边栏、控制栏各司其职。
- pdfjs-dist:这是Mozilla(火狐浏览器背后的组织)开源的PDF渲染引擎,是真正的“实力派”。它不负责界面,只干最核心的脏活累活:解析PDF文件,把每一页的内容渲染到
<canvas>画布上。它的能力非常底层且强大,为我们提供了最大的灵活性。 - pubsub-js:这是一个轻量级的“发布-订阅”库。你可以把它想象成一个高效的“广播站”或“事件中心”。在我们的阅读器里,PDF渲染是一个异步操作,而且可能涉及多个独立组件(比如主视图渲染和缩略图渲染)。
pubsub-js的作用就是让这些组件之间能够“优雅地对话”,而不需要它们直接互相引用或传递复杂的回调函数。比如,主区域渲染完成时,发布一个renderFinish消息,控制栏组件订阅了这个消息,收到后就知道可以显示操作按钮了。
这个组合的优势在于,它把视图层(React)、核心渲染层(pdfjs-dist) 和通信层(pubsub-js) 完美解耦了。你完全可以掌控每一个细节,从UI设计到交互逻辑,都能按照你的想法来。接下来,我就带你一步步把这个阅读器从零搭建起来,我会把我在实战中遇到的“坑”和优化技巧都分享给你。
2. 项目初始化与核心依赖安装
万事开头先搭环境。假设你已经有了一个React项目(用Create React App或者Vite创建的都行),我们首先需要把两位“核心员工”请进来。
打开你的终端,在项目根目录下执行:
npm install pdfjs-dist pubsub-js
这里简单解释一下:
pdfjs-dist:我们安装的是它的客户端版本,它包含了在浏览器中运行所需的所有代码。pubsub-js:非常小巧,API也极其简单,几乎零学习成本。
安装完成后,我们需要对pdfjs-dist进行一个关键配置。pdfjs-dist在渲染时,会把一些计算密集型任务(比如解析字体、解码图片)交给一个Web Worker去处理,以避免阻塞主线程导致页面卡顿。所以我们需要告诉它这个Worker脚本在哪里。
在你的入口文件(比如displayPDF.js或App.js的顶部),添加以下配置:
import * as PDF from 'pdfjs-dist';
// 注意:这里导入的是worker的入口文件,webpack等打包工具会处理它
import workerSrc from 'pdfjs-dist/build/pdf.worker.entry';
// 将Worker的路径设置给全局配置
PDF.GlobalWorkerOptions.workerSrc = workerSrc;
这一步非常重要,如果缺失,在渲染复杂PDF时你可能会在控制台看到错误,并且页面渲染会非常慢。我当初就忘了配,调试了半天才发现是这个问题。
至于pubsub-js,它暂时不需要特殊配置,我们会在用到的时候直接引入。现在,我们的基础环境就准备好了。
3. 核心渲染引擎:异步渲染PDF页面
这是整个阅读器的“心脏”功能。我们的目标不仅仅是把PDF画出来,还要画得快、画得流畅,让用户一打开就能看到内容,而不是对着一个空白页面干等。
原始文章里提供了一个displayPDF函数,它接受PDF地址、父容器等参数。我这里想重点和你深入聊聊它内部的 mainAreaAsyncRender 异步渲染函数。这个函数的设计是性能优化的关键。
想象一下,一个上百页的PDF,如果等所有页面都渲染完再一次性显示给用户,那体验将是灾难性的。我们采用的策略是“来一页,显示一页”。但直接用for循环去await每一页,虽然也是顺序执行,但页面仍然要等到循环结束才会统一更新。
我们来看看优化后的实现思路:
async function mainAreaAsyncRender(pdf, totalPages, currentPage, scale, canvasClassName, outputScale, containerNode, callback) {
// 1. 获取当前页
const page = await pdf.getPage(currentPage);
// 2. 根据缩放比例创建视口
const viewport = page.getViewport({ scale });
// 3. 创建canvas元素
const canvas = document.createElement('canvas');
canvas.className = canvasClassName;
const ctx = canvas.getContext('2d');
// 4. 处理高清屏(R


2517

被折叠的 条评论
为什么被折叠?



