简介这是一份基于Mozilla开源项目PDF.js的实战Demo面向需要在Web端实现PDF在线预览与打印的前端开发者尤其适合希望摆脱插件依赖、用纯JavaScript完成文档渲染的中级学习者。压缩包共376个文件约2.49MB以bcmap字体映射文件、properties语言资源、png与svg图标、js脚本及html页面为主另含少量pdf示例、css样式与license说明完整保留了PDF.js运行所需的静态资源与源码结构。Demo覆盖文档加载、Canvas渲染、页面缩放、本地文件读取与浏览器打印等核心环节并涉及Web Worker后台解析、分页加载与缓存等性能优化思路同时提示了同源策略与XSS安全注意事项。目前已有9927人学习下载读者可据此快速搭建可运行的预览环境理解PDFDocumentProxy与page.render等关键API的调用方式并在此基础上扩展注释、搜索或书签等应用场景。1. 浏览器里跑通 PDF 预览为什么我最后选了 pdf.js 这个 Demo前阵子帮一个做内部文档系统的朋友看问题他们的需求很朴素用户上传 PDF 后不装任何插件、不跳转第三方服务直接在网页里翻页看还要能点一下打印。听起来简单真动手才发现坑不少——用iframe嵌 PDFChrome 和 Firefox 表现不一致用浏览器自带的 PDF 查看器打印按钮的样式和回调完全不受控想自己画又得处理字体、编码、分页。折腾一圈最后还是回到 pdf.js 这个纯 JS 方案上。这份 Demo 的价值就在这儿它把「PDF 在线预览 打印」这条链路用纯前端 JS 走通了不依赖后端渲染服务也不需要用户装任何东西。核心是 pdf.js 负责解析和渲染 PDF 到 canvas打印则借助浏览器原生的打印能力配合渲染结果。适合谁做后台管理系统、在线教育资料页、合同/报表预览这类场景的前端尤其是那些不想引入重型 PDF 商业库、又对可控性有要求的团队。下面我按「它怎么工作 → 怎么跑起来 → 参数怎么调 → 坑在哪」的顺序拆一遍。2. pdf.js 的渲染链路从二进制流到 canvas 到底发生了什么2.1 为什么不是 iframe也不是浏览器内置查看器先说选型。很多人第一反应是iframe srcxxx.pdf这在桌面 Chrome 上确实能显示但问题在于移动端 Safari 和部分安卓 WebView 根本不渲染直接变成下载就算渲染了你也拿不到页码、缩放、打印的控制权。浏览器内置查看器比如 Chrome 的 PDF viewer本质是个扩展你的 JS 无法和它通信想自定义工具栏、想监听「打印完成」事件门都没有。pdf.js 的思路完全不同它把 PDF 当成一份需要解析的数据自己在 JS 里实现解析器把每一页的绘制指令翻译成 canvas 上的像素。这样一来页码、缩放、旋转、文本层、打印全部在你手里。代价是要自己管渲染性能后面会讲。2.2 三个核心对象getDocument、page、renderTaskpdf.js 的 API 其实就围绕三个东西转pdfjsLib.getDocument(url)加载 PDF返回一个PDFDocumentProxy里面带着总页数numPages。pdfDoc.getPage(n)拿到第 n 页的PDFPageProxy可以查它的视口getViewport({scale})。page.render({canvasContext, viewport})真正把这一页画到 canvas 上返回一个RenderTask可以.cancel()。理解这三层后面所有参数调整都有落脚点。比如缩放本质就是改viewport的 scale比如清晰度本质是 canvas 的物理尺寸和 CSS 尺寸的比例。2.3 一个最小可跑的渲染函数先看核心渲染逻辑这是整个 Demo 的心脏// 假设 pdfjsLib 已经通过 script 标签或模块引入 async function renderPage(pdfDoc, pageNum, canvas, scale) { // 1. 取页 const page await pdfDoc.getPage(pageNum); // 2. 按缩放算视口scale1 时是 72dpi 的原始尺寸 const viewport page.getViewport({ scale }); // 3. 处理高分屏canvas 物理像素 CSS 像素 * devicePixelRatio const 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; const ctx canvas.getContext(2d); // 4. 关键把上下文按 dpr 放大否则高分屏会糊 ctx.scale(dpr, dpr); // 5. 渲染返回的 task 可用于取消 const renderTask page.render({ canvasContext: ctx, viewport: viewport, }); await renderTask.promise; return renderTask; }逻辑说明第 3、4 步是新手最容易漏的。如果只设canvas.width viewport.width在 Retina 屏上浏览器会把 canvas 拉伸字就发虚。正确做法是物理尺寸乘devicePixelRatio再用ctx.scale把绘制坐标系还原这样渲染出来的像素密度才够。参数说明scale是缩放系数1 表示原始尺寸viewport里的width/height是 CSS 像素renderTask一定要留着引用翻页太快时旧任务不取消会和新任务抢 canvas出现花屏。3. 把 Demo 跑起来目录结构、加载顺序与打印实现3.1 引入方式与 worker 配置pdf.js 有个绕不开的点它默认用 Web Worker 做解析避免阻塞主线程。所以除了主库pdf.js还得有pdf.worker.js。常见做法是script src./build/pdf.js/script script // 告诉主库 worker 文件在哪路径写错会静默降级到主线程 pdfjsLib.GlobalWorkerOptions.workerSrc ./build/pdf.worker.js; /script注意workerSrc必须是能直接访问的 URL跨域或路径不对时pdf.js 会回退到「假 worker」在主线程跑大文件直接卡死 UI。这是排查「为什么一打开就卡」的第一检查点。3.2 加载与翻页的完整流程把加载、翻页、缩放串起来一个能用的预览器大概长这样let pdfDoc null; let currentPage 1; let currentScale 1.2; let currentTask null; async function loadPdf(url) { // 用 arraybuffer 方式加载便于后续做鉴权头等扩展 const loadingTask pdfjsLib.getDocument({ url: url, // 常见做法带 cookie 的请求需要这个 withCredentials: false, }); pdfDoc await loadingTask.promise; document.getElementById(total).textContent pdfDoc.numPages; await goToPage(1); } async function goToPage(num) { // 翻页前先取消上一次渲染避免抢画布 if (currentTask) { currentTask.cancel(); currentTask null; } currentPage num; const canvas document.getElementById(pdf-canvas); currentTask await renderPage(pdfDoc, num, canvas, currentScale); document.getElementById(current).textContent num; }逻辑说明getDocument返回的是loadingTask它本身也有.promise和.destroy()。翻页时先cancel旧任务是防止快速点击「下一页」时两页内容叠在一起。参数上withCredentials决定是否带 cookie做需要登录的文档系统时要按后端策略设。3.3 打印别直接 print先把所有页渲染出来打印是这份 Demo 里最容易被低估的部分。直接window.print()只会打印当前 DOM而你的 canvas 只有一页。正确思路是打印前把所有页渲染到一个隐藏容器里再触发打印。async function printAll() { const container document.getElementById(print-container); container.innerHTML ; for (let i 1; i pdfDoc.numPages; i) { const page await pdfDoc.getPage(i); // 打印用固定 scale保证每页尺寸一致 const viewport page.getViewport({ scale: 1.5 }); const canvas document.createElement(canvas); const dpr window.devicePixelRatio || 1; canvas.width viewport.width * dpr; canvas.height viewport.height * dpr; canvas.style.width 100%; canvas.style.height auto; const ctx canvas.getContext(2d); ctx.scale(dpr, dpr); await page.render({ canvasContext: ctx, viewport }).promise; container.appendChild(canvas); } window.print(); }逻辑说明打印容器平时用 CSSdisplay: none藏起来打印时通过media print显示、隐藏主预览区。参数上打印 scale 建议固定别跟着屏幕缩放走否则每页大小不一。另外window.print()是同步阻塞的渲染没完成就调用会打出空白页所以必须await完所有页。配套的打印样式media print { #preview-area { display: none; } #print-container { display: block; } #print-container canvas { page-break-after: always; } }page-break-after: always保证每张 canvas 单独一页不然浏览器会把它们挤在一起。4. 参数与性能scale、dpr、分页加载怎么配才不卡4.1 scale 和清晰度的取舍scale直接决定渲染像素量。scale1 时一页 A4 约 595×842 CSS 像素乘 dpr2 后 canvas 是 1190×1684约 200 万像素scale 提到 2像素量翻四倍渲染时间也差不多翻倍。我的经验是屏幕预览用 1.21.5 足够打印用 1.52再高收益很小但内存涨得凶。场景建议 scale说明普通屏幕预览1.01.3兼顾清晰与流畅高分屏预览1.21.5配合 dpr 已足够锐利打印输出1.52.0固定值保证页间一致缩略图0.20.3只做导航用别用全尺寸4.2 大文件的分页与懒渲染几十页的 PDF 一次性全渲染内存直接爆。常见做法是只渲染当前页和前后各一页其余用占位。判断「当前页」可以用 IntersectionObserver 监听每页容器。参数上预渲染页数别超过 3否则滚动时反而更卡。4.3 文本层与选中复制如果需求里有「能选中文字」光画 canvas 不够还得叠加 pdf.js 的文本层getTextContent()把文字按坐标绝对定位到 canvas 上方。这一步会明显增加 DOM 节点数页数多时建议只在当前页生成文本层翻页即销毁。提示文本层的字体要和 canvas 渲染的视觉对齐否则选中高亮会错位这是很多人反馈「选中框歪了」的根因。5. 避坑与排查那些让我返工三次的问题5.1 现象页面一片空白控制台无报错原因workerSrc路径错误pdf.js 静默回退主线程但某些版本回退逻辑不完整导致渲染 promise 一直 pending。 解决打开 Network 面板确认pdf.worker.js是否 200路径用绝对路径或基于import.meta.url计算别用相对路径硬拼。5.2 现象高分屏上文字发虚、边缘模糊原因只设了 canvas 的 CSS 尺寸没设物理尺寸也没做ctx.scale(dpr, dpr)。 解决按 2.3 的写法物理尺寸乘 dpr绘制前 scale。改完如果还糊检查是不是 CSS 里又对 canvas 做了非整数缩放。5.3 现象快速翻页时两页内容叠在一起原因旧renderTask没取消新任务和旧任务同时往一个 canvas 上画。 解决翻页前currentTask.cancel()并await新任务。注意cancel()后 promise 会 reject要 catch 掉别让它冒泡成未处理异常。5.4 现象打印出来是空白或只有第一页原因window.print()在渲染完成前被调用或者打印容器被display: none挡住浏览器不渲染隐藏内容。 解决await所有页渲染完再 print打印容器用media print切换显示别用visibility: hidden那个仍占位但不打印。5.5 现象跨域 PDF 加载失败报 CORS原因getDocument默认用 fetch 拉文件跨域时后端没给Access-Control-Allow-Origin。 解决让后端加 CORS 头或改用同源代理。注意withCredentials: true时后端不能把Allow-Origin设成*必须回显具体域名。6. 进阶技巧用 canvas 转图片做缩略图导航与验证预览器做完了怎么验证它真的靠谱我一般会加一个「缩略图侧栏」顺便当成渲染质量的验收手段。思路很简单对每一页用低 scale 渲染成小 canvas再toDataURL转成图片塞进侧栏。这样既能导航又能一眼看出哪些页渲染异常。async function buildThumbnails(pdfDoc) { const sidebar document.getElementById(thumbnails); for (let i 1; i pdfDoc.numPages; i) { const page await pdfDoc.getPage(i); // 缩略图用低 scale省内存 const viewport page.getViewport({ scale: 0.25 }); const canvas document.createElement(canvas); canvas.width viewport.width; canvas.height viewport.height; const ctx canvas.getContext(2d); await page.render({ canvasContext: ctx, viewport }).promise; const img document.createElement(img); // 转成 dataURL避免保留大量 canvas 对象 img.src canvas.toDataURL(image/jpeg, 0.7); img.onclick () goToPage(i); sidebar.appendChild(img); } }逻辑说明缩略图 scale 压到 0.25一页 canvas 只有几十 KB几十页也不心疼。转dataURL后原来的 canvas 可以被 GC 回收比一直挂着 canvas 省内存。参数上JPEG 质量 0.7 是清晰度和体积的平衡点再低就糊了。验证方法渲染完缩略图后肉眼扫一遍如果某页全白或全黑基本是那页 PDF 本身有问题比如扫描件用了特殊编码而不是你的代码。这时候可以单独getPage那页看getOperatorList()有没有异常定位是解析层还是渲染层。从那以后我每次接 PDF 预览需求都强制先跑一遍「缩略图全渲染」——它能在五分钟内暴露 90% 的兼容性和性能问题比翻页点半天高效得多。希望帮到你。本文还有配套的精品资源点击获取