简介:这份资源是面向前端开发者与Web应用工程师的PDF.js实战Demo,用于解决浏览器内免插件预览与打印PDF的需求。包内共376个文件,以168个bcmap字符映射表、105个properties语言资源、72个png与9个svg界面素材为主,另有6个js脚本、3个html页面及css样式,压缩包约2.49MB,完整保留了PDF.js库的静态资源与示例入口。Demo覆盖在线加载渲染、页面缩放、本地文件打开与调用浏览器打印接口等核心能力,并演示了PDFDocumentProxy加载文档、page.render渲染画布、Web Worker后台解析等关键实现思路,目录中src与web模块分工清晰,便于对照源码理解解析、渲染与交互的完整链路。目前已有9927人学习下载,适合希望快速搭建PDF预览功能、研究跨平台阅读方案或扩展注释、搜索等场景的开发者参考。
1. 浏览器里直接翻 PDF:这个纯 JS Demo 把预览和打印都包了
很多后台系统一到「合同附件在线看」就犯难:用 iframe 嵌 PDF 吧,Chrome 能看、某些环境直接触发下载;用第三方云预览吧,文件得先传出去,内网项目根本走不通。我最近拆的这个项目,就是拿 pdf.js 做的一个纯前端 Demo,把 PDF 在线预览和打印这两件事一次性解决掉——不依赖后端渲染,不装浏览器插件,一个静态页面丢进项目里就能跑。
它适合谁?做管理后台、OA、合同系统、报表导出的前端同学,尤其是那种「文件不能出内网、又想要接近原生阅读器体验」的场景。整个 Demo 的核心就是 pdf.js 这套 JS 库:解析、渲染、分页、缩放、打印全在浏览器里完成,服务端只负责把 PDF 文件吐出来。下面我按「它怎么组织 → 怎么跑起来 → 核心渲染链路 → 打印怎么接 → 怎么改」的顺序拆一遍,中间踩过的坑也一并写出来。
2. 项目结构与技术选型:一个静态页怎么撑起完整阅读器
2.1 目录长什么样
这个 Demo 的体量很小,属于「拿来就能塞进现有工程」的类型。典型结构是这样:
pdfjs-demo/ ├── index.html # 入口页,工具栏 + 画布容器 ├── css/ │ └── viewer.css # 工具栏、画布、加载态样式 ├── js/ │ ├── main.js # 初始化、事件绑定、状态管理 │ ├── render.js # 页面渲染核心逻辑 │ └── print.js # 打印流程封装 ├── lib/ │ └── pdfjs/ # pdf.js 发行包 │ ├── pdf.min.js │ └── pdf.worker.min.js └── files/ └── sample.pdf # 演示用 PDFlib/pdfjs是整个项目的命脉,pdf.min.js是主库,pdf.worker.min.js是解析工作线程。很多人第一次跑 pdf.js 翻车就翻在这里——worker 路径没配对,控制台报一句Setting up fake worker failed,页面白屏,看着像库坏了,其实是路径问题。
2.2 为什么选 pdf.js 而不是别的方案
选型逻辑其实很直接。浏览器原生<embed>、<iframe>看 PDF,体验完全交给浏览器,你控制不了工具栏、控制不了缩放、移动端还经常直接下载。云预览服务体验好,但文件要出内网,合规上过不去。pdf.js 是 Mozilla 出的纯 JS 实现,把 PDF 解析成 Canvas 绘制,等于你自己拿回了渲染控制权。
代价也有:它是「自己画」,所以大文件、复杂字体、扫描件的性能要自己扛。这个 Demo 的价值就在于把这条链路跑通并封装好,你拿去改比从零啃 pdf.js API 快得多。
2.3 关键依赖与版本注意
Demo 依赖的只有 pdf.js 一个库,没有框架绑定,jQuery、Vue、React 都能接。需要注意的是 pdf.js 的主库和 worker 版本必须一致,混用不同版本的pdf.min.js和pdf.worker.min.js,轻则渲染错位,重则直接抛异常。我一般会把两个文件放同一目录、同一版本号,升级时一起换。
提示:pdf.js 从某个版本起拆分了 legacy 构建(兼容旧浏览器)和现代构建,老项目里如果还要兼容旧内核浏览器,选 legacy 那份,别拿现代构建硬顶。
3. 跑起来:从零到页面能翻页的完整步骤
3.1 起一个本地静态服务
pdf.js 加载 worker 和 PDF 文件都走网络请求,直接双击index.html用file://打开,大概率被浏览器的同源策略拦下,worker 加载失败。所以第一步是起个静态服务:
# 在项目根目录执行,任选一种 python3 -m http.server 8080 # 或者 npx serve -l 8080然后浏览器访问http://localhost:8080。这一步看着废话,但它是新手最常见的卡点——「我代码没错啊怎么白屏」,八成就是file://打开的。
3.2 初始化 pdf.js 并加载文档
main.js里最核心的就是拿到 PDF 文档对象。下面这段是精简后的初始化逻辑:
// 指定 worker 路径,必须和主库版本一致 pdfjsLib.GlobalWorkerOptions.workerSrc = './lib/pdfjs/pdf.worker.min.js'; let pdfDoc = null; // 当前文档对象 let currentPage = 1; // 当前页码 let scale = 1.2; // 缩放比例 // 加载 PDF:url 可以是文件地址,也可以是后端接口 async function loadPdf(url) { const loadingTask = pdfjsLib.getDocument({ url, // 大文件场景可开启分片加载 rangeChunkSize: 65536, }); pdfDoc = await loadingTask.promise; document.getElementById('pageCount').textContent = pdfDoc.numPages; renderPage(currentPage); }逻辑说明:getDocument返回的是一个loadingTask,不是文档本身,必须await它的promise才拿到pdfDoc。rangeChunkSize控制分片大小,PDF 支持 Range 请求时,pdf.js 会按需拉取,不用一次性下载整个文件——这是大文件优化的关键参数,默认 64KB 对多数场景够用。
参数上,url既可以是静态文件路径,也可以是你后端返回文件流的接口地址,只要接口支持 Range 请求,体验会明显更好。
3.3 渲染单页到 Canvas
拿到文档后,渲染逻辑在render.js:
async function renderPage(num) { const page = await pdfDoc.getPage(num); const viewport = page.getViewport({ scale }); const canvas = document.getElementById('pdfCanvas'); const ctx = canvas.getContext('2d'); // 处理高分屏,避免文字发虚 const ratio = window.devicePixelRatio || 1; canvas.width = viewport.width * ratio; canvas.height = viewport.height * ratio; canvas.style.width = viewport.width + 'px'; canvas.style.height = viewport.height + 'px'; ctx.scale(ratio, ratio); await page.render({ canvasContext: ctx, viewport, }).promise; }逻辑说明:getViewport({ scale })根据缩放算出画布尺寸,page.render把这一页画到 Canvas 上。这里有个血泪经验——高分屏必须按devicePixelRatio放大画布再缩放上下文,否则 Retina 屏上文字糊得像蒙了层雾。很多人第一次做出来觉得「pdf.js 渲染质量不行」,其实就差这几行。
参数上,scale直接决定清晰度和性能,1.0 是原始尺寸,1.2~1.5 是阅读舒适区,再大就要考虑虚拟滚动,不然内存吃不消。
3.4 翻页与缩放的事件绑定
工具栏按钮绑定的就是改currentPage和scale再重渲染:
document.getElementById('prev').onclick = () => { if (currentPage <= 1) return; currentPage--; renderPage(currentPage); }; document.getElementById('next').onclick = () => { if (currentPage >= pdfDoc.numPages) return; currentPage++; renderPage(currentPage); }; document.getElementById('zoomIn').onclick = () => { scale = Math.min(scale + 0.2, 3); renderPage(currentPage); };逻辑说明:每次翻页或缩放都重新调renderPage,简单直接。Demo 阶段这样够用,但要注意——同一个 Canvas 上重复 render 前,最好先清空或取消上一次渲染任务,快速连点翻页时会出现两页叠影,这是 pdf.js 的经典坑,后面避坑章节细说。
4. 打印链路:把 Canvas 渲染接进浏览器打印
4.1 打印为什么不能直接 window.print()
最直觉的做法是window.print()打印整个页面,但那样会把工具栏、按钮全打进去,而且 Canvas 在打印时经常被裁切或缩放错位。正确思路是:把要打印的页面渲染成图片,塞进一个专门的打印容器,再触发打印。
4.2 逐页渲染成图片再打印
print.js的核心逻辑:
async function printPdf() { const printWindow = window.open('', '_blank'); const total = pdfDoc.numPages; for (let i = 1; i <= total; i++) { const page = await pdfDoc.getPage(i); // 打印用更高倍率,保证清晰度 const viewport = page.getViewport({ scale: 2 }); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: ctx, viewport }).promise; // 转成图片写入打印窗口 const img = document.createElement('img'); img.src = canvas.toDataURL('image/png'); img.style.width = '100%'; printWindow.document.body.appendChild(img); } // 等图片加载完再打印,否则打出空白页 printWindow.document.close(); printWindow.onload = () => { printWindow.focus(); printWindow.print(); }; }逻辑说明:这里逐页渲染、逐页转dataURL,再拼进新窗口。scale: 2是为了打印清晰度,屏幕看 1.2 够,打印得翻倍。关键点是必须等图片加载完成再调print(),否则浏览器打印预览里全是空白——这是打印功能最常见的翻车点。
参数上,toDataURL('image/png')质量高但体积大,页数多时内存压力明显;换成image/jpeg加质量参数能压下来,但文字边缘会有轻微噪点,合同类文档建议还是 PNG。
4.3 打印样式与分页控制
光有图片还不够,得让每张图独占一页:
@media print { img { page-break-after: always; /* 每张图后强制分页 */ max-width: 100%; } img:last-child { page-break-after: auto; } }逻辑说明:page-break-after: always保证一页 PDF 对应一张打印纸,最后一张不加,避免多出一张空白页。这个细节不做,打印出来页码全乱。
注意:部分浏览器对
window.open有拦截,打印按钮的点击必须由用户手势直接触发,不能在异步回调里延迟太久再开窗口,否则会被当成弹窗广告拦掉。
5. 避坑与排查:这几个问题我几乎每次都遇到
5.1 白屏,控制台报 fake worker failed
现象:页面加载后一片空白,控制台提示 worker 加载失败。原因:workerSrc路径写错,或者用file://协议打开导致跨域。解决:确认workerSrc指向真实存在的pdf.worker.min.js,并且通过 HTTP 服务访问,别双击 HTML。
5.2 快速翻页出现叠影或错页
现象:连点下一页,画布上两页内容重叠。原因:上一次render还没结束,新的渲染就开始了,两个任务往同一个 Canvas 上画。解决:维护一个渲染任务引用,新渲染前调renderTask.cancel(),或者给每次渲染加个序号,只认最新一次的结果。
5.3 中文 PDF 显示成方块或乱码
现象:英文正常,中文全是方框。原因:PDF 内嵌字体缺失,或者 pdf.js 的 CMap 资源没配。解决:把 pdf.js 发行包里的cmaps目录一起拷进项目,并在getDocument时指定cMapUrl和cMapPacked: true。这个坑在扫描件和国产办公软件导出的 PDF 上特别常见。
5.4 大文件加载慢、内存飙升
现象:几十兆的 PDF 一打开浏览器就卡死。原因:一次性渲染所有页,或者没开分片加载。解决:只渲染当前页,翻页时销毁上一页的 Canvas;getDocument开启rangeChunkSize走分片;页数特别多时上虚拟滚动,只保留可视区附近的页面。
5.5 打印出来是空白或只有第一页
现象:点打印,预览里没内容。原因:图片还没加载完就调了print(),或者打印窗口的document没close()。解决:把print()放到onload或图片onload计数回调里,确保所有图片就绪再触发。
6. 二次开发:把它改成你自己的阅读器
拿到这个 Demo,真正要落地到项目里,通常得改三块:文件来源、交互形态、渲染策略。
先说文件来源。Demo 里是静态路径,实际项目里 PDF 多半来自后端接口,还带鉴权。常见做法是把getDocument的url换成一个带 token 的接口地址,或者用httpHeaders参数把认证头带上:
const loadingTask = pdfjsLib.getDocument({ url: '/api/contract/preview?id=123', httpHeaders: { Authorization: 'Bearer ' + token }, withCredentials: true, });这样文件流还是走你自己的后端,权限、日志、水印都能在服务端控制,前端只负责渲染。
再说交互形态。Demo 是单页翻页,实际后台里更常见的是连续滚动——把所有页竖着排下来,滚动到哪渲染哪。改造思路是给每页建一个占位容器,用IntersectionObserver监听进入视口再渲染,离开视口就清空 Canvas 释放内存。这个模式对几十页的文档体验提升非常明显,也是从 Demo 走向可用产品的关键一步。
最后是渲染策略。如果文档页数多、又要支持缩放,建议引入「渲染缓存 + 取消机制」:缓存最近看过的几页位图,快速翻回时直接复用;同时给渲染任务加取消,避免无效渲染堆积。我一般会维护一个Map,key 是页码_缩放比,超过一定数量就按 LRU 淘汰。
验证改造是否成功,我习惯盯三个指标:首屏渲染时间、连续翻页是否掉帧、内存占用是否随页数线性增长。前两个用 Performance 面板看,内存用 Chrome 的 Memory 快照对比。只要内存不随翻页无限涨,基本就稳了。
有个教训我印象很深:早期图省事,每次翻页都新建 Canvas 却不销毁旧的,测试十几页没事,用户一开两百页的标书,浏览器直接崩。从那以后我每次做 Canvas 渲染,都强制走一遍「先取消旧任务、再清空画布、最后渲染新页」的流程,一步都不省。希望这个 Demo 的拆解能帮你少走点弯路。
本文还有配套的精品资源,点击获取