简介这是一份基于Mozilla开源项目PDF.js的实战Demo面向需要在网页中嵌入PDF阅读能力的Web前端开发者与学习者用纯JavaScript解决浏览器无插件预览、缩放与打印PDF的问题。压缩包共376个文件约2.49MB其中168个bcmap字符映射文件用于中文、日文等多语言编码解析105个properties与72个png、9个svg构成界面与本地化资源6个js、3个html及1个css承载核心渲染与交互逻辑另附示例PDF与说明文档。已有9927人学习下载热度较高。Demo完整演示了通过PDFDocumentProxy加载文档、遍历页码渲染到Canvas、动态调整scale缩放比例、借助File API打开本地文件以及调用window.print()实现打印的完整链路并给出Web Worker后台解析、分页加载等性能优化思路适合直接参考或二次扩展注释、搜索、书签等功能。1. 纯前端 PDF 预览与打印为什么我最终选了 pdf.js 这个 Demo去年接手一个内部文档管理系统时产品提的需求很直接上传 PDF 后要能在浏览器里直接看还得支持打印不能依赖任何后端转换服务。我第一反应是iframe或embed直接塞进去结果在移动端和部分国产浏览器上翻车得厉害——要么白屏要么打印出来缺页。后来换成 pdf.js 这个纯 JS 方案才算把预览和打印两条链路都跑通。pdf.js 是 Mozilla 主导的开源库核心能力是把 PDF 解析成 Canvas 或 SVG 渲染完全不依赖浏览器原生 PDF 插件。这个 Demo 的价值在于它把「加载、翻页、缩放、打印」四个最常用的动作封装成了可直接复用的最小闭环适合需要快速集成 PDF 预览能力的前端开发者也适合想理解 PDF 渲染原理的工程师拿来拆解。如果你正在被浏览器兼容性和打印样式折磨这个方向值得花时间看下去。2. 拆解 pdf.js 的渲染管线从 PDF 二进制到 Canvas 像素2.1 核心对象与数据流pdf.js 的工作方式不是「把 PDF 当图片显示」而是真正解析 PDF 文件结构。整个链路可以拆成四层getDocument负责加载和解析 PDF 二进制流产出PDFDocumentProxygetPage从文档代理里取出单页得到PDFPageProxygetViewport根据缩放比例和旋转角度计算视口尺寸最后render把页面内容绘制到 Canvas 的 2D 上下文里。这四步是串行的任何一步失败都会导致白屏。我一般会先确认 PDF 文件本身是否加密或损坏因为 pdf.js 对加密文档的处理比较保守密码错误时不会抛明显异常而是卡在getDocument的 Promise 里。另一个容易忽略的点是worker线程pdf.js 默认会启动一个 Web Worker 来解析 PDF避免阻塞主线程。如果部署环境不允许加载 worker 文件就需要手动设置disableWorker: true但代价是解析大文件时页面会卡顿。2.2 最小可运行示例下面这段代码是我从 Demo 里抽出来的核心逻辑去掉了 UI 装饰只保留加载和渲染// 引入 pdf.js 主库和 worker import * as pdfjsLib from ./pdfjs/build/pdf.mjs; pdfjsLib.GlobalWorkerOptions.workerSrc ./pdfjs/build/pdf.worker.mjs; const loadingTask pdfjsLib.getDocument({ url: /docs/sample.pdf, cMapUrl: ./pdfjs/cmaps/, // 中文等 CJK 字符映射 cMapPacked: true, // 使用压缩的 cmap 文件 disableAutoFetch: false, // 允许分段加载大文件 }); loadingTask.promise.then(async (pdf) { const page await pdf.getPage(1); // 取第一页 const viewport page.getViewport({ scale: 1.5 }); // 1.5 倍缩放 const canvas document.getElementById(pdf-canvas); const context canvas.getContext(2d); canvas.width viewport.width; canvas.height viewport.height; const renderContext { canvasContext: context, viewport: viewport, }; await page.render(renderContext).promise; // 等待渲染完成 console.log(第 1 页渲染完成尺寸, viewport.width, x, viewport.height); }).catch((err) { console.error(PDF 加载失败, err.message); });这段代码里几个参数值得展开说。cMapUrl和cMapPacked是中文 PDF 预览的关键如果不配中文会显示成空白或乱码这是血泪经验。scale控制渲染分辨率1.5 倍在普通屏幕上够清晰但打印场景建议提到 2 倍以上否则打印出来边缘发虚。disableAutoFetch设为 false 时pdf.js 会按需加载页面数据适合大文件如果 PDF 只有几页设成 true 反而减少请求数。2.3 多页渲染与内存控制单页渲染跑通后下一步就是多页。Demo 里用的是「滚动容器 懒加载」策略只渲染视口附近的页面滚动时动态创建和销毁 Canvas。这样做的好处是内存占用可控坏处是快速滚动时会出现短暂空白。const container document.getElementById(pdf-container); const pdf await pdfjsLib.getDocument(/docs/large.pdf).promise; const totalPages pdf.numPages; // 只渲染前 3 页作为初始视图 for (let i 1; i Math.min(3, totalPages); i) { const page await pdf.getPage(i); const viewport page.getViewport({ scale: 1.2 }); const canvas document.createElement(canvas); canvas.className pdf-page; canvas.width viewport.width; canvas.height viewport.height; container.appendChild(canvas); await page.render({ canvasContext: canvas.getContext(2d), viewport: viewport, }).promise; }这里有个坑每次getPage都会在内存里保留页面对象如果一次性渲染几百页浏览器标签页会直接崩掉。我一般会维护一个页面池滚动出视口的 Canvas 用canvas.width 0释放位图内存页面对象也置为 null让 GC 回收。3. 打印链路把 Canvas 渲染结果送进打印队列3.1 打印方案选型pdf.js 本身不提供打印 APIDemo 里用的是「Canvas 转图片 隐藏 iframe 打印」的方案。具体做法是把需要打印的页面渲染到离屏 Canvas转成 dataURL插入一个隐藏的 iframe然后调用iframe.contentWindow.print()。这个方案兼容性最好Chrome、Firefox、Edge 以及主流国产浏览器都能跑通。另一种方案是直接用window.print()打印当前页面但会把工具栏、按钮都打进去需要额外写media print样式隐藏。我倾向于 iframe 方案因为打印内容完全隔离不会污染主页面样式。3.2 打印代码实现async function printPdf(pdf, pageNumbers) { const iframe document.createElement(iframe); iframe.style.position fixed; iframe.style.right 0; iframe.style.bottom 0; iframe.style.width 0; iframe.style.height 0; iframe.style.border 0; document.body.appendChild(iframe); const doc iframe.contentWindow.document; doc.open(); doc.write(htmlheadtitle打印/title/headbody/body/html); doc.close(); for (const num of pageNumbers) { const page await pdf.getPage(num); const viewport page.getViewport({ scale: 2.0 }); // 打印用 2 倍分辨率 const canvas document.createElement(canvas); canvas.width viewport.width; canvas.height viewport.height; await page.render({ canvasContext: canvas.getContext(2d), viewport: viewport, }).promise; const img doc.createElement(img); img.src canvas.toDataURL(image/png); img.style.width 100%; img.style.pageBreakAfter always; // 每页后分页 doc.body.appendChild(img); } // 等待图片加载完成再打印 const images doc.querySelectorAll(img); await Promise.all(Array.from(images).map((img) { return new Promise((resolve) { if (img.complete) resolve(); else img.onload resolve; }); })); iframe.contentWindow.focus(); iframe.contentWindow.print(); // 打印后清理 iframe setTimeout(() document.body.removeChild(iframe), 1000); }这段代码里scale: 2.0是打印清晰度的关键1 倍渲染打印出来会有明显锯齿。pageBreakAfter: always保证每页图片单独分页否则多页会挤在一张纸上。toDataURL(image/png)比 JPEG 更适合文字型 PDF不会出现压缩噪点但文件体积会大一些。最后那个setTimeout清理 iframe 是必须的否则多次打印会残留一堆隐藏 iframe内存泄漏。3.3 打印样式与分页控制iframe 里的打印样式需要单独写因为主页面样式不会继承进去。我一般会在doc.write的 HTML 里内联一段 CSSpage { size: A4; margin: 10mm; } body { margin: 0; padding: 0; } img { display: block; width: 100%; height: auto; page-break-after: always; } img:last-child { page-break-after: auto; }page里的size: A4和margin控制纸张尺寸和边距不同打印机驱动对page的支持程度不一样但主流浏览器都认。如果打印出来内容被截断优先检查margin是否设得太小以及图片宽度是否超出纸张可打印区域。4. 避坑与排查那些让我加班到凌晨的细节4.1 中文乱码或空白现象PDF 里的中文在 Canvas 上显示为空白方块或乱码。原因pdf.js 默认不加载 CJK 字符映射表需要手动指定cMapUrl。解决下载 pdf.js 发布包里的cmaps目录放到静态资源路径下在getDocument时传入cMapUrl和cMapPacked: true。注意路径结尾要带斜杠否则会 404。4.2 跨域加载失败现象getDocument报PDF.js vX.X.X (build: XXXXX) Message: Unexpected server response (403)或 CORS 错误。原因PDF 文件放在不同域名下或者服务器没配Access-Control-Allow-Origin。解决要么把 PDF 放到同域下要么让后端加 CORS 头。如果无法改后端可以用fetch先拿到 ArrayBuffer再传给getDocument({ data: arrayBuffer })绕过 pdf.js 内部的 XHR 请求。4.3 打印出来是空白页现象点击打印后打印预览里全是白纸。原因iframe 里的图片还没加载完就调用了print()或者 Canvas 渲染是异步的图片 dataURL 还没生成。解决一定要等所有img.onload完成后再调print()。我一般会加一个Promise.all等待所有图片加载再延迟 200ms 调打印给浏览器留出渲染时间。4.4 大文件加载卡死现象打开 100MB 以上的 PDF 时页面直接无响应。原因pdf.js 默认会一次性解析整个文档结构主线程被阻塞。解决确保 worker 正常加载不要设disableWorker: true。同时开启disableAutoFetch: false和disableStream: false让 pdf.js 分段拉取数据。如果还是卡考虑在后端做分片前端只加载当前页范围。4.5 移动端手势缩放冲突现象在手机上双指缩放时页面跟着缩放而不是 PDF 内容缩放。原因pdf.js 的 Canvas 不响应手势浏览器默认缩放整个视口。解决在容器上监听touchmove事件用e.preventDefault()阻止默认缩放然后手动调整scale参数重新渲染。这个方案性能开销较大移动端建议限制最大缩放倍数。5. 进阶技巧用 viewport 做精准缩放与旋转5.1 动态缩放与旋转的实现pdf.js 的getViewport支持scale和rotation两个参数旋转角度必须是 90 的倍数。我一般会维护一个状态对象记录当前缩放比例和旋转角度每次操作后重新计算 viewport 并重绘。let currentScale 1.0; let currentRotation 0; function updateViewport(page) { const viewport page.getViewport({ scale: currentScale, rotation: currentRotation, }); const canvas document.getElementById(pdf-canvas); canvas.width viewport.width; canvas.height viewport.height; return viewport; } // 放大按钮 document.getElementById(zoom-in).addEventListener(click, async () { currentScale Math.min(currentScale 0.25, 3.0); // 最大 3 倍 const page await pdf.getPage(currentPageNum); const viewport updateViewport(page); await page.render({ canvasContext: canvas.getContext(2d), viewport: viewport, }).promise; }); // 旋转按钮 document.getElementById(rotate).addEventListener(click, async () { currentRotation (currentRotation 90) % 360; const page await pdf.getPage(currentPageNum); const viewport updateViewport(page); await page.render({ canvasContext: canvas.getContext(2d), viewport: viewport, }).promise; });这里有个细节每次缩放或旋转都要重新调getPage因为PDFPageProxy对象会缓存上一次的 viewport。如果直接复用旧 page 对象渲染结果可能不更新。我一般会在状态变更后重新获取 page虽然多一次异步调用但能保证渲染正确。5.2 打印指定页码范围Demo 里默认打印全部页面但实际业务中经常需要「打印第 3 到第 7 页」。实现思路很简单把页码范围解析成数组传给打印函数即可。function parsePageRange(input, totalPages) { // 支持 1-5,8,10-12 格式 const pages new Set(); input.split(,).forEach((part) { const range part.trim().split(-).map(Number); if (range.length 1) { pages.add(range[0]); } else { for (let i range[0]; i range[1]; i) { pages.add(i); } } }); return Array.from(pages) .filter((p) p 1 p totalPages) .sort((a, b) a - b); } // 使用示例 const pagesToPrint parsePageRange(1-3,5,7-9, pdf.numPages); printPdf(pdf, pagesToPrint);这个解析函数处理了逗号分隔和连字符范围最后过滤掉越界页码并排序。注意Set会自动去重避免重复打印同一页。5.3 验证打印效果的方法打印效果没法靠肉眼在屏幕上判断我一般用「打印到 PDF」功能做验证在打印对话框里选择「另存为 PDF」生成一个文件后打开检查页数、边距和清晰度。如果发现文字模糊把scale从 2.0 提到 3.0 再试如果分页不对检查page-break-after是否生效。另一个技巧是在 Chrome DevTools 的「渲染」面板里模拟打印媒体类型实时看样式变化。从那以后我每次集成 pdf.js都会先把cMapUrl、worker 路径和打印分辨率这三个参数写进配置检查清单少一个都可能让预览或打印翻车。希望帮到你。本文还有配套的精品资源点击获取