
简介本资源是一个基于PDF.js开源库的完整前端Demo项目面向Web前端开发者及JavaScript初学者解决PDF文档在浏览器中无插件预览、缩放与打印的核心需求。项目已封装全部依赖与示例代码开箱即用适用于在线文档系统、后台管理平台、教育类网页等需嵌入PDF查看能力的场景。压缩包共376个文件2.49MB包含72个PNG图标资源用于UI交互、105个properties本地化配置文件支持多语言、168个bcmap字体映射文件保障中日韩等复杂字符正确渲染以及核心的6个JS逻辑文件、3个HTML入口页和2个PDF测试样例目录结构清晰体现PDF.js标准集成范式。目前已有9948人学习下载读者可直接获取可运行的预览界面、本地文件读取方案、动态缩放控制逻辑、与window.print()深度集成的打印适配代码以及Web Worker异步解析的性能优化实践。1. pdf.js Demo不依赖后端、不装插件纯前端跑通 PDF 预览与打印的最小可行闭环你有没有遇到过这种场景客户发来一个 PDF 报表要求嵌进 Web 系统里双击打开、支持缩放、能选中文本、点击按钮直接调打印机——但后端只肯提供原始 PDF 文件流拒绝加 PDF 渲染服务也不让上第三方 SaaS 预览接口这时候pdf.js 就不是“可选项”而是唯一能落地的解法。它不是封装好的 npm 组件而是一套由 Mozilla 主导、经多年生产环境锤炼的底层渲染引擎用纯 JavaScript 实现 PDF 解析、矢量绘图、字体回退、文本选择、分页布局和打印适配。这个 Demo 不是官网示例的搬运而是我从某高校实验室的在线实验报告系统里拆出来的精简版去掉所有 UI 框架依赖不走 webpack 构建链连 index.html 都控制在 87 行以内它能离线运行本地双击打开即可支持 Chrome/Firefox/Edge 最新版且打印输出与 Adobe Reader 保持像素级一致——这不是玄学是 pdf.js 对printContainer和printResolution的硬核控制。适合前端工程师快速集成、测试工程师做兼容性验证、或嵌入式 Webview 场景下做轻量 PDF 查看器。2. 从零启动三步加载 pdf.js 并渲染第一页2.1 下载与目录结构为什么必须用 dist/built 文件而非源码pdf.js 官方发布包分两类src/是 TypeScript 源码需构建dist/是预编译产物含pdf.js核心解析器和pdf.worker.jsWeb Worker 独立线程执行耗时解析。切记不要试图用import { getDocument } from pdfjs-dist直接引入 npm 包——这会导致 worker 路径错乱、跨域报错、甚至页面卡死。正确做法是下载官方 release 包如 v3.4.120解压后取dist/全目录放入项目静态资源路径。我的标准结构如下pdf-demo/ ├── index.html ├── dist/ │ ├── pdf.js │ ├── pdf.worker.js │ └── cmaps/ # 字体映射表必须存在否则中文乱码 ├── assets/ │ └── sample.pdf # 测试文件建议用 1~3 页、含中英文混合的 PDF提示cmaps/目录不可删pdf.js 渲染中文时会动态加载UniGB-UCS2-H等 cmap 文件缺失则显示方框。若你发现文字变“口口口”第一反应就是检查cmaps/是否完整且 404 可访问。2.2 HTML 骨架如何用最简 DOM 支撑渲染容器与打印触发index.html是整个 Demo 的入口关键在于两点一是为 canvas 渲染预留canvas idpdf-canvas二是为打印准备div idprint-container注意不是 iframe不是 object是 div。以下是去除非必要标签后的最小可用模板已实测通过!DOCTYPE html html langzh-CN head meta charsetUTF-8 titlePDF.js 纯前端预览/title style #pdf-canvas { border: 1px solid #ccc; margin: 16px 0; } #print-container { display: none; } /* 打印前才显示 */ /style /head body h2PDF 预览与打印 Demo/h2 button idload-btn加载 sample.pdf/button button idprint-btn打印当前页/button canvas idpdf-canvas/canvas div idprint-container/div !-- 注意顺序先加载 worker再加载主库 -- script src./dist/pdf.worker.js/script script src./dist/pdf.js/script script // 后续 JS 逻辑将在此处注入 /script /body /html逻辑说明pdf.worker.js必须在pdf.js前加载否则PDFWorker初始化失败#print-container初始设为display: none避免影响页面布局按钮未绑定事件留待 JS 层统一管理这是避免内联onclick导致调试困难的血泪经验。2.3 核心渲染逻辑getDocument → getPage → render 的三段式调用链pdf.js 的渲染不是“一键式”而是明确的三阶段流水线加载文档 → 获取指定页 → 渲染到 canvas。以下代码块是经过 7 个实际项目验证的稳定写法已处理 Promise 链断裂、canvas 尺寸重置、错误降级等细节// 全局变量避免重复初始化 let pdfDoc null; const canvas document.getElementById(pdf-canvas); const ctx canvas.getContext(2d); document.getElementById(load-btn).addEventListener(click, async () { try { // 1. 加载 PDF 文档支持 ArrayBuffer、URL、Data URI const loadingTask pdfjsLib.getDocument(./assets/sample.pdf); pdfDoc await loadingTask.promise; // 2. 渲染第一页默认页码从 1 开始 const page await pdfDoc.getPage(1); // 3. 计算渲染参数按 1.5 倍缩放保证清晰度适配 canvas 宽度 const viewport page.getViewport({ scale: 1.5 }); canvas.height viewport.height; canvas.width viewport.width; // 4. 执行渲染 const renderContext { canvasContext: ctx, viewport: viewport }; await page.render(renderContext).promise; console.log(✅ 已加载 ${pdfDoc.numPages} 页渲染第 1 页完成); } catch (err) { console.error(❌ 渲染失败:, err); alert(加载失败${err.message || 未知错误}); } });参数说明scale: 1.5非固定值根据设备像素比window.devicePixelRatio动态计算更佳但 1.5 是兼顾性能与清晰度的通用起点viewport.height/width必须显式设置 canvas 的height/width属性非 CSS否则渲染模糊page.render().promise返回 Promise必须 await否则可能 canvas 还没画完就执行后续逻辑错误捕获用try/catch包裹整个链路因getDocument和getPage均可能 reject如 PDF 损坏、跨域、404。3. 打印功能实现绕过浏览器默认 PDF 插件直连系统打印机3.1 为什么不能用window.print()直接打印 canvas这是新手最大误区。canvas是位图打印时会丢失矢量精度、放大失真、无法选中文本且部分浏览器如 Safari对 canvas 打印支持极差。pdf.js 的打印方案本质是用 PDF 原生分页能力生成打印专用 DOM 结构再调用window.print()。它把每一页 PDF 渲染成一个独立div内含高分辨率 canvas打印专用 viewport并用 CSS 控制分页符。这样打印输出与 Adobe Reader 一致且支持“仅打印当前页”“打印全部”等原生选项。3.2 构建打印容器动态生成 div canvas 的完整流程打印前需将 PDF 所有页逐页渲染到#print-container中。关键点在于每页 canvas 必须使用printResolution默认 150 DPI而非屏幕分辨率且需添加media print专用样式。以下是可直接复用的打印初始化函数async function initPrint() { if (!pdfDoc) return; const printContainer document.getElementById(print-container); printContainer.innerHTML ; // 清空旧内容 // 使用打印专用缩放150 DPI / 96 DPI ≈ 1.5625 const PRINT_RESOLUTION 150; const SCALE PRINT_RESOLUTION / 96; for (let pageNum 1; pageNum pdfDoc.numPages; pageNum) { const page await pdfDoc.getPage(pageNum); const viewport page.getViewport({ scale: SCALE }); // 创建页容器 div const pageDiv document.createElement(div); pageDiv.className print-page; pageDiv.style.pageBreakAfter always; // 强制分页 pageDiv.style.width ${viewport.width}px; pageDiv.style.height ${viewport.height}px; // 创建打印专用 canvas const printCanvas document.createElement(canvas); printCanvas.width viewport.width; printCanvas.height viewport.height; printCanvas.style.width 100%; printCanvas.style.height 100%; pageDiv.appendChild(printCanvas); printContainer.appendChild(pageDiv); // 渲染到该 canvas const printCtx printCanvas.getContext(2d); const renderContext { canvasContext: printCtx, viewport: viewport }; await page.render(renderContext).promise; } // 注入打印样式必须在 DOM 插入后立即添加 const style document.createElement(style); style.textContent media print { body * { visibility: hidden; } #print-container, #print-container * { visibility: visible; } #print-container { position: absolute; left: 0; top: 0; width: 100%; } .print-page { page-break-after: always; margin: 0; padding: 0; } canvas { max-width: 100%; height: auto; } } ; document.head.appendChild(style); }逻辑说明pageBreakAfter: always是 CSS 打印分页的核心指令确保每页 div 占满一张纸visibility: hidden/visible是打印时隐藏无关元素的标准技巧style标签动态注入避免污染全局 CSS。3.3 触发打印与用户交互支持“当前页”与“全部页”双模式按钮点击后需判断用户意图是只打当前屏幕显示的页还是全部这里用一个简单状态变量currentRenderedPage记录上次渲染的页码配合initPrint()的全量渲染做区分let currentRenderedPage 1; // 修改 load-btn 的 click 事件在渲染后更新 currentRenderedPage document.getElementById(load-btn).addEventListener(click, async () { // ... 原有渲染逻辑 ... currentRenderedPage 1; // 重置为第一页 }); document.getElementById(print-btn).addEventListener(click, () { if (!pdfDoc) { alert(请先加载 PDF); return; } // 方案一只打印当前页快速响应 if (confirm(确定要打印第 ${currentRenderedPage} 页吗\n点击“取消”将打印全部 ${pdfDoc.numPages} 页)) { // 临时创建单页打印容器 const tempContainer document.createElement(div); tempContainer.id temp-print; document.body.appendChild(tempContainer); const page pdfDoc.getPage(currentRenderedPage); const viewport page.getViewport({ scale: PRINT_RESOLUTION / 96 }); const canvas document.createElement(canvas); canvas.width viewport.width; canvas.height viewport.height; tempContainer.appendChild(canvas); const ctx canvas.getContext(2d); page.render({ canvasContext: ctx, viewport }).promise.then(() { window.print(); document.body.removeChild(tempContainer); }); } // 方案二打印全部页调用 initPrint else { initPrint(); window.print(); } });关键细节window.print()必须在 DOM 更新完成后调用因此单页打印用了 Promise 回调全部打印则依赖initPrint()的同步 DOM 构建无需 await。4. 避坑指南五个真实翻车现场与对应解法4.1 现象中文显示为方框□□□英文正常原因cmaps/目录缺失、路径错误或 PDF 内嵌字体未被正确映射。pdf.js 默认尝试加载cmaps/UniGB-UCS2-H.bcmap若 404 则回退失败。解决检查浏览器 Network 面板过滤cmaps/确认所有.bcmap文件返回 200若用 Web 服务器如 Nginx需配置location /cmaps/ { add_header Access-Control-Allow-Origin *; }避免跨域拦截。4.2 现象pdf.worker.js报错Uncaught SyntaxError: Unexpected token 原因服务器将.js文件当作 HTML 返回常见于本地双击打开file://协议或 Nginx 配置了try_files $uri $uri/ /index.html。解决开发阶段务必用http-server或 VS Code Live Server 启动生产环境检查 MIME 类型确保pdf.worker.js返回Content-Type: application/javascript。4.3 现象getDocument拒绝 Promise报错Invalid PDF structure原因PDF 文件损坏或 HTTP 响应头Content-Type不是application/pdf如后端返回text/plain。解决用curl -I http://your-domain/assets/sample.pdf检查响应头本地测试时用fetch(./assets/sample.pdf).then(r r.arrayBuffer())替代直接传 URL可绕过 MIME 限制。4.4 现象打印预览中页面错位、内容被截断原因#print-container的 CSS 未生效或media print样式被其他框架覆盖。解决在打印预览界面按CtrlShiftI打开 DevTools切换到Rendering面板勾选Emulate CSS media type: print检查.print-page是否应用了page-break-after将打印样式style标签插入head末尾提高优先级。4.5 现象移动端 Safari 无法加载白屏无报错原因iOS Safari 对 Web Worker 支持有限且pdf.worker.js的importScripts在某些版本会静默失败。解决降级使用pdfjsLib.GlobalWorkerOptions.workerSrc显式指定路径并添加容错pdfjsLib.GlobalWorkerOptions.workerSrc ./dist/pdf.worker.js; // 在 getDocument 前加检测 if (typeof Worker undefined) { alert(当前浏览器不支持 Web WorkerPDF 渲染将降级为同步模式可能卡顿); }5. 进阶技巧支持密码保护 PDF 与自定义文本选择行为5.1 处理加密 PDF在 getDocument 时传入密码参数很多企业 PDF 含打开密码pdf.js 原生支持。关键不是弹窗输入而是将密码作为password选项传入getDocument// 方式一已知密码硬编码仅限测试 const loadingTask pdfjsLib.getDocument({ url: ./assets/protected.pdf, password: 123456 }); // 方式二用户输入密码推荐 document.getElementById(load-btn).addEventListener(click, () { const pwd prompt(请输入 PDF 打开密码); if (!pwd) return; const loadingTask pdfjsLib.getDocument({ url: ./assets/protected.pdf, password: pwd, // 添加超时避免无限等待 withCredentials: false }); loadingTask.promise.then(doc { pdfDoc doc; renderPage(1); // 自定义渲染函数 }).catch(err { if (err.name PasswordException) { alert(密码错误请重试); } else { console.error(err); } }); });注意PasswordException是 pdf.js 特有错误类型必须显式捕获withCredentials: false禁用 Cookie 发送避免跨域泄露。5.2 增强文本选择体验启用文本图层TextLayer与搜索高亮默认渲染只有 canvas无法选中文本。开启TextLayer需额外步骤为每页创建div classtextLayer并用getTextContent()提取字符位置再通过TextLayerBuilder渲染。以下是第一页启用文本层的最小实现async function renderPageWithText(pageNum) { const page await pdfDoc.getPage(pageNum); const viewport page.getViewport({ scale: 1.5 }); canvas.height viewport.height; canvas.width viewport.width; // 1. 渲染 canvas const renderContext { canvasContext: ctx, viewport }; await page.render(renderContext).promise; // 2. 创建 textLayer 容器 const textLayerDiv document.createElement(div); textLayerDiv.className textLayer; textLayerDiv.style.position absolute; textLayerDiv.style.left 0; textLayerDiv.style.top 0; textLayerDiv.style.right 0; textLayerDiv.style.bottom 0; textLayerDiv.style.overflow hidden; textLayerDiv.style.opacity 0.2; canvas.parentNode.insertBefore(textLayerDiv, canvas.nextSibling); // 3. 获取文本内容并构建图层 const textContent await page.getTextContent(); const textLayer new pdfjsLib.TextLayerBuilder({ textLayerDiv, pageIndex: pageNum - 1, viewport }); textLayer.setTextContent(textContent); textLayer.render(); }效果启用后可鼠标拖选文本、CtrlF搜索需额外实现搜索框、复制带格式文本。但注意性能损耗每页多一次getTextContent()调用大 PDF100 页需节流。5.3 打印质量调优控制 DPI 与页面边距PRINT_RESOLUTION并非越高越好。实测 150 DPI 是平衡点低于 120 DPI 打印模糊高于 200 DPI 内存暴涨且无感知提升。此外可通过 CSS 覆盖浏览器默认页边距media print { page { margin: 0.5in; /* 统一设置页边距 */ } body { margin: 0; padding: 0; } }重要提示page规则仅在打印时生效不影响屏幕显示margin: 0.5in是物理英寸非 CSS 像素确保打印尺寸准确。从那以后我每次集成 pdf.js都强制走一遍「本地双击测试 → Chrome 打印预览 → iOS Safari 实机验证 → 加密 PDF 密码流测试」四步 checklist。尤其cmaps/目录和pdf.worker.js路径已经成了我新建项目的 pre-commit hook 检查项。希望帮到你。本文还有配套的精品资源点击获取