简介:这是一份基于PDF.js实现浏览器端在线预览PDF的完整前端资源包,面向Web前端开发者和需要快速集成PDF预览功能的技术人员。资源共402个文件,压缩包大小仅3.06MB,核心由JS、HTML、CSS构成可直接运行的示例页面与工具脚本,并配套bcmap、properties等字体映射文件,以及PNG、SVG图标素材,确保离线环境下也能稳定渲染中文等复杂PDF内容。包体内还包含pdfjs-dist核心库、单页渲染Demo和多页预览相关代码,覆盖getDocument加载文档、page.render绘制Canvas、scale参数缩放、progress事件监听进度等关键技术点,同时提供文本选择与搜索、Web Worker后台解析等进阶配置的说明性文件。此外,代码结构清晰,适合初学者与有经验者研读,可直接用于快速搭建在线预览系统或作为二次开发基础。目前已有3138人学习下载,是理解PDF.js渲染链路、完善预览需求的实用资料。 我们平时做管理系统、门户网站之类的前端项目,总会碰到一个绕不开的需求:在线查看 PDF 文件。用户那边合同是 PDF、报表是 PDF、说明书也是 PDF,你要么让他下载了再看,要么直接在页面上打开。下载这种方法体验太差,用户一来嫌麻烦,二来容易泄露文件路径,三来预览需求往往还伴随着“只让看不让下载”“要能把当前页打出来”这类附加条件。所以“JS 在线查看 PDF 文件”虽然是个老话题,但真做起来,方案选型和踩坑点都比想象中多。
这篇文章我把我做过的几种方案、实际用的代码、以及每次都会踩的坑整理出来。无论你是准备从零开始做一个在线文件预览模块,还是项目里已经接了某个 PDF 插件但问题不断,这篇文章都会给你一个比较完整的参考思路。
1. 在线 PDF 查看的整体方案选择
1.1 原生浏览器打开为什么不够用
最简单的方案其实不需要写任何 JS:直接把 PDF 文件地址丢到<a>标签里,或者用window.open(url)打开,Chrome、Edge、Firefox 都有内置的 PDF 阅读器,能看能翻页能打印。但实际项目里这个方案很快会撞墙:
- 样式完全不可控,浏览器自带工具栏和你的系统风格完全不搭,用户会觉得“跳出去了”。
- 如果 PDF 地址需要权限校验(比如带 Token、带签名),浏览器地址栏直接访问往往拿不到文件。
- 移动端体验很不稳定,iOS 上打开 PDF 的行为和 Android 差异很大,部分国产浏览器甚至直接变成下载。
- 你无法统计用户看了第几页、看了多久,也没法做水印、禁止下载这类业务控制。
所以只要需求稍微复杂一点,就必须走前端代码控制路线。
1.2 三类实现方案对比
我大致把常见方案分成三类,选择依据主要是项目技术栈和定制深度。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| iframe / embed / object 直接嵌套 | 零代码,改动最小 | 跨域限制多、样式不可控、移动端兼容差 | 内部系统快速预览,文件与本系统同源 |
| 基于 pdf.js 的自定义查看器 | 可控性强、解析 PDF 效果最好、可定制 UI、可做权限控制 | 需要写较多代码,处理不当有性能问题 | 绝大多数需要在线预览业务场景 |
| 服务端转换图片/HTML 后再展示 | 兼容性最好,任何终端都能看 | 需要消耗服务器资源,转换耗时,文字不可选中 | 复杂格式转换、老旧环境(如 IE 兼容) |
其中 pdf.js 是最主流的选择。它是 Mozilla 官方维护的 PDF 解析渲染引擎,用纯 JS 实现,浏览器上能跑,Node 上也能跑。你说的 vue-pdf、react-pdf、ng2-pdf-viewer 这些组件,底层核心其实都是 pdf.js,只是每个框架把它封装了一层。
2. PDF.js 是绕不开的核心
2.1 PDF.js 到底做了什么
PDF.js 的渲染原理一句话总结:后端不对 PDF 做任何处理,前端拿到的是原始 PDF 字节流,由 JS 解析并绘制到 Canvas 上。
它内部包含几个核心部分:
- PDFDocumentProxy:代表加载完成的 PDF 文档对象,可以拿到页数、页面尺寸、书签等元信息。
- PDFPageProxy:代表单页,调用
page.render()就能把该页绘制到指定 canvas 上。 - PDFWorker:负责在 Web Worker 里跑解析逻辑,避免阻塞主线程,提升大文件渲染流畅度。
- 文本层(TextLayer):用于实现文字选中、复制、搜索,配合 Canvas 渲染出的图形层一起工作。
你不需要记住所有 API 细节,但一定要知道这套机制。因为实际开发中会让你卡住的坑,基本都藏在“Canvas 绘制”“Worker 加载”“文本层计算”这三个环节里。
2.2 导入方式与版本坑
pdf.js 在 npm 上的包名是pdfjs-dist,版本迭代比较快,API 也偶尔有破坏性变更,我做过几个项目,说几个实际经验。
如果你是 Vue 2 项目,很多老教程让你用vue-pdf,这个包方便,但维护已经不太活跃,遇到高版本 Chrome 或复杂 PDF 时偶尔会出现渲染异常。如果项目工期紧,vue-pdf 能跑就先用;如果你要长期维护,我建议直接用pdfjs-dist自己封装,可控性更强。
npm 安装示例:
npm install pdfjs-dist@3.11.174为什么我锁版本?因为 4.x 版本之后,worker 的引入方式变了,CSP(内容安全策略)环境下的兼容处理也不一样。锁定具体版本,至少保证团队内环境一致。
核心代码引入方式:
import * as pdfjsLib from 'pdfjs-dist'; import workerUrl from 'pdfjs-dist/build/pdf.worker.min.js?url'; pdfjsLib.GlobalWorkerOptions.workerSrc = workerUrl;关键点在workerSrc。很多人第一次接触 pdf.js 都会忘掉设置 Worker,于是 PDF 直接在主线程解析,轻则卡顿,重则直接报错“Setting up fake worker failed”。如果用的是 Vite,用?url的方式去拿 worker 地址最省事;如果用 webpack,一般要配合file-loader或者直接把 worker 文件放到 public 目录,然后写死路径。
3. 手写一个可用 PDF 查看器的完整过程
3.1 加载文档与渲染首页
我带着你从零搭一个极简但能用的查看器,先不做花哨 UI,把核心逻辑走通。
HTML 部分:
<div id="pdf-container"> <canvas id="pdf-canvas"></canvas> </div> <div> <span>第 <span id="page-num">1</span> / <span id="page-count">-</span> 页</span> <button id="prev">上一页</button> <button id="next">下一页</button> <input type="range" id="scale-range" min="0.5" max="2" step="0.1" value="1" /> <button id="print">打印</button> </div>加载 PDF 并渲染第一页:
let pdfDoc = null; let currentPage = 1; let currentScale = 1.0; async function loadPdf(url) { const loadingTask = pdfjsLib.getDocument(url); pdfDoc = await loadingTask.promise; document.getElementById('page-count').textContent = pdfDoc.numPages; await renderPage(currentPage); } async function renderPage(pageNum) { const page = await pdfDoc.getPage(pageNum); const viewport = page.getViewport({ scale: currentScale }); const canvas = document.getElementById('pdf-canvas'); const ctx = canvas.getContext('2d'); // 关键:canvas 的尺寸必须和 viewport 一致 canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: ctx, viewport }).promise; document.getElementById('page-num').textContent = pageNum; } loadPdf('/path/to/your.pdf');这里有个容易出错的地方:Canvas 的宽高属性是像素宽度,如果不按viewport.width / height赋值,而是靠 CSS 拉伸,渲染出来的 PDF 文字会模糊得像隔了一层毛玻璃。一定要同时设置 canvas 的 width/height 属性和 CSS 尺寸。
3.2 翻页、缩放与页码联动
翻页逻辑本身不复杂,但要注意边界条件和渲染状态:
document.getElementById('prev').addEventListener('click', () => { if (currentPage <= 1) return; currentPage--; renderPage(currentPage); }); document.getElementById('next').addEventListener('click', () => { if (currentPage >= pdfDoc.numPages) return; currentPage++; renderPage(currentPage); });边界条件要加,否则用户一直点下一页,页码会越界,渲染时会传一个不存在的页号然后报错。
缩放这里我多写一句。如果你只改 canvas 的 CSS 尺寸,不重新调用 render,缩放后页面虽然看起来变大了,但清晰度会直线下降。正确的做法是改currentScale后重新走一遍renderPage(),让 PDF.js 按新比例重新绘制。
再看一下渲染时的细节:如果用page.render()后马上翻页,上一次渲染还没结束就开启下一次渲染,浏览器会强行中断上一次任务,并且控制台会抛异常。解决办法是加一个渲染任务标记:
let renderTask = null; async function renderPage(pageNum) { if (renderTask) { renderTask.cancel(); } // ... renderTask = page.render({ canvasContext: ctx, viewport }); await renderTask.promise; }renderTask.cancel()是 pdf.js 官方提供的取消渲染方法,处理快速翻页场景非常管用。
3.3 加一个打印按钮
在线查看器十有八九要顺带支持打印,甚至只打印当前页。最初我做的时候直接在页面调window.print(),结果打印出来的内容要么只有 canvas 第一屏,要么布局全乱。后来整理出一套比较稳的做法:用隐藏 iframe 承载待打印内容。
function printPage() { 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 iframeDoc = iframe.contentWindow.document; iframeDoc.write('<html><head><title>打印</title></head><body>'); // 把当前 canvas 转成图片放进 iframe const img = iframeDoc.createElement('img'); img.src = document.getElementById('pdf-canvas').toDataURL('image/png'); iframeDoc.body.appendChild(img); iframeDoc.write('</body></html>'); iframeDoc.close(); iframe.contentWindow.print(); // 打印后回收 iframe iframe.contentWindow.onafterprint = () => { document.body.removeChild(iframe); }; }要点:直接把 canvas 用toDataURL转成图片再打印,规避了 canvas 打印时样式丢失的问题。如果你的 PDF 页数很多,建议按“当前页”“全部页”两个按钮分开做,全部页需要循环遍历渲染再组成图片列表,性能开销比较大,最好加上进度提示。
4. 在线查看常见问题与排查
4.1 跨域与文件权限问题
pdf.js 通过getDocument(url)拉取文件时,受浏览器同源策略限制。如果 PDF 文件在另一个域名,或者加了鉴权头,简单传 URL 是不行的。
实际项目里最常见的是:PDF 文件要求登录后才能看,直接传 URL 等于没传。解决方案是先用fetch带上 token 拿文件流,再转成 ArrayBuffer 交给 pdf.js:
async function loadPdfWithToken(url, token) { const response = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) { throw new Error('PDF 加载失败: ' + response.status); } const buffer = await response.arrayBuffer(); const loadingTask = pdfjsLib.getDocument({ data: buffer }); pdfDoc = await loadingTask.promise; }4.2 字体、乱码与中文显示问题
绝大多数“乱码”问题并不是 pdf.js 的锅,而是 PDF 文件本身的字体子集化问题。PDF 里内嵌了字体,解析时依赖浏览器字体渲染能力,正常现代浏览器都能处理。如果碰到中文乱码,首先换最新版 Chrome/Edge 试试,如果新版没问题,说明就是你本地浏览器版本太老。
还有一种情况:用户的 PDF 是由扫描件组成的,本质是图片,那需要后端 OCR 才能提取文字,前端再怎么做也没有用。这种要先和需求方确认文件来源。
4.3 大文件加载慢、Canvas 崩溃
上百 MB 的 PDF 在线查看是性能杀手。getDocument()默认是按需加载页面数据的,所以打开时只渲染当前页,速度还能接受,但翻页和大图渲染时依然可能卡顿。
几个优化手段我实测下来比较有用:
- 懒加载:只渲染当前页和相邻页,不要一次把所有页都渲染出来。
- 限制同时渲染的 canvas 数量:翻页时释放上一页的资源,
canvas.width = 1; canvas.height = 1;可以强制释放显存。 - 控制最大缩放比例:PDF.js 在 3 倍以上缩放时,canvas 尺寸可能超过浏览器最大限制,导致白屏。实测如果超过浏览器 canvas 的宽高上限,需要把渲染分成多个小 canvas 拼接,但工程量大,简单粗暴的方式是限制最大比例 2.5 或 3。
大文件加载建议加一个 loading 动画:
const loadingTask = pdfjsLib.getDocument(url); loadingTask.onProgress = (progress) => { if (progress.total > 0) { const percent = Math.round((progress.loaded / progress.total) * 100); console.log(`加载进度:${percent}%`); } };onProgress可以拿到已加载字节数,用来做进度条很合适。
5. 再往前一步:列表、缩略图与权限控制
5.1 缩略图侧边栏其实没你想的那么难
很多人看到网上各种 PDF 查看器带缩略图列表,以为很复杂。其实 pdf.js 里拿缩略图数据特别直接:渲染每一页时,把 viewport 的 scale 调小(比如 0.2),渲染到一个很小的 canvas 上,再把 canvas 当成缩略图插入侧边栏。
async function renderThumbnail(pageNum, container) { const page = await pdfDoc.getPage(pageNum); const viewport = page.getViewport({ scale: 0.2 }); const canvas = document.createElement('canvas'); canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise; container.appendChild(canvas); }但要注意,如果 PDF 有几十上百页,一次性渲染全部缩略图会让页面卡死。稳妥做法是滚动到哪个区域,再按需渲染那个区域的缩略图,这也是浏览器推荐的做法。
5.2 权限控制思路
PDF 的权限控制要分两层:
一层是加载权限,就是前面说的带 token 请求,拿不到文件就什么都看不到。
另一层是操作权限,比如禁止下载原文件、禁止复制文字。pdf.js 本身不禁止用户保存文件,但可以这样缓解:
- 前端把源文件 URL 设置为带签名的一次性地址,过期失效。
- 禁止右键和拖拽(防君子不防小人,但能做)。
- 在渲染层加水印,比如平铺用户 ID 或者邮箱,一旦截图泄露能追责。
水印实现方式不复杂,canvas 渲染完成后,在其上方覆盖一层 canvas,用globalAlpha降低水印透明度,循环绘制字符串。
6. 一些个人经验
最后分享一点我自己的经验吧。做在线 PDF 预览,技术选型不算难,真正浪费时间的地方全在边界场景:有人传一堆扫描件说是 PDF,有人用特别老版本的手机浏览器打开测试页面,有人从某个网盘下载的 PDF 本身损坏了但下载后能看,你说奇怪不奇怪。
所以代码尽量写得防御性强一点:
getDocument失败一定要 catch,并且给用户一个友好的提示,而不是白屏。- 加载大文件时务必做进度反馈,不然用户以为系统挂了。
- 用
pdfjs-dist时锁死版本,升级前先测试几个不同类型的 PDF。 - 一旦项目里用到了 iframe 嵌套打印、canvas 转图片、缩略图批量渲染,提前做好内存回收,不然长时间翻页后页面会越来越迟钝。
我们后来还把部分文件转换成了轻量化 HTML 或者图片格式优先展示,PDF 作为一种兜底,这样服务器开销和前端渲染压力都小很多。如果你只是需要“能看就行”,那上面这套方案完全够用;如果需求是“不但能看,还要好看、好用、可管理”,那就是一个持续迭代的过程了。
本文还有配套的精品资源,点击获取