
简介pdf.js是一款由Mozilla推出的开源PDF渲染引擎能够在网页中直接嵌入并展示PDF文档无需安装插件或借助第三方阅读器。它面向Web前端开发者、文档站点维护者以及需要在线预览PDF的企业系统可解决传统浏览器无法原生解析PDF、需反复下载文件等问题。该资源以ZIP压缩包形式分享整体大小约2.47MB上传信息中未列明具体的文件数量与类型明细实际使用时可直接根据项目需求选用。已有1342人浏览学习适合在开发在线阅读、电子合同、在线报告等场景中集成使用。通过这套框架开发者可快速实现PDF的翻页、缩放、文本选择与打印等常见交互有效提升网页端的文档阅读体验。1. 先搞清楚你要找的是库还是下载功能很多人在搜“pdf.js 下载”的时候需求其实是两岔的一种是“我想用 PDF.js 这个开源渲染库但不知道去哪下、下哪个版本”另一种是“我已经把 PDF.js 集成进去了就想在页面上加一个下载按钮把当前 PDF 保存到本地”。这两种需求我都遇到过而且踩的坑完全不一样。PDF.js 本身只负责解析和渲染 PDF它不提供现成的下载按钮也不会帮你把渲染好的页面变成文件。所以这里我打算一条龙讲清楚怎么正确获取和部署 PDF.js怎么渲染出第一页怎么实现真正能用的下载动作以及上线前容易翻车的那些玄学问题。适合正在做预览、打印、附件导出功能的前端开发也适合内网项目里需要离线部署 PDF 阅读器的同学。2. 获取PDF.js的三种方式CDN、包管理器和Zip包版本怎么选PDF.js 的发布物其实是一组配套文件主库文件负责暴露 APIworker 文件负责在后台线程解析 PDF两个文件必须版本一致。下载这个库不是“下完一个 JS 就完事”而是要先理解这套文件组合。不然很容易遇到“主库有、worker 缺”的诡异问题。我平时按使用场景分三种方式获取。2.1 方式一公共CDN引脚本适合快速验证如果你只是想写一个本地 Demo或者做一个纯浏览器端的原型最快的方式是直接用公共 CDN 引脚本。注意需要同时引入主库和 Worker 文件因为 PDF.js 的解析工作默认放在 Web Worker 里跑否则会退回到主线程页面交互会卡。!-- 以远程方式加载 PDF.js 为例地址换成你自己可访问的源 -- script srchttps://example.com/pdfjs-dist/3.x.y/pdf.min.js/script script // 设置 worker 文件路径版本必须和上方主库一致 pdfjsLib.GlobalWorkerOptions.workerSrc https://example.com/pdfjs-dist/3.x.y/pdf.worker.min.js; /script这里的pdfjsLib.GlobalWorkerOptions.workerSrc是全局配置它告诉库到哪里找 worker 脚本。代码里的3.x.y是一个占位版本号实际部署时替换成你安装的具体版本主库和 worker 的路径要完全相同不能一个用 3.x 一个用 2.x否则控制台会直接报版本不匹配。workerSrc支持相对路径、绝对路径和跨域地址。如果放在内网项目里建议用绝对路径尤其当页面路由是 history 模式时相对路径容易算到错误位置。CDN 方式适合原型验证但不建议直接用于生产一方面公共 CDN 的可用性不受你控制另一方面离线内网环境根本用不了。还有一点CDN 方式引入时会占用全局命名空间如果你的项目本身依赖很多全局变量容易冲突。快速验证时不用太纠结但一旦要往正式项目里搬还是建议切换到模块化引入。2.2 方式二包管理器安装适合工程化项目在正式的前端工程里我一般用包管理器安装。这样版本会锁在依赖描述文件里构建时也能把依赖一起打包。命令很简单npm install pdfjs-dist然后在你自己的模块里引入import * as pdfjsLib from pdfjs-dist; // 用构建工具解析 worker 路径 pdfjsLib.GlobalWorkerOptions.workerSrc new URL( pdfjs-dist/build/pdf.worker.min.js, import.meta.url ).toString();上面这个new URL的写法是构建工具例如支持import.meta.url的那一类的常见做法里最常见的做法能让你不用手工维护 worker 路径。如果你的构建环境不支持import.meta.url也可以把依赖目录里的pdf.worker.min.js复制到项目的静态资源目录然后写成简单的/pdf.worker.min.js。这里要注意一个混用问题CDN 方式暴露的是全局变量window.pdfjsLib而模块方式返回的是模块对象两者 API 相同但全局变量名只在 script 标签场景下有效。如果你一边用模块引入主库一边又在 HTML 里写window.pdfjsLib很容易出现“pdfjsLib 未定义”的报错。2.3 方式三下载Zip包自己托管适合内网部署内网项目最稳妥的办法是手动下载发布包然后把关键文件放在自己的静态目录里。你需要的基本文件有主库pdf.min.js、worker 文件pdf.worker.min.js如果页面要显示中文、日文或其他非拉丁文字的 PDF还需要cmaps目录。有些发布包会带许可证文件部署时一起保留就行。# 以 Linux/macOS 下的命令为例手动把文件放到静态目录 mkdir -p /srv/www/static/pdfjs cp pdf.min.js /srv/www/static/pdfjs/ cp pdf.worker.min.js /srv/www/static/pdfjs/ cp -r cmaps /srv/www/static/pdfjs/cmaps目录是 PDF.js 用来查找字体编码映射的补充数据。如果你的站点会打开中文合同、日文文档或某些特殊编码的 PDF缺少这个目录常常会出现文字位置错乱、显示成问号。如果确实不需要非拉丁字体可以暂时不部署但后续一旦遇到乱码问题第一反应就应该是把cmaps补上。内网部署还要注意一点不要从公共 CDN 复制链接到内网而是把文件下载下来再上传到内网服务器。原因很明显内网通常无法访问公网 CDN就算能访问每次页面加载都跨公网拉资源性能和稳定性都不靠谱。手动托管的另一个好处是你能完全控制静态资源的加载顺序和缓存策略比如给这些文件设置长缓存时间提高二次打开速度。2.4 版本选型主线版本、旧版兼容和别追最新版本选择上我吃过一次亏项目原本用 2.x 版本后来手滑升了 4.x结果原来正常的打印功能全部失效原因是 4.x 改了一部分内部 API。所以我的建议是生产环境不要追最新选一个已经发布超过半年的稳定版本锁死。没有安全或性能上的硬需求不要做跨大版本升级。版本区间适用场景注意点2.x老项目、依赖旧 API 的代码worker 路径默认在 build 目录升级要改全局配置3.x新项目起步API 相对稳定文本选择和缩放质量较好4.x需要新特性、长期维护内部 API 有变更迁移必须回归测试这里的 2.x、3.x、4.x 只是描述大版本风格不是具体推荐。我一般会去官方发布列表里看最近半年的更新频率和 issue 反馈选一个没有显著 bug 的版本。确定后主库和 worker 版本必须完全一致每次构建后都要核对产物里这两个文件是不是同一版本。3. 把PDF画到页面上最小渲染命令与三个必调参数拿到库之后第一件事是渲染一个 PDF 页面到 canvas。这一步是后面所有功能的地基下载、打印、搜索都依赖它。三个必调参数分别是workerSrc、scale、viewport。下面拆开讲。3.1 初始化workerSrc 是第一个必须设置的参数import * as pdfjsLib from pdfjs-dist; // 1. 设置 worker pdfjsLib.GlobalWorkerOptions.workerSrc /static/pdfjs/pdf.worker.min.js; // 2. 用 URL 或二进制数据加载 PDF const loadingTask pdfjsLib.getDocument({ url: /static/samples/sample.pdf, // 也可以直接传 data: Uint8Array适合从后端接口拿二进制 });getDocument返回的是一个PDFDocumentLoadingTask对象真正的加载结果是它身上的promise。如果你发现不设置workerSrc也能跑通那是因为 PDF.js 会尝试加载“fake worker”也就是在同一个线程里模拟 worker。这种方式小文件看不出问题但稍微大一点的 PDF主线程会被解析任务堵住页面会明显卡顿甚至无响应。所以workerSrc在生产环境是必选项不是可选项。getDocument的入参可以传url、data、range等。传url时要注意同源策略跨域请求必须让服务端允许 CORS。传data时可以直接传ArrayBuffer或Uint8Array这也是实现“下载后立即预览”的常用方式。不建议传 base64 字符串因为解析前还需要先转二进制白费一次转换。3.2 加载文档并渲染第一页从 getDocument 到 canvasconst task pdfjsLib.getDocument({ url: /static/samples/sample.pdf, }); task.promise .then((pdf) { // 加载第一页 return pdf.getPage(1); }) .then((page) { // 按 1.5 倍清晰度渲染适合普通屏幕 const viewport page.getViewport({ scale: 1.5 }); const canvas document.getElementById(pdf-canvas); const ctx canvas.getContext(2d); // canvas 尺寸必须和 viewport 一致否则画面会模糊或拉伸 canvas.width viewport.width; canvas.height viewport.height; const renderContext { canvasContext: ctx, viewport: viewport, }; return page.render(renderContext).promise; }) .then(() { console.log(第一页渲染完成); }) .catch((err) { console.error(渲染失败, err); });页面对象page的getViewport方法接收一个包含scale的配置对象返回的viewport有width、height、transform等属性。canvas 的像素尺寸必须手动设置为 viewport 的宽高不能只写 CSS 尺寸否则渲染结果会被浏览器拉伸文字变糊。scale是渲染清晰度参数普通屏幕 1 到 1.5 足够高分屏建议乘上window.devicePixelRatio。renderContext里最关键的是canvasContext和viewport其中viewport必须是page.getViewport返回的对象不能自己手动构造一个类似结构的对象。page.render返回一个RenderTask它的promise在渲染完成时 resolve。如果你想在渲染中途取消必须保留这个RenderTask对象后面调用它的cancel()方法。3.3 翻页和多页渲染循环队列、cancel 和内存释放let currentPage 1; let pdfDoc null; let renderTask null; async function renderPage(pageNumber) { if (!pdfDoc) return; if (renderTask) { // 如果上一次渲染还没结束先取消 renderTask.cancel(); } const page await pdfDoc.getPage(pageNumber); 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; renderTask page.render({ canvasContext: ctx, viewport, }); await renderTask.promise; // 渲染完成后释放页面对象占用的内存 page.cleanup(); } // 加载后保存 pdfDoc供翻页使用 pdfjsLib.getDocument({ url: /static/samples/sample.pdf }).promise.then((pdf) { pdfDoc pdf; renderPage(1); });多页渲染时不要把所有页面一次性渲染成隐藏的 canvas 存起来内存会瞬间涨上去。正确做法是按需渲染页码切换时先cancel掉上一次的renderTask再渲染新的一页。否则快速点击上一页、下一页时多个渲染任务并发浏览器内存飙升部分任务还会报“Rendering cancelled”的错误。page.cleanup()的作用是让页面对象内部的缓存比如图像、字体进入可释放状态。它不会卸载已经绘制到 canvas 上的内容所以可以在渲染完成后放心调用。还有一个隐藏点如果渲染后的 canvas 不再需要展示要把它的width和height清零并移除引用否则浏览器不会立刻回收这块像素内存。4. 给页面加下载按钮三种保存方案和边界判断PDF.js 本身没有 download API所以“下载”得自己写。我总结三种方案按场景选有原始文件地址、手上有二进制数据、想要图片格式的导出结果。前两种保存的是 PDF 文件第三种保存的是图片用途完全不同。4.1 方案一浏览器原生下载简单但有跨域前提a href/static/samples/sample.pdf download项目文档.pdf下载 PDF/a这是最直接的下载方式给a标签加上download属性点击会触发下载而不是在浏览器里打开。前提有两个第一href必须是同源地址或允许跨域的地址第二下载必须是用户主动点击触发。如果链接指向第三方地址download属性基本无效浏览器会忽略你设置的文件名按响应头里的文件名来保存。这种方式不需要 PDF.js 参与但它确实是下载需求里最常见的实现适合 PDF 就部署在你自己静态目录里的场景比如合同、证书、报表。用这种方式时要确认服务端没有设置强制的Content-Disposition响应头否则它优先级更高会覆盖download属性。4.2 方案二fetch 拿 Blob 再下载绕开文件名和跨域拦截如果你希望文件名由前端决定同时避开服务端响应头的干扰可以用fetch把 PDF 内容拉成 Blob再用URL.createObjectURL生成临时链接触发下载。async function downloadPdf(url, filename) { // 1. 拉取二进制流 const response await fetch(url); if (!response.ok) { throw new Error(下载失败 response.status); } // 2. 转成 Blob注意 type 要写 application/pdf const blob await response.blob(); const objectUrl URL.createObjectURL(blob); // 3. 用临时 a 标签触发下载 const link document.createElement(a); link.href objectUrl; link.download filename || 下载.pdf; document.body.appendChild(link); link.click(); document.body.removeChild(link); // 4. 释放临时 URL URL.revokeObjectURL(objectUrl); }这个方案能解决大多数文件名不对、后缀丢失、点开是预览而不是下载的问题。fetch拿到的 Blob 是二进制原始数据只要响应头的Content-Type是application/pdfBlob 的type就是对的。link.download里一定要带.pdf后缀有些浏览器会自动补有些不会自己带上最稳。revokeObjectURL在link.click()后立即调用大多数现代浏览器是安全的。但如果你遇到过 0 字节文件就把它延后到事件循环尾部。另外即使方案二绕过了download属性限制fetch本身依然受 CORS 约束跨域请求需要服务端在响应头里允许当前站点访问。4.3 方案三把渲染好的当前页存成图片function downloadCurrentPageAsImage(canvas, filename) { // 将 canvas 转成 PNG 数据 const dataUrl canvas.toDataURL(image/png); const link document.createElement(a); link.href dataUrl; link.download filename || 当前页面.png; link.click(); }这个方案和 PDF.js 渲染结果绑定。比如你的页面已经渲染了某一页到 canvas用户想单独保存这一页直接调用canvas.toDataURL即可。优点是所见即所得页面上看到什么保存出来的图片就是什么。缺点是分辨率受 canvas 尺寸影响而 canvas 尺寸取决于渲染时的scale。如果scale不够大保存的图片放大后会有明显锯齿。toDataURL的常见格式有两种image/png和image/jpeg。PNG 适合文字和线条体积大JPEG 体积小但白色背景容易产生噪点。如果要透明背景才用 PNG。如果保存的是工程图纸或宽表格可以在渲染当前帧前把scale临时调高到 2 或 3渲染完成后截取图片这样可以输出高分辨率文件。注意跨域加载产生的 canvas 会被标记为“被污染”调用toDataURL会抛SecurityError所以导出图片前要保证 PDF 数据来源同源或允许跨域。4.4 三种方案怎么选一个判断建议我的判断习惯是如果 PDF 已经存在服务端并且能同源访问优先用方案一代码最少如果文件名需要前端严格控制或者环境里存在各种响应头、缓存干扰用方案二如果需求是把预览中的当前页保存为图片用方案三。特别提醒方案三保存的是图片不是 PDF别把它当成“导出 PDF 原件”来用。还有一类场景页面已经通过 PDF.js 加载了 PDF并且拿到的是一段二进制数据。这时下载不要再去fetch一次直接复用那一段数据用new Blob([data], { type: application/pdf })创建下载链接。这样省一次网络请求也能保证用户下载的就是当前预览的那份内容。这个技巧在离线内网环境尤其实用。5. 部署避坑PDF.js下载场景里最容易翻车的五个地方PDF.js 的集成难度不在 API而在部署环境和浏览器行为。下面五条都是我在实际项目里遇到过的“现象级”问题每条按现象、原因、解决来写。5.1 worker 加载失败fake worker 报错和路径玄学现象控制台出现类似 “Setting up fake worker failed” 或 “Failed to fetch dynamically imported module” 的报错功能时好时坏。原因workerSrc指向的路径错误或者 worker 文件没有被正确打包、复制。PDF.js 发现 worker 无法加载时会退回到 fake worker 模式解析任务全部跑在主线程大文件一多就卡。还有一种情况是部署时主库和 worker 文件版本不一致浏览器加载了旧缓存的 worker。解决打开网络面板确认请求里有没有pdf.worker.min.js并且状态码是 200。把workerSrc改成绝对路径或完整的 CDN 地址不要用动态拼接的相对路径。构建后去发布目录核实 worker 文件确实存在。如果构建工具给静态资源加了 hash 后缀而workerSrc里写的是固定文件名需要同步修改配置或者用 2.2 里的new URL方式让构建工具解析。我现在的习惯是每次部署后先看 Network 面板里 worker 请求是否成功这一步能省下很多排查时间。5.2 文件名错乱Content-Disposition 和 download 属性的关系现象用户点击下载保存的文件名不是自己设置的而是乱码或者变成链接里的最后一段名字。原因a标签的download属性只在同源或前端生成 Blob 链接时才绝对可靠。直接链接到服务端文件时服务端返回的Content-Disposition响应头优先级高于download属性。某些服务端框架会自动带filename*参数浏览器优先按响应头取名。解决不要和服务端抢文件名。要么让后端在响应头里正确设置文件名要么用方案二fetch拿 Blob由前端决定。选fetch方案时Blob 的type一定要是application/pdf否则部分浏览器会先下载成.bin或没有后缀。文件名里包含/、:这类字符会被系统过滤建议在前端统一替换成_。5.3 中文乱码变方块字体子集和 cMap 配置现象PDF 里中文变成方块或问号有些字能显示有些不能预览和下载后的表现还不一致。原因PDF 文档嵌入了字体子集PDF.js 解析时需要字体映射文件来还原字符。部署环境缺少cmaps目录或文档使用了 Type3 字体、非标准编码就会出现乱码。版本太旧的 PDF.js 对某些字体格式支持不全也会造成同样问题。解决先把cmaps目录部署好然后在getDocument里传入cMapUrl和cMapPackedpdfjsLib.getDocument({ url: /static/samples/sample.pdf, cMapUrl: /static/pdfjs/cmaps/, cMapPacked: true, });cMapPacked设为true表示加载压缩后的映射文件体积更小。加了 cMap 还乱码可以尝试同主版本内的最新补丁版本不要跨大版本。如果还是不行问题可能出在 PDF 文件本身需要服务端重新转一版这不是前端能解决的问题。5.4 大文件卡死渲染节奏和 canvas 清理现象打开超过二三十页的 PDF内存占用一路涨翻页越来越慢偶尔白屏或直接崩溃。原因一次性把所有页面渲染完并保存 canvas或者渲染任务没有取消导致多个渲染同时进行。另一个常见原因是page.cleanup()没调用页面对象内部的字体、图像缓存一直不释放叠加起来内存爆炸。解决改成按需渲染用IntersectionObserver或页码切换触发一次只渲染当前可见页。渲染新页前先取消旧任务并清理旧页面。canvas 不再显示时把宽高设为 0。大文件还可以在getDocument里设置disableAutoFetch: true让 PDF.js 不预取整份文件按需读取数据。这个参数会降低加载速度但对大文件内存帮助明显。scale不要超过 2高分屏控制在设备像素比的 1 倍左右否则就是纯像素爆炸。如果页面使用了前端路由离开页面时也要把pdfDoc引用置为 null让垃圾回收机制处理。5.5 下载文件只有 0 字节Blob 生命周期和 fetch 失败现象点击下载后文件很快就保存了但打开一看大小是 0 字节或者下载到一半中断控制台偶发 fetch 相关报错。原因方案二里revokeObjectURL在click之后立刻执行浏览器还没来得及开始下载临时 URL 就失效了下载动作拿到一个空流。另一种情况是fetch请求命中了缓存返回 304 但 body 为空或者跨域响应被拦截。解决先看网络面板如果是跨域请求失败会有 CORS 报错。没有报错但文件是 0 字节把revokeObjectURL延后到事件循环尾部比如setTimeout(() URL.revokeObjectURL(objectUrl), 100)。大多数情况下也可以不主动 revoke依赖页面关闭时浏览器回收。每次下载要生成新的objectUrl不要复用旧的。如果从 PDF.js 的内部数据转 Blob建议先复制成新的Uint8Array避免原数据被内部引用修改导致文件内容异常。6. 生产验证下载功能上线前检查什么6.1 验证清单我把下载功能上线前要检查的点整理成一张表每次发版前对着过一遍能避免大部分“本地好好的上线就废”的问题。检查项通过标准常见问题worker 加载network 面板只有一个 worker 请求状态 200404、缓存旧版本、跨域报错预览清晰度文字边缘平滑无模糊拉伸canvas 尺寸未匹配 viewport下载文件名保存的文件名符合预期并带 .pdfContent-Disposition 或 download 失效下载内容完整性下载后能正常打开且页数一致Blob 截断、revoke 过早、fetch 失败连续翻页10次内存无明显持续上涨无卡死未调用 cleanup、渲染任务叠加中文/特殊字体页面无方块乱码位置正确cMap 缺失、字体子集不全验证时不要只在开发者环境点一下要分别用内网地址、公网地址各试一次并且用无痕窗口关闭缓存测试。特别要注意有些浏览器会先弹下载栏如果你的代码在click后又立刻释放 Blob弹下载栏那一下刚好会拿到空流。这些都要在真实浏览器矩阵里走一遍。6.2 两个小技巧统一文件名和释放内存我习惯在项目里封装一个统一的下载工具函数所有 PDF 下载都走同一个入口文件名规则、错误提示、日志都归口管理。另一个习惯是在翻页组件销毁时手动调用渲染任务的cancel方法并把 canvas 的尺寸清零。这个动作不复杂但能避免单页应用里路由切换后内存不返还。还有一个容易忽略的点如果页面里已经用 PDF.js 加载了同一个文件下载时不要再去fetch一次直接复用已经拿到的二进制数据。做法是把加载时传入的data保存起来下载时用new Blob([data])转换。只适用于data方式加载的场景但能省一次请求也能避免用户看到两份数据不一致。最后说说我自己的教训有一阵子下载文件总是 0 字节查了两天才发现是revokeObjectURL时机太早当时非要同步释放。后来学乖了所有 Blob 下载统一用延时释放或触发后释放。PDF.js 这个方向不难但每个细节都可能让你加班到深夜。希望帮到你。本文还有配套的精品资源点击获取