纯前端 PDF 在线预览与打印实战:pdf.js 渲染、性能与避坑指南
简介这是一份基于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% 的兼容性和性能问题比翻页点半天高效得多。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

DNS、TCP套接字与P2P协同实现自主节点发现与NAT穿透

DNS、TCP套接字与P2P协同实现自主节点发现与NAT穿透

1. 这三个词放在一起,不是凑数,而是网络底层能力的黄金三角很多人看到“DNS、P2P、TCP套接字编程”这个标题,第一反应是:这像三门课的期末复习提纲——一个讲域名解析,一个讲去中心化通信,一个讲基础网络编…

2026/10/15 23:38:55 阅读更多 →
Unity场景标注指南:运行时点击任意位置添加可持久化文字备注

Unity场景标注指南:运行时点击任意位置添加可持久化文字备注

1. 需求缘起与方案定位做Unity开发久了,你会发现一个很常见的需求:在场景的某个位置加一个备注,写点提示信息。比如模型审查时在问题部位标记“此处碰撞体未对齐”,或者给策划看关卡时在某块区域写“这个平台将来放传送门”&#…

2026/10/15 23:38:55 阅读更多 →
光纤传感器参数全面仿真:建模、扫描与交叉耦合分析

光纤传感器参数全面仿真:建模、扫描与交叉耦合分析

做光纤传感研究最怕什么?答案很统一:参数太多,变量踩不完。数值孔径、纤芯半径、掺杂浓度、传感区长度、弯曲半径……随便哪个动一动,光谱就给你演一出“大变活人”。我做这个仿真课题的时候,最大的体会就是&#xff1…

2026/10/15 23:37:54 阅读更多 →

最新新闻

微电网调度MATLAB工程实现:双时间尺度MILP优化与不确定性建模

微电网调度MATLAB工程实现:双时间尺度MILP优化与不确定性建模

简介:本资源是一套面向电力系统方向本科生、研究生及科研人员的智能微电网调度算法MATLAB实现方案,聚焦可再生能源接入背景下微电网的经济性调度与稳定性优化问题。压缩包共20个文件,含18个核心MATLAB函数(.m)、1个备份…

2026/10/16 0:34:22 阅读更多 →
Windows SDK中国象棋:从双缓冲到Alpha-Beta的完整课设实战

Windows SDK中国象棋:从双缓冲到Alpha-Beta的完整课设实战

简介:基于Windows SDK的象棋程序完整工程源码包,面向C/C初学者及桌面游戏开发入门者,重点演示如何使用Win32 API与GDI实现棋盘绘制、棋子拖放、走棋规则判断和人机对弈。压缩包共38个文件,主要包含C源文件(.cpp)、头文件(.h)、图标…

2026/10/16 0:34:22 阅读更多 →
NC附件上传的本地保存与OSS上传架构设计

NC附件上传的本地保存与OSS上传架构设计

1. 项目概述:为什么“NC传输附件的本地文件保存和文件上传”是企业级系统绕不开的硬需求在某高校实验室参与一个跨部门协同管理平台开发时,我第一次被业务方拉着开了个紧急会——他们刚上线的NC(用友NCCloud)系统里,所…

2026/10/16 0:34:22 阅读更多 →
离散周期信号频谱分析:DFS原理、混叠与泄露的工程实战

离散周期信号频谱分析:DFS原理、混叠与泄露的工程实战

1. 从“周期”二字说起:为什么离散周期信号的频谱值得单独拎出来讲很多人学数字信号处理,学到“离散周期信号的频谱”这一节时,第一反应是——公式好多,但好像跟前面学的离散时间傅里叶变换(DTFT)差不多&am…

2026/10/16 0:34:22 阅读更多 →
Python安装与环境配置全指南:Windows/macOS/Linux从零到跑通项目

Python安装与环境配置全指南:Windows/macOS/Linux从零到跑通项目

简介:一份面向Python零基础学习者的图文安装指南,通过步骤拆解与截图对照,围绕Python解释器与PyCharm集成开发环境的完整搭建流程展开,帮助读者从官网下载、安装选项勾选、命令行验证到创建并运行第一个Python程序,解决…

2026/10/16 0:34:22 阅读更多 →
脑电信号模拟发生系统:为脑机接口测试打造可复现的基准数据

脑电信号模拟发生系统:为脑机接口测试打造可复现的基准数据

简介:面向脑机接口测试的高精度脑电信号模拟发生系统设计文档,聚焦医疗设备制造商、科研工作者与脑机接口开发团队的测试需求。方案针对传统信号发生器系统复杂、体积庞大、精度不足等问题,提出含数模转换、同相比例放大与信号衰减电路的模拟…

2026/10/16 0:33:22 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →