简介:PDF.js 是 Mozilla 团队推出的开源 JavaScript 库,可在浏览器中不依赖插件直接渲染 PDF 文档。pdfjs-2.2.228-dist.rar 为 2.2.228 版发行包,面向需要在线预览 PDF 的前端开发者,尤其适合在 Vue、React 或原生页面中快速集成文档阅读能力。压缩包共 402 个文件,大小 3.73MB,核心包含 4 个 js 文件(pdf.js 与 pdf.worker.js 等)、1 个 html 示例、168 个 bcmap 字符映射、124 个 properties 配置、86 个 png 与 9 个 svg 图标资源,以及 license、css 等辅助文件,分别承担渲染器、Worker 线程、界面样式和多语言映射等职责。已有 1285 人学习下载,拿到后可直接将 build 目录接入现有页面,快速实现 PDF 加载、翻页、缩放、文本搜索与书签跳转,也可参考 web 目录默认 UI 和样式,按需定制工具栏、全屏模式或表单展示。整体目录结构清晰,是按功能体系理解 PDF.js 内部机制的优质实践素材。 手头项目突然要加一个 PDF 预览功能,运维从网盘里拖下来一个压缩包,文件名就叫pdfjs-2.2.228-dist.rar,解压出来往 Nginx 目录一丢,指望着/web/viewer.html?file=xxx.pdf能直接打开。这个场景我太熟了——pdfjs-dist2.2.228 这个构建版本,过去几年在没有任何前端工程化的项目里出现频率极高,网盘和资源站上一搜一大把。但“能打开”和“能稳定跑在生产环境里”是两回事,这个版本我前前后后踩了不少坑,也帮人擦过不少次屁股。这篇就把基于 2.2.228 做 PDF 预览的完整链路、版本差异、框架集成方式和生产环境里的典型问题一次说完,给正准备“下载一个 pdfjs dist 包直接开干”的朋友一个明确参考。
1. 为什么是 2.2.228:这个版本卡住了多少项目的脖子
很多新入行的前端可能不理解,pdfjs-dist都出到 4.x 甚至 5.x 了,为什么还有一堆老项目锁死在2.2.228。原因其实很实际:PDF.js 从 3.0 开始对构建产物和模块规范做了比较大的调整,而 2.x 时代恰好是各种“散装部署”最舒服的版本。
1.1 一个 .rar 背后的历史包袱
pdfjs-2.2.228-dist.rar解压之后,本质上是 PDF.js 官方仓库里pdfjs-dist这个 npm 包的完整构建输出,版本号 2.2.228 对应的就是 2019 年前后的 PDF.js 2.2 系列。那个时间段,Webpack 4 还是主流,很多老后台系统的前端基建还停留在 jQuery + Bootstrap 或者原生 HTML 阶段。这类项目要加 PDF 预览,最常见的做法不是npm install,而是找一个网上打包好的 dist 压缩包,解压后直接暴露到静态目录。
这个选择本身没有对错,但确实埋下了不少雷。比如我在一个老 OA 系统里见到的情况:运维把整个 dist 目录塞进了项目的public/下,viewer.html 能打开,但点开中文 PDF 后部分字体不显示;又比如某个 uniapp 套壳 App 里,开发者把pdf.js和pdf.worker.js直接放在hybrid/html目录,结果在 Android WebView 里一直报file://协议的跨域错误。这些问题不是 2.2.228 本身多差,而是它作为一套相对完整的渲染引擎,部署方式比想象中敏感得多。
1.2 2.2.228 和 2.6.347 差在哪里
经常被问到:网上很多人推荐 2.6.347,和 2.2.228 到底有什么区别?从我实际使用体验看,2.6.347 在渲染性能上确实有优化,尤其是大文件翻页时的响应速度,2.2.228 偶尔会有明显卡顿。此外 2.6.x 对部分 CSS 变量和 viewer 控件的内部实现做了调整,API 层面没有爆炸性变化,但如果你用的是官方 viewer.html,升级后样式细节会有小改动。
更关键的是 3.x 之后的断档。PDF.js 3.0 把 worker 脚本从pdf.worker.js换成了.mjs模块,在 Webpack 5 和 Vite 里需要额外配置;4.x 又进一步改了部分渲染 API 的内部结构。对于没有构建步骤的老项目,升级成本远远大于收益,所以一直停在 2.2.228 就成了合理选择。这个版本虽然老,但如果你不需要最新的注释编辑器、表单增强这些功能,它的核心getDocument+getPage+render渲染链路是足够稳定的,问题往往出在怎么正确地把它集成到自己的代码里。
2. 把 dist 压缩包拆开:哪些能直接用,哪些得自己动手
拿到压缩包第一件事不是解压扔服务器,而是先看清里面每一部分的作用。2.2.228 的目录结构大体上是固定的:build/下面放着pdf.js、pdf.worker.js、对应的.min.js版本;web/下面是 viewer.html、viewer.js、viewer.css 这套官方阅读器;还有cmaps/和standard_fonts/两个辅助目录。很多人只把 viewer.html 和 build 目录拷走,结果中文乱码,就是漏掉了 cmaps 和 standard_fonts。
2.1 目录里那些文件都是干什么的
pdf.js是主库,负责解析 PDF 文档结构、管理渲染任务;pdf.worker.js是运行在 Web Worker 里的解析和执行引擎,主线程通过消息协议跟它通信。这两个文件必须成对出现,版本还不能乱配——把 2.2.228 的pdf.js配一个 2.6.347 的pdf.worker.js,会出现一些非常诡异的渲染中断,控制台报错也不明显。
cmaps/目录里的.bcmap文件是字符映射表,处理 PDF 内嵌字体的 CID 编码时要用。很多需要显示中文、日文、韩文 PDF 的场景,如果服务端只部署了 build 目录而没带 cmaps,文字就极可能变成乱码或方块。standard_fonts/则是 PDF 标准 14 种字体的替代字形,用于渲染那些没有真正内嵌字体的文档。
2.2 官方 viewer 和自己写渲染逻辑怎么选
dist 里的 viewer.html 是官方阅读器,带工具栏、缩略图、搜索、缩放这些完整功能。如果需求就是“给我一个能看 PDF 的页面”,直接用它是成本最低的方案:静态服务器上把整个 dist 目录放好,访问viewer.html?file=要预览的PDF地址就行。
但官方 viewer 也有很麻烦的一面:定制 UI 得改它的内部结构,跟现有网站的权限系统、下载按钮、水印逻辑对接非常痛苦。我见过不少项目绕了一大圈去 hack viewer.js,最后还不如自己写 50 行渲染代码。如果你只是要“在某个页面里嵌入预览区域,周围还是自己产品的导航和操作按钮”,我强烈建议用pdf.js暴露的 API 自己控制渲染,而不是套 viewer.html。
3. 手写渲染链路:从 PDF 文件到 Canvas 像素
抛开官方 viewer,自己基于 2.2.228 实现 PDF 预览的核心逻辑其实非常简洁。整个过程可以拆成三步:加载文档、拿到页面、渲染到 Canvas。每一层都有状态对象要管理,很多人只关注“画出来”,忽略了 promise 链和销毁逻辑,后面翻页或切文档时就容易出问题。
3.1 核心三步:getDocument、getPage、render
先配置 worker 路径:
pdfjsLib.GlobalWorkerOptions.workerSrc = '/lib/pdfjs/pdf.worker.min.js';然后加载文档:
const loadingTask = pdfjsLib.getDocument({ url: 'https://example.com/files/sample.pdf', }); loadingTask.promise.then((pdf) => { // pdf 对象表示整个文档 console.log('总页数:', pdf.numPages); return pdf.getPage(1); }).then((page) => { // page 对象表示某一页 const viewport = page.getViewport({ scale: 1.5 }); const canvas = document.getElementById('pdf-canvas'); const ctx = canvas.getContext('2d'); canvas.width = viewport.width; canvas.height = viewport.height; return page.render({ canvasContext: ctx, viewport: viewport }).promise; }).catch((err) => { console.error('PDF 渲染失败:', err); });getDocument返回的是一个PDFDocumentLoadingTask,核心成员就是promise和destroy()。page.render返回的渲染任务也有独立的promise和cancel()。这两个任务是后面做页面切换和组件卸载时要重点处理的,很多人就是在这里随便写写导致内存泄漏和渲染错乱。
3.2 scale 参数不是越高越清楚
page.getViewport({ scale })里的 scale 是渲染分辨率系数,默认 1.0 表示按 PDF 原始尺寸(72 DPI)输出。实际显示时,如果你直接设scale = 2,Canvas 里每个 PDF 点会对应 2 个物理像素,文字确实更锐利,但内存占用也变成原来的 4 倍。
更合理的做法是结合 CSS 显示宽度和设备像素比动态计算。比如页面里预览区宽度是 800px,PDF 页面原始宽度是 612pt(A4 横向),那基础 scale 应该是800 / 612 ≈ 1.31,再乘上window.devicePixelRatio(一般手机是 2 或 3)得到最终渲染 scale:
const baseScale = containerWidth / viewportAtScale1.width; const dpr = window.devicePixelRatio || 1; const scale = baseScale * dpr;注意 Canvas 的实际尺寸要按最终 scale 设置,但 CSS 尺寸保持逻辑宽度,这样渲染出来在 Retina 屏上才清晰。我在 uniapp 的 WebView 场景里就吃过这个亏,不乘devicePixelRatio时,PDF 文字在手机上看起来明显发虚。
4. 塞进 Vue / React / uniapp 的差异化处理
2.2.228 本身是一套跟框架无关的库,但不同框架对它的集成方式差异很大。尤其是 canvas 的 ref 管理、页面卸载时的清理逻辑、以及 uniapp 这种跨端环境里的 worker 加载,几乎每个项目都要单独调一遍。
4.1 Vue 组件里管理 canvas 生命周期
Vue 2/3 里集成 pdfjs-dist,核心注意点是:canvas 的 DOM 必须等mounted之后才能拿到,而 PDF 加载是异步的。组件销毁时如果还有未完成的loadingTask或renderTask,要主动调用destroy()和cancel(),否则页面跳转后浏览器会持续被渲染任务占用。
我习惯用下面这种结构:
export default { data() { return { loadingTask: null, renderTask: null, }; }, mounted() { this.renderPdf(this.pdfUrl); }, beforeDestroy() { if (this.renderTask) this.renderTask.cancel(); if (this.loadingTask) this.loadingTask.destroy(); }, methods: { async renderPdf(url) { this.loadingTask = pdfjsLib.getDocument(url); const pdf = await this.loadingTask.promise; // 后续 getPage + render } } }一个很容易忽略的细节:连续滚动翻页时,用户可能快速滑过好几页,上一次render还没结束,下一次就开始了。这时候如果不cancel上一次的renderTask,Canvas 上会出现“后一页先画完,前一页又把画布覆盖掉”的竞态问题。我自己的处理方式是在渲染前先检查当前渲染任务是否进行中,是则cancel(),再重新渲染目标页。
4.2 uniapp 里最常见的 web-view + dist 方案
热搜里有“uniapp 集成 pdfjs 预览”,这块要单独展开。uniapp 的 H5 端理论上可以直接 npm 安装pdfjs-dist@2.2.228然后import,但在 App 端(Android/iOS WebView)和各类小程序端,情况完全不同。
小程序没有浏览器 Canvas 的完整 API,不能用 pdfjs 直接渲染;App 端虽然 WebView 支持 Canvas,但如果你把 pdfjs 相关文件放到hybrid/html下用web-view打开,走的是file://协议,这时候pdf.worker.js一般加载不出来,因为 Worker 不允许跨 scheme 启动。
我踩坑之后的结论是:App 端最稳妥的方式是起一个本地静态服务器,或者把 PDF 预览 HTML 挂到远程 URL 下再通过web-view加载。如果文档不想传公网,可以放到应用沙盒里然后让 WebView 访问http://localhost风格的本机服务,但这样原生开发工作量就上来了。很多团队的“妥协方案”其实是用官方 viewer.html 挂远程服务器,viewer.html?file=encodeURIComponent(远程PDF地址),简单但没法个性化定制 UI。
4.3 跨域、CDN 和 worker 地址问题
用 CDN 分发 pdfjs 资源的时候,GlobalWorkerOptions.workerSrc最好写完整的绝对地址,不要写相对路径。因为 Worker 脚本的加载规则会受当前页面 URL 影响,相对路径在带有 hash 路由的单页应用里经常解析错。
另外,通过getDocument({ url })加载远程 PDF 时,目标服务器必须返回正确的 CORS 响应头。否则在页面上直接请求会失败,而官方 viewer.html 也提供file参数直接加载,本质一样受跨域限制。如果 PDF 跟你的预览页同源,就没有这个问题;如果跨域,后端需要允许Access-Control-Allow-Origin。
5. 生产环境最常见的 6 个坑和绕坑思路
这些坑不是看文档能发现的,都是我在真实项目里被线上反馈砸过之后才总结出来。如果你也在用 2.2.228,建议直接对照排查。
5.1 Canvas 最大尺寸导致的空白页
部分 Android WebView 和低配浏览器对 Canvas 最大边长有限制,不同设备上限从 4096 到 8192 不等。当 PDF 页面很大(比如 A0 图纸)或者 scale 乘完之后超过限制,Canvas 可能画出空白,也可能直接抛错。
我的绕坑方案:渲染前计算最终 canvas 宽高,如果长边超过 4096,就把 scale 等比调小,保证“渲染完整”优先于“绝对清晰”。这类超长页面本来就适合用矢量缩放看,在 97% 的屏幕尺寸下 3000px 宽已经够了。
5.2 大文件内存只增不减
一个 50MB 的 PDF,连续翻页后内存占用能到几百 MB。原因一般是两个:一个是没有销毁不再使用的page对象,另一个是renderTask.cancel()没有调用。PDF.js 的page对象持有不少解析后的资源,翻页时把上一页page的引用置空,并主动cancel掉上一轮的渲染任务,内存增长会明显改善。
如果确实要长时间驻留,建议加一个“超过 N 页自动释放前几页”的策略,只保留当前页和前后各一页的page对象引用,其他页在需要时重新getPage。
5.3 CMap 和字体:中文 PDF 乱码的罪魁祸首
中文、日文、韩文 PDF 乱码,十有八九是cMapUrl没配置。用官方 viewer.html 时它自动指向cmaps/,自己写渲染时就容易漏。
配置方式:
const loadingTask = pdfjsLib.getDocument({ url: pdfUrl, cMapUrl: '/lib/pdfjs-dist/cmaps/', cMapPacked: true, });cMapPacked: true表示使用压缩后的.bcmap文件,如果服务端没放 cmaps 目录会直接加载失败,所以这个目录务必跟着 dist 一起部署,不要手贱只拷贝 build 文件夹。
5.4 多个 PDF 实例互相干扰
如果一个页面里有多个 PDF 预览区域(比如对比工具),或者同一个 canvas 被复用去学习加载不同文档,要把loadingTask和renderTask按实例隔离。我见过最典型的问题:第一次渲染的异步任务还没完成,第二次getDocument就开始跑,结果两个渲染任务同时往同一个 canvas 上画,最终显示的是后完成的那个,逻辑完全错乱。
解决方案就是前面提到的竞态控制:每次渲染前取消上一次未完成的任务,或者用一个自增 token,只有当前 token 的渲染结果才允许写入 canvas。
5.5 “看起来跟 pdfjs 无关”的安装报错
热搜词里那个“npm run start cannot find module ajv dist compile codegen”我看着特别眼熟。它不是 pdfjs 的问题,但往往出现在你npm install pdfjs-dist@2.2.228之后的启动阶段,根源是依赖树里ajv版本不匹配,webpack 或某个插件在运行时找不到ajv/dist/compile/codegen。常规处理是把node_modules清掉重新安装,并且固定依赖版本,不要装最新版去碰运气。
类似的还有“could not retrieve https://nodejs.org/dist/latest/shasums256.txt”这种,属于某些原生依赖在安装时要下载 Node 头文件但网络环境拿不到。这类报错基本都不是项目代码写错,而是构建环境不一致。排查时先确认 Node 版本和依赖要求的版本对得上,再考虑是不是需要手动指定依赖版本以跳过自动下载。
5.6 清理 PDFJS 的全局状态
最后一条,也是很多老项目忽视的:pdfjsLib的全局状态一旦被某个模块改了,会影响到后续所有页面。典型操作是某个页面里重新赋值了GlobalWorkerOptions.workerSrc,或者全局改了PDFJS.verbosity,导致另一个页面里的 PDF 预览静默失效。
我的习惯是封装一个独立的pdfService,集中管理pdfjsLib的初始化和配置,业务代码不直接 importpdfjs-dist。这样既保证 workerSrc 只配置一次,也方便未来统一升级版本。
另外,接手这类老项目时,建议第一件事就是在控制台打印pdfjsLib.version,确认实际跑的是不是你以为的版本。我遇到过 2.2.228 的页面里混入了一个 2.0.550 的 worker,看起来都是 2.x,但渲染行为差异非常明显。先确认版本,再定位问题,能省下大量排查时间。
本文还有配套的精品资源,点击获取