
简介这份资源面向需要在H5页面中嵌入PDF预览功能的Web前端开发者基于Mozilla开源的PDF.js库解决HTML5原生无法直接展示PDF的痛点让用户无需安装插件即可在网页内流畅查看PDF内容。压缩包共402个文件约3.67MB以168个bcmap字符映射文件、124个properties配置、86个png与9个svg界面素材为主另含4个js核心脚本、3个map、2个cur光标、1个css样式及1个html示例页覆盖字体编码、界面资源与渲染逻辑等模块。目前已有1224人学习下载。借助其中的库文件、示例页面与文档读者可快速掌握PDF加载、解析、Canvas分页渲染及缩放、平移、搜索等交互实现并了解Web Worker后台解析的优化思路适合作为H5在线PDF阅读器的集成参考。1. 移动端直接预览 PDF 的痛点为什么我最后选了 pdf.js做过 H5 项目的人大概率都碰过这个需求后端丢过来一个 PDF 链接产品要求点开就能看最好还能翻页、缩放、跳页。第一反应往往是直接iframe srcxxx.pdf或者window.open桌面浏览器上跑得挺欢一上手机就原形毕露——iOS Safari 要么白屏要么只渲染第一页安卓各家 WebView 表现更是玄学有的直接触发下载有的把 PDF 丢给系统阅读器跳走了页面上下文全丢。这个坑我踩过不止一次。后来固定下来的方案就是 pdf.jsMozilla 出的纯前端 PDF 渲染库把 PDF 解析成 Canvas 逐页画出来不依赖浏览器原生 PDF 插件行为完全可控。它解决的核心问题就一个让 PDF 在 H5 里变成你能操控的 DOM 和 Canvas而不是一个黑匣子。适合谁做移动端文档预览、合同签署、课件阅读、发票查看这类场景的前端尤其是需要自定义工具栏、埋点、加水印的团队。2. pdf.js 的渲染原理与工程化选型从解析到 Canvas 落屏pdf.js 不是简单地把 PDF 当图片塞进页面它内部走了一条完整的链路读取 PDF 二进制 → 解析对象结构xref 交叉引用表、对象流、字体、图像→ 构建页面操作符列表 → 交给 Canvas 2D 上下文逐条绘制。理解这条链路才能明白后面为什么会有跨域、字体、内存这些坑。2.1 核心对象与渲染流程pdf.js 暴露出来的关键对象有几个用熟了基本就够pdfjsLib.getDocument(url)拿到PDFDocumentProxy这是整个文档的入口能读页数、元数据、逐页取PDFPageProxy。page.getViewport({ scale })算出这一页在给定缩放下的宽高Canvas 尺寸就靠它。page.render({ canvasContext, viewport })真正把内容画到 Canvas 上返回一个RenderTask可以cancel()。渲染是异步且分页的一页一个 Canvas。移动端内存紧张所以工程上不会一次性把所有页都渲染出来常见做法是只渲染当前页和前后各一页滚动时动态替换。这就是为什么很多成熟方案里你会看到「虚拟滚动 按需渲染」的组合。2.2 三种引入方式的取舍pdf.js 的引入方式直接决定了你后面踩坑的多少我一般按项目形态选引入方式适用场景注意点CDN 直接 script 引入快速验证、demo版本要锁死别用 latestnpm 安装pdfjs-dist现代打包项目Vite/Webpack要处理 worker 路径源码构建自定义需要裁剪体积、改默认行为构建成本高一般用不上绝大多数 H5 项目走第二种。pdfjs-dist是官方发布的 npm 包版本和 API 对得上比满世界找 CDN 靠谱。下面这段是 Vite 项目里的标准接法// 引入主库和 worker import * as pdfjsLib from pdfjs-dist; // worker 用 ?url 让打包器处理成独立资源避免路径 404 import workerUrl from pdfjs-dist/build/pdf.worker.min.mjs?url; // 关键一步指定 worker 地址不指定会退化到主线程卡到怀疑人生 pdfjsLib.GlobalWorkerOptions.workerSrc workerUrl; // 加载文档 const loadingTask pdfjsLib.getDocument({ url: /files/sample.pdf, // 关闭自动取字体移动端可减少请求 disableFontFace: false, }); const pdf await loadingTask.promise; console.log(总页数, pdf.numPages);逻辑说明GlobalWorkerOptions.workerSrc是必须配的pdf.js 把解析这种重活丢给 Web Worker主线程只负责绘制不配的话解析全压在主线程长文档直接卡死。参数上disableFontFace默认 false保持字体正常渲染如果你的 PDF 字体特别多又不在乎显示精度可以设 true 省点开销。getDocument还接受cMapUrl、cMapPacked用于中文等 CJK 字体的映射中文 PDF 乱码时基本就是这里没配。2.3 单页渲染的最小可用代码拿到pdf对象后渲染一页的完整流程如下async function renderPage(pdf, pageNum, canvas, scale 1.5) { const page await pdf.getPage(pageNum); // 根据设备像素比放大避免高清屏发虚 const dpr window.devicePixelRatio || 1; const viewport page.getViewport({ scale: scale * dpr }); const ctx canvas.getContext(2d); // Canvas 实际像素尺寸 canvas.width viewport.width; canvas.height viewport.height; // CSS 尺寸还原成逻辑尺寸否则会显示得巨大 canvas.style.width ${viewport.width / dpr}px; canvas.style.height ${viewport.height / dpr}px; const renderTask page.render({ canvasContext: ctx, viewport, }); await renderTask.promise; return renderTask; }逻辑说明这里最容易翻车的是 DPR 处理。Canvas 的width/height是绘制缓冲区尺寸style.width/height是显示尺寸两者不分开设高清屏上要么糊要么大得离谱。scale我一般给 1.5 起步配合 DPR 后清晰度够用再大内存吃不消。renderTask要留着页面切换时调cancel()不然上一页还在画新页已经上来会出现画面错乱。3. 翻页、缩放与手势把预览器做成能用的交互光能渲染一页不算完用户要的是能翻、能缩、能跳。这一章把交互层补齐也是 H5 场景下体验差距最大的地方。3.1 翻页与页码状态管理翻页本质就是换pageNum重新渲染。但直接换会闪常见做法是维护一个当前页状态配合上一页/下一页按钮和滚动监听let currentPage 1; const totalPages pdf.numPages; async function goToPage(num) { if (num 1 || num totalPages) return; // 取消上一页未完成的渲染任务 if (window.__currentTask) { window.__currentTask.cancel(); } currentPage num; const task await renderPage(pdf, num, canvasEl, 1.5); window.__currentTask task; // 更新页码显示 pageIndicator.textContent ${num} / ${totalPages}; } prevBtn.onclick () goToPage(currentPage - 1); nextBtn.onclick () goToPage(currentPage 1);逻辑说明cancel()那一步是血泪经验不取消的话快速连点会同时跑好几个渲染任务内存飙升低端机直接崩。页码状态单独存别从 DOM 里读读出来的是字符串还得转。totalPages从pdf.numPages拿别自己数。3.2 双指缩放与手势冲突处理移动端缩放有两种思路改scale重新渲染清晰但重或者用 CSStransform缩放 Canvas快但放大后糊。我一般折中——双指过程中用 CSS 缩放给即时反馈手势结束后按最终比例重新渲染一次。let pinchStartDist 0; let baseScale 1.5; canvasEl.addEventListener(touchstart, (e) { if (e.touches.length 2) { // 记录双指初始距离 pinchStartDist Math.hypot( e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY ); } }); canvasEl.addEventListener(touchmove, (e) { if (e.touches.length 2 pinchStartDist 0) { const dist Math.hypot( e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY ); const ratio dist / pinchStartDist; // 实时用 CSS 缩放限制范围防止失控 const visual Math.min(Math.max(ratio, 0.5), 3); canvasEl.style.transform scale(${visual}); } }); canvasEl.addEventListener(touchend, async () { // 手势结束按最终比例重渲染保证清晰 const match canvasEl.style.transform.match(/scale\(([\d.])\)/); if (match) { baseScale baseScale * parseFloat(match[1]); canvasEl.style.transform ; await renderPage(pdf, currentPage, canvasEl, baseScale); } pinchStartDist 0; });逻辑说明touchmove里只改transform不重渲染是为了跟手重渲染有延迟会顿。touchend里把视觉缩放折算进baseScale再重画一次兼顾流畅和清晰。缩放范围限制在 0.5 到 3 倍超出范围用户会迷失。注意touchmove要配合passive: false和preventDefault才能阻止页面整体滚动否则缩放手势会带着页面一起动。3.3 工具栏与常用功能挂载一个能用的预览器工具栏至少要有页码跳转、缩放加减、下载/打印入口。这些不复杂但要注意别把 pdf.js 的渲染和 UI 状态耦合死。我习惯把pdf、currentPage、scale收在一个模块级对象里UI 只读不写改状态统一走goToPage和setScale两个函数。这样后面加埋点、加水印、加批注都有地方插。4. 避坑与排查pdf.js 在 H5 上最容易翻车的五件事这一章全是实打实踩出来的按「现象 → 原因 → 解决」写遇到问题对着查。4.1 中文 PDF 显示成方块或乱码现象英文 PDF 正常中文 PDF 打开后文字全是方块或者干脆不显示。原因pdf.js 默认不带 CJK 字体的字符映射表CMap遇到用非标准编码的中文 PDF 就认不出来。解决在getDocument里显式指定cMapUrl和cMapPacked把官方提供的 cmaps 目录挂上去const pdf await pdfjsLib.getDocument({ url: /files/cn.pdf, cMapUrl: /cmaps/, // 把 pdfjs-dist/cmaps 拷到静态目录 cMapPacked: true, }).promise;cmaps目录在pdfjs-dist包里就有构建时拷到 public 下即可。这一步不做中文项目基本必翻车。4.2 跨域 PDF 加载失败现象控制台报Failed to fetch或CORS本地文件能开线上链接不行。原因pdf.js 用 fetch/XHR 拉 PDF 二进制跨域资源没带 CORS 头就被浏览器拦。解决让后端在 PDF 响应上加Access-Control-Allow-Origin或者走同源代理转发。别想着前端绕绕不过去。如果 PDF 是 base64 或 ArrayBuffer用getDocument({ data })直接传数据能避开跨域但要注意大文件转 base64 内存翻倍。4.3 长文档内存暴涨甚至崩溃现象几十页的 PDF 翻到后面越来越卡最后页面白屏或标签页崩溃。原因每页都创建 Canvas 且不释放加上渲染任务没取消内存只增不减。解决只保留当前页和相邻页的 Canvas其余页canvas.width 0; canvas.height 0主动释放切换页面前cancel()掉未完成的renderTaskscale别设太大1.5 到 2 之间够用。低端安卓机尤其要注意DPR 高的时候缓冲区尺寸是成倍涨的。4.4 worker 路径 404 导致主线程卡死现象页面能渲染但滚动、点击都卡长文档尤其明显。原因workerSrc没配或路径错pdf.js 静默退化到主线程解析。解决确认GlobalWorkerOptions.workerSrc指向真实可访问的 worker 文件。用打包器时通过?url或new URL(...)让构建工具处理路径别手写相对路径。上线后打开 Network 面板看 worker 文件有没有 200。4.5 iOS 上 Canvas 尺寸超限白屏现象iOS Safari 上大尺寸 PDF 渲染出来一片白安卓正常。原因iOS 对单个 Canvas 的像素面积有上限约 1670 万像素超过就不画。解决限制scale * dpr的乘积或者对大页面分块渲染。简单做法是给 viewport 算出来的宽高加个上限判断超了就降 scale。这个坑很隐蔽因为不报错就是白。5. 进阶技巧文本层、水印与按需加载的落地细节基础预览跑通后真正拉开差距的是这几个进阶点。先说文本层——pdf.js 除了画 Canvas还能生成一层透明的文本 DOM叠在 Canvas 上这样用户就能选中、复制、搜索 PDF 里的文字。做法是page.getTextContent()拿到文本项再配合pdfjsLib.renderTextLayer渲染。移动端如果不需要选中可以省掉这层能省不少 DOM 节点。水印我一般不用改 PDF 源文件直接在 Canvas 渲染完成后用ctx.fillText叠一层或者用一个绝对定位的 DOM 层盖上去。DOM 层的好处是能跟着缩放走坏处是截图能截到——真要防截图得在服务端做。按需加载则是把「只渲染可视区域」做扎实监听滚动容器算出当前视口对应第几页只渲染那一页和前后各一页其余页用占位 div 撑高度。这套组合下来几百页的 PDF 在手机上也能滑得动。验证方法很简单拿一份 200 页以上的中文 PDF在低端安卓机上从头滑到尾看内存曲线是否平稳、有没有白屏、文字是否正常。我习惯用 Chrome 远程调试连真机Performance 面板录一段重点看 JS heap 有没有持续爬升。爬升就是没释放回去查 Canvas 和 renderTask。从那以后我每次接 PDF 预览需求都强制先跑一遍这份检查清单worker 配没配、cmaps 带没带、DPR 处理对不对、cancel 有没有调、iOS 尺寸限没限。这五条过完基本就不会在上线后被打回。希望帮到你。本文还有配套的精品资源点击获取