
简介pdf.js 1.9.426 是一份纯 JavaScript 实现的免费开源 PDF 阅读组件面向需要在网页中嵌入 PDF 预览功能的开发者尤其适合不想让用户额外安装插件的 Web 项目。它支持缩放、打印、查找等常用操作兼容火狐、谷歌及 IE9 以上主流浏览器但基于 H5 技术IE8 及更低版本无法使用且文件体积偏大会对加载速度有一定影响。压缩包共 372 个文件约 5.54MB其中 168 个 bcmap 字符映射文件用于多语言编码支持105 个 properties 为本地化语言配置另有 74 个 png、9 个 svg 及少量 js、css、html 等构成界面与核心逻辑目录结构完整。目前已有 989 人学习下载适合前端开发者直接集成或二次开发快速搭建可缩放、可检索的在线 PDF 阅读页面。1. 老版本 PDF.js 的离线包为什么还有人专门找如果你在维护一套内网文档系统或者要给一个不能连外网的终端做 PDF 预览你大概率会遇到一个很尴尬的局面新版 PDF.js 用 ES Module 打包构建链依赖 Node 版本、依赖一堆 polyfill扔进老项目里动不动就报Unexpected token export。这时候有人会翻出一个叫pdfjs1.9.426.rar的压缩包——它是 PDF.js 在 1.9 分支上的一个发行版本属于典型的 legacy 构建直接给浏览器用的pdf.jspdf.worker.js两个文件就能跑不依赖构建工具。这个标题背后真正要解决的问题不是「下载一个压缩包」而是「怎么把一份老版本 PDF.js 稳定地集成进一个不能再升级前端栈的系统里并且把预览、分页、缩放、文本层这些基础能力跑通」。适合的读者是维护 jQuery 时代后台的工程师、做嵌入式 Web 界面的开发者、需要离线部署文档预览的运维同学。下面我按「先搞清楚它是什么 → 怎么落地 → 坑在哪 → 怎么调优」的顺序讲全部基于 1.9.x 这条线的通用行为不假设你手上那份包的具体目录结构。2. 先搞清 1.9.426 这条线的能力边界与选型理由2.1 为什么是 legacy 构建而不是现代构建PDF.js 从 2.x 开始逐步转向 ES6 和模块化到 3.x、4.x 基本以pdf.mjs为主。1.9.x 属于更早的稳定分支产物是 UMD 风格的普通脚本挂一个全局pdfjsLib早期版本是PDFJS。这意味着你可以用一行script引入不需要 webpack、vite、rollup 任何一样东西。选它的核心理由有三个第一宿主环境老浏览器可能是某个定制内核不支持import第二构建链冻结团队不允许为了一个预览功能引入 npm 依赖树第三离线要求所有资源必须本地化不能走 CDN。这三点只要中一条legacy 构建就是合理选择。反过来说如果你的项目本身是 Vite React 18那没必要碰 1.9直接用新版pdfjs-dist更省事。2.2 1.9.x 能做什么、不能做什么能做的单页/连续滚动渲染、缩放、旋转、文本层选择与复制、基础搜索、页面跳转、打印。这些在 1.9 时代已经成熟。不能做或做得差的注释编辑、表单填写部分支持但坑多、复杂 CJK 字体的嵌入渲染依赖 cmaps 目录、高 DPI 屏幕下的清晰度需要手动处理devicePixelRatio。另外 1.9 的 worker 通信协议和后续版本不兼容你不能把 1.9 的主线程文件和 3.x 的 worker 混用这是最常见的翻车点。2.3 一份可用的离线目录应该长什么样不管压缩包内部怎么组织落地到项目里我一般整理成这样的结构方便后续替换和排查pdfjs/ build/ pdf.js pdf.worker.js web/ viewer.html # 可选用官方 viewer 时才有 viewer.js viewer.css cmaps/ # 中文等 CJK 字体映射必须整目录拷贝 standard_fonts/ # 标准字体兜底 images/ # viewer 用到的图标关键点是cmaps和standard_fonts这两个目录。很多人只拷了build下的两个 js结果中文 PDF 打开是空白或者方块就是缺 cmaps。这两个目录必须和主文件一起离线部署路径通过cMapUrl和standardFontDataUrl指定。2.4 版本号 1.9.426 的含义PDF.js 的版本号大致对应「主版本.次版本.构建号」426 是这条线上的一个构建序号。不同构建号之间 API 基本一致差异多在 bug 修复和少量渲染细节。所以你在网上找到 1.9.426 或 1.9.426 附近的包集成方式是一样的不用纠结必须精确到某个构建号。真正要盯的是「1.9」这个大版本跨大版本才需要改代码。3. 把 PDF.js 1.9 集成进页面的最小可跑通步骤3.1 引入脚本并确认全局对象先写一个最小 HTML只做一件事确认脚本加载后全局对象存在。这一步能帮你排除 90% 的「路径写错」问题。!DOCTYPE html html head meta charsetutf-8 !-- 引入主文件注意路径按你实际部署目录调整 -- script src./pdfjs/build/pdf.js/script /head body canvas idcv/canvas script // 1.9.x 挂载的全局名可能是 pdfjsLib也可能是 PDFJS // 先打印出来确认不要凭记忆写 console.log(pdfjsLib:, typeof window.pdfjsLib); console.log(PDFJS:, typeof window.PDFJS); var lib window.pdfjsLib || window.PDFJS; if (!lib) { throw new Error(PDF.js 未加载检查 script 路径); } // 指定 worker 路径必须和 pdf.worker.js 实际位置一致 lib.GlobalWorkerOptions.workerSrc ./pdfjs/build/pdf.worker.js; /script /body /html逻辑说明第一步永远是确认全局对象名。1.9 这条线上不同小版本对全局名的处理有过变化有的挂PDFJS有的挂pdfjsLib用||兜底最稳。workerSrc必须显式设置否则 PDF.js 会尝试用默认路径去找 worker离线环境下必然 404然后退化成主线程解析大文件直接卡死页面。参数说明workerSrc指向的是pdf.worker.js的 URL可以是相对路径也可以是绝对路径但必须能被浏览器直接 GET 到。如果你把 worker 放在别的域还要处理跨域离线场景一般同域不用管。3.2 加载文档并渲染第一页确认全局对象后写加载和渲染逻辑。下面这段是能直接抄的最小实现var lib window.pdfjsLib || window.PDFJS; lib.GlobalWorkerOptions.workerSrc ./pdfjs/build/pdf.worker.js; // 用 URL 加载也可以用 { data: arrayBuffer } 加载内存中的文件 var loadingTask lib.getDocument({ url: ./sample.pdf, // CJK 支持路径指向 cmaps 目录结尾斜杠不能少 cMapUrl: ./pdfjs/cmaps/, cMapPacked: true, // 标准字体兜底目录 standardFontDataUrl: ./pdfjs/standard_fonts/ }); loadingTask.promise.then(function (pdf) { console.log(总页数:, pdf.numPages); return pdf.getPage(1); }).then(function (page) { var scale 1.5; // 缩放系数1.0 是原始尺寸 var viewport page.getViewport({ scale: scale }); var canvas document.getElementById(cv); var ctx canvas.getContext(2d); // 处理高分屏避免模糊 var dpr window.devicePixelRatio || 1; canvas.width viewport.width * dpr; canvas.height viewport.height * dpr; canvas.style.width viewport.width px; canvas.style.height viewport.height px; ctx.scale(dpr, dpr); var renderTask page.render({ canvasContext: ctx, viewport: viewport }); return renderTask.promise; }).then(function () { console.log(第一页渲染完成); }).catch(function (err) { // 把错误打全PDF.js 的报错信息通常很具体 console.error(渲染失败:, err err.message, err); });逻辑说明getDocument返回的是一个 loadingTask不是 promise 本身必须取.promise。getPage的页码从 1 开始不是 0。getViewport必须传对象参数{ scale }老版本有人写成getViewport(1.5)会报错。渲染前先按devicePixelRatio放大 canvas 的实际像素再用 CSS 缩回逻辑尺寸这是高分屏不糊的标准做法。参数说明scale控制清晰度和性能的平衡1.5 是常用值超过 3 在大文档上会明显吃内存。cMapPacked: true表示使用打包过的 cmaps体积更小配合cMapUrl使用。standardFontDataUrl在 1.9 后期版本才有早期版本可能不认这个参数不认就删掉不影响基本渲染。3.3 连续滚动与分页渲染的取舍单页渲染只能看一页实际项目要么做「上一页/下一页」按钮要么做连续滚动。连续滚动的做法是遍历所有页每页一个 canvas用 IntersectionObserver 懒渲染。1.9 时代 IntersectionObserver 兼容性一般很多项目用滚动监听 计算可视区域。我的建议是页数少于 50 页直接全部渲染简单可靠超过 50 页做懒加载只渲染可视区前后各两页。判断依据是内存每个 canvas 在 scale1.5 时大约占几 MB100 页全渲染轻松上 G。// 懒渲染核心思路只渲染进入视口的页 function renderPage(pdf, pageNum, container) { pdf.getPage(pageNum).then(function (page) { var viewport page.getViewport({ scale: 1.5 }); var canvas document.createElement(canvas); var dpr window.devicePixelRatio || 1; canvas.width viewport.width * dpr; canvas.height viewport.height * dpr; canvas.style.width viewport.width px; canvas.style.height viewport.height px; canvas.dataset.page pageNum; container.appendChild(canvas); var ctx canvas.getContext(2d); ctx.scale(dpr, dpr); page.render({ canvasContext: ctx, viewport: viewport }); }); }逻辑说明每页独立 canvas用>page.getTextContent().then(function (textContent) { var textLayerDiv document.createElement(div); textLayerDiv.className textLayer; // 定位到和 canvas 完全重合 textLayerDiv.style.position absolute; textLayerDiv.style.left 0; textLayerDiv.style.top 0; textLayerDiv.style.width viewport.width px; textLayerDiv.style.height viewport.height px; container.appendChild(textLayerDiv); // 1.9 用 renderTextLayer参数名和后续版本不同 lib.renderTextLayer({ textContent: textContent, container: textLayerDiv, viewport: viewport, textDivs: [] }); });逻辑说明文本层的每个 span 通过绝对定位对齐到 canvas 上对应文字的位置所以 viewport 必须和 canvas 用同一个。textDivs是输出参数渲染后里面是生成的 span 数组搜索高亮时要用到。CSS 上文本层要设pointer-events: auto让文字可选中同时颜色透明只保留选择高亮。参数说明renderTextLayer在 1.9 是挂在全局对象上的函数不是 page 的方法这点和 2.x 不同。如果你的版本报renderTextLayer is not a function说明这个构建把它挪到了别处查一下全局对象上的方法列表。5.2 搜索的实现思路搜索依赖文本层。拿到所有页的 textContent 后在字符串里找关键词记录页码和字符偏移然后滚动到对应页并高亮对应的 span。1.9 没有内置搜索 UI需要自己写。核心是把textContent.items里的str拼起来注意 item 之间有空格和换行拼接规则要和 PDF.js 内部一致否则偏移对不上。一个稳妥的做法是逐页搜索用户输入关键词后遍历pdf.numPages每页getTextContent后匹配命中就记录。页数多时加个进度提示别让用户以为卡死了。5.3 打印与导出打印最简单的做法是调用浏览器的window.print()但 canvas 打印出来是位图清晰度取决于渲染 scale。要清晰就把 scale 提到 2 或 3 再打印。另一种做法是用 PDF.js 的getData拿到原始字节直接给用户下载原文件打印交给系统 PDF 阅读器这样最保真。// 拿到原始 PDF 字节用于下载或二次处理 pdf.getData().then(function (uint8Array) { var blob new Blob([uint8Array], { type: application/pdf }); var url URL.createObjectURL(blob); // url 可以直接给 a 标签下载也可以给 iframe 预览 console.log(原始字节大小:, uint8Array.length); });逻辑说明getData返回的是原始 PDF 的 Uint8Array不做任何重新编码所以体积和原文件一致。用它做下载最合适。注意URL.createObjectURL创建的 url 用完要revokeObjectURL释放否则内存泄漏。5.4 一个我自己的习惯每次接手一个老版本 PDF.js 的集成我第一件事不是写业务代码而是先跑一个「最小验证页」一个 canvas、一个本地 PDF、把 worker、cmaps、字体三个路径全部确认 200。这三步过了后面 90% 的问题都不会出现。血泪经验是很多所谓的「渲染 bug」其实是资源路径问题跟 PDF.js 本身没关系。另外替换版本时永远成对替换主文件和 worker并且把 cmaps 一起更新别只换一个。这套流程跑顺了1.9 这条老线在内网环境里能稳稳用很多年。希望帮到你。本文还有配套的精品资源点击获取