资讯动态

Paged.js 实战:用 CSS 分页媒体规范实现网页导出 PDF

发布时间:2026/10/15 17:47:45 来源:尧图企业网站定制
简介这是一份面向前端开发者的 paged.js 系统教程资料围绕如何将 HTML 内容转换为带分页效果的 PDF 展开适合希望掌握浏览器端 PDF 生成、CSS Paged Media 分页规则与打印样式适配的初中级开发者。内容涵盖库的安装引入、基础分页示例、页面尺寸与边距配置、自定义 page 规则、Grid 与 Flexbox 复杂布局处理、toBlob/toDataURL 输出下载以及与 React、Vue 等框架的集成和调试优化思路。资源包共 32401 个文件以 28814 个 png 图片、2918 个 js 脚本为主另含 otf、eot、woff 等字体文件及少量 html、svg、json、md 文档整体约 40.16MB多为示例站点与依赖资源。目前已有 585 人学习下载可帮助读者快速理解分页原理并落地到实际项目。1. 用 Paged.js 把网页变成能打印的 PDF一份写给前端的一线笔记如果你做过「网页导出 PDF」这个需求大概率经历过这样的场景浏览器打印预览里排版全乱分页位置诡异页眉页脚对不齐表格跨页断得莫名其妙。用后端方案吧又要维护一套完全独立的模板前端改个样式得同步改两遍。Paged.js 就是冲着这个痛点来的——它把 CSS 的 Paged Media 规范搬进浏览器让你用写网页的方式写打印稿同一套 HTML 和 CSS屏幕上能看打印出来也能看。这篇笔记面向的是需要落地「HTML 转 PDF」的开发者尤其是做报表、合同、电子书、试卷、发票这类有严格分页要求的场景。我会从 Paged.js 的核心机制讲起然后给出一套能直接跑通的最小工程再拆解分页控制、页眉页脚、页码、目录生成这些高频需求的具体写法最后把我在实际项目里踩过的坑一条条列出来。读完你应该能判断这个方案值不值得引入你的项目以及引入后怎么少走弯路。2. Paged.js 到底做了什么从 CSS 分页模型到浏览器渲染2.1 浏览器原生打印为什么不够用浏览器自带的打印能力本质上是把屏幕上的连续流式布局硬塞进纸张尺寸里。它只认media print里那点有限的样式page规则支持得残缺不全页码、页眉页脚、跨页断点控制基本靠猜。你写page-break-before: always有时候生效有时候不生效表格跨页时表头不会自动重复图片被拦腰截断也没法控制。Paged.js 的思路不是去修补浏览器打印而是绕开它在页面加载后用 JavaScript 读取你的 HTML 和 CSS按照 Paged Media 规范重新计算布局把内容切成一页一页的「纸张」DOM 结构再把这些纸张渲染到屏幕上。你看到的预览就是最终 PDF 的样子导出时直接对这个分页后的结构做打印或截图转 PDF。这个机制决定了它的能力边界凡是 CSS Paged Media 规范里定义的分页属性它基本都支持凡是依赖浏览器原生打印引擎的特性它可能不认。理解这一点后面很多「为什么这个属性没生效」的问题就都有答案了。2.2 最小可运行工程三行引入加一个配置块先跑通一个最小例子感受一下它的工作方式。新建一个 HTML 文件内容如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 titlePaged.js 最小示例/title !-- 引入 Paged.js 的 polyfill 脚本 -- script srchttps://unpkg.com/pagedjs/dist/paged.polyfill.js/script style /* 定义纸张尺寸和页边距 */ page { size: A4; margin: 20mm 18mm; } /* 屏幕预览时给纸张加个阴影方便看分页效果 */ .pagedjs_page { box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); margin-bottom: 16px; } body { font-family: Noto Sans SC, sans-serif; font-size: 12pt; line-height: 1.7; } /* 强制某段内容从新页开始 */ .chapter { break-before: page; } /style /head body section classchapter h1第一章 概述/h1 p这是第一章的内容Paged.js 会自动把它排到第一页。/p /section section classchapter h1第二章 细节/h1 p因为设置了 break-before: page这一章会从新的一页开始。/p /section /body /html这段代码里paged.polyfill.js是 Paged.js 的完整打包版本引入后它会自动扫描页面内容并执行分页。page规则里的size和margin是纸张级参数break-before: page是分页控制属性。打开这个文件你会看到内容被切成了两张带阴影的「纸」每张纸的尺寸和边距都按 A4 算好了。参数说明size可以写A4、A3、letter也可以写具体尺寸如210mm 297mmmargin支持四值写法顺序是上右下左。break-before的取值还有avoid尽量避免在此元素前分页、left、right强制分到左页或右页适合双面印刷。2.3 分页引擎的工作流程与关键钩子Paged.js 的分页不是一次性完成的它分几个阶段先解析 CSS 里的page规则建立页面模板然后遍历 DOM把内容按块级元素逐个「流」进页面当一页装不下时触发分页逻辑创建新的页面容器最后在所有页面渲染完成后触发rendered事件。这个流程里有两个关键钩子值得记住。一个是beforeRender在分页开始前触发适合做内容预处理比如给所有表格加上重复表头的类名。另一个是afterRendered在所有页面生成后触发适合做后处理比如给页码元素填值、生成目录。// 在分页开始前给所有表格标记需要重复表头 class TableHandler extends Paged.Handler { beforeRender(chunker, polisher, caller) { document.querySelectorAll(table).forEach(table { const thead table.querySelector(thead); if (thead) { // Paged.js 识别这个属性来决定是否重复表头 thead.style.display table-header-group; } }); } } Paged.registerHandlers(TableHandler);这段代码注册了一个自定义 Handler在渲染前把所有带thead的表格标记为表头重复组。table-header-group是 CSS 表格布局里的标准值Paged.js 在分页时会识别它让表头在每一页顶部重复出现。如果不加这个处理跨页表格的第二页开始就没有表头了阅读体验很差。3. 页眉页脚、页码与目录把打印稿的「标配」做出来3.1 用 page 边距框写页眉页脚Paged Media 规范里有一套「边距框」机制允许你在页面的上下左右边距区域放内容。Paged.js 实现了这套机制写法是在page规则里用top-center、bottom-right这类 at-rule。page { size: A4; margin: 25mm 18mm 20mm 18mm; /* 页眉居中显示文档标题 */ top-center { content: 项目验收报告; font-size: 9pt; color: #666; border-bottom: 0.5pt solid #ccc; width: 100%; padding-bottom: 4mm; } /* 页脚右侧显示页码 */ bottom-right { content: 第 counter(page) 页 / 共 counter(pages) 页; font-size: 9pt; color: #666; } }counter(page)是当前页码counter(pages)是总页数这两个计数器由 Paged.js 自动维护。边距框的位置关键字有top-left、top-center、top-right、bottom-left、bottom-center、bottom-right以及左右两侧的left-top、left-middle、left-bottom等。content属性里可以拼接字符串和计数器写法跟 CSS 的content一致。注意一点边距框里的样式是独立于正文的width: 100%是相对于边距框自身区域不是整页宽度。如果你想让页眉横线贯穿整个版心得在边距框里用border-bottom并配合合适的宽度设置。3.2 首页不同、奇偶页不同的页眉页脚正式文档经常要求封面不显示页眉页脚或者奇偶页的页码位置不同。Paged.js 支持用命名页面来实现。/* 默认页面有页眉页脚 */ page { bottom-center { content: counter(page); } } /* 封面页不显示页码 */ page cover { bottom-center { content: none; } } /* 给封面元素指定使用 cover 页面 */ .cover-page { page: cover; break-after: page; }page: cover这个属性把元素绑定到名为cover的页面模板上。page cover里把content设为none页码就不显示了。同样的思路可以做奇偶页定义page :left和page :right分别设置不同的页码位置。:left和:right是伪类选择器Paged.js 会根据页面在文档中的位置自动应用。3.3 自动生成目录从标题采集到页码回填目录是打印稿的刚需但 Paged.js 本身不提供目录生成功能需要自己写 Handler 来实现。思路分两步先采集所有标题元素和它们所在的页码再在目录区域渲染出带页码的列表。class TocHandler extends Paged.Handler { constructor(chunker, polisher, caller) { super(chunker, polisher, caller); this.tocEntries []; } // 每个页面渲染完成后记录该页上的标题 afterPageLayout(pageElement, page, breakToken) { const headings pageElement.querySelectorAll(h1, h2, h3); headings.forEach(h { this.tocEntries.push({ level: parseInt(h.tagName[1]), text: h.textContent.trim(), page: page.position, // 当前页码 id: h.id || }); }); } // 所有页面渲染完成后把目录填进占位容器 afterRendered(pages) { const tocContainer document.querySelector(#toc); if (!tocContainer) return; const html this.tocEntries.map(entry { const indent (entry.level - 1) * 16; return div classtoc-item stylepadding-left:${indent}px span classtoc-text${entry.text}/span span classtoc-page${entry.page}/span /div; }).join(); tocContainer.innerHTML html; } } Paged.registerHandlers(TocHandler);这段代码里afterPageLayout在每一页布局完成后触发此时能拿到该页的 DOM 和页码page.position。把所有标题收集起来后afterRendered在全部页面完成后统一渲染目录。目录项的缩进按标题层级计算h1不缩进h2缩进 16px以此类推。有个细节要注意目录本身也占页面如果目录在正文之前它的页码会影响后续内容的页码计算。常见做法是把目录放在一个独立的命名页面里用罗马数字编页码正文从阿拉伯数字 1 重新开始。这需要配合counter-reset来实现稍微复杂一些但原理就是把页码计数器在正文开始处重置。4. 分页控制与内容适配表格、图片、代码块的实战处理4.1 表格跨页表头重复与行断裂控制表格是分页里最容易出问题的元素。默认情况下一个长表格跨页时第二页不会重复表头而且某一行可能被从中间截断上半行在第一页下半行在第二页。table { width: 100%; border-collapse: collapse; /* 允许表格跨页断开 */ break-inside: auto; } thead { /* 表头在每页重复 */ display: table-header-group; } tr { /* 禁止单行内部断裂 */ break-inside: avoid; } td, th { border: 0.5pt solid #999; padding: 6pt 8pt; font-size: 10pt; }break-inside: avoid加在tr上保证一行不会被拆到两页。display: table-header-group加在thead上让表头在每页顶部重复。这两个属性配合使用基本能解决表格跨页的绝大多数问题。如果表格特别长还可以在tbody上设置break-inside: auto允许表格体自由断开。4.2 图片与代码块避免被拦腰截断图片和代码块这类「原子性」内容最怕被分页线切开。处理方式是在这些元素上设置break-inside: avoid。figure, pre, blockquote { break-inside: avoid; } img { max-width: 100%; height: auto; /* 图片底部留一点间距避免紧贴下一页顶部 */ margin-bottom: 8pt; } pre { background: #f5f5f5; padding: 10pt; font-size: 9pt; line-height: 1.5; /* 代码块过长时允许横向滚动不撑破页面 */ overflow-x: auto; white-space: pre-wrap; word-break: break-all; }break-inside: avoid告诉分页引擎如果这个元素在当前页放不下就整体挪到下一页不要从中间切开。对于特别长的代码块如果一页都放不下这个属性会失效引擎只能强行断开。这时候可以考虑把长代码拆成多个pre或者接受断开并在断开处加视觉提示。4.3 强制分页与避免分页的取舍break-before和break-after控制元素前后的分页行为取值有auto、page、avoid、left、right。实际项目里章节标题前用break-before: page强制新起一页标题后跟内容之间用break-after: avoid防止标题和正文被分开。h1 { break-before: page; break-after: avoid; } h2, h3 { break-after: avoid; } /* 某些短小的提示框不要单独占一页 */ .tip-box { break-inside: avoid; }这里有个容易忽略的点break-after: avoid加在标题上意思是「标题后面不要分页」但引擎是否遵守取决于后续内容能否在当前页剩余空间放下。如果标题后面跟了一个大表格表格放不下要挪到下一页那标题也可能被一起挪过去这是符合预期的。但如果标题后面是普通段落段落能放下标题就不会被孤立在页尾。5. 避坑与排查Paged.js 落地时最容易翻车的五个地方5.1 样式不生效先查选择器优先级和作用域现象写了page规则但页边距、页眉页脚完全没变化。原因Paged.js 在分页时会重新构建 DOM 结构你的样式如果依赖特定的父级选择器或body div这类结构可能在分页后匹配不到。另外page规则必须写在style标签里通过外部 CSS 文件引入时要注意加载顺序Paged.js 可能在样式加载完成前就开始分页了。解决把page相关样式放在head里靠前的位置确保在 Paged.js 脚本执行前已经解析。如果用了外部样式表给link加relstylesheet并确保它在脚本之前。调试时可以在浏览器控制台里检查.pagedjs_page元素的计算样式看page规则有没有被应用。5.2 页码错乱目录页和正文页的计数器冲突现象目录页用了罗马数字正文从 1 开始但正文第一页显示的是目录页的页码加一。原因counter(page)是全局递增的不会因为命名页面切换而自动重置。目录页占了 2 页正文第一页的counter(page)就是 3。解决在正文开始处重置页码计数器。可以用一个 Handler 在正文第一页的afterPageLayout里执行page.element.style.counterReset page 1或者用 CSS 的counter-reset配合命名页面。更稳妥的做法是给正文容器加一个类在afterRendered里遍历页面找到正文第一页后手动设置页码起始值。5.3 中文字体渲染异常字体加载时机与回退现象导出的 PDF 里中文变成方框或默认宋体跟网页上预览的字体不一致。原因Paged.js 分页时如果字体还没加载完它会用回退字体渲染。等字体加载完分页已经结束了不会自动重排。另外PDF 导出工具对字体的嵌入策略也会影响最终效果。解决在分页开始前确保字体已加载。可以用document.fonts.ready这个 Promise等它 resolve 后再初始化 Paged.js。如果用的是自定义字体在 CSS 里用font-face声明并设置font-display: block避免回退字体先渲染。// 等字体加载完成后再启动分页 document.fonts.ready.then(() { // 这里再动态引入 paged.polyfill.js 或调用 Paged.js 的初始化方法 });5.4 长内容分页性能差分块渲染与虚拟滚动现象文档超过 100 页时分页过程卡顿明显浏览器甚至无响应。原因Paged.js 默认会一次性把所有内容分页并渲染到 DOM 里页面数量多了以后DOM 节点数爆炸布局计算量急剧上升。解决对于超长文档考虑分块处理。把内容按章节拆成多个独立的 Paged.js 实例每个实例只处理一章最后合并 PDF。或者用 Paged.js 的chunker配置项调整分块大小。另一个思路是只在导出时启用 Paged.js预览时用普通滚动视图减少实时分页的开销。5.5 导出 PDF 的两种路径浏览器打印与截图转 PDF现象用浏览器打印对话框导出 PDF发现页眉页脚重复出现或者边距不对。原因浏览器打印时会加上自己的页眉页脚URL、日期、页码这些是浏览器行为不是 Paged.js 控制的。另外打印时的缩放比例、纸张来源设置也会影响最终输出。解决在打印对话框里把「页眉和页脚」选项取消勾选边距设为「无」缩放设为 100%。如果对 PDF 质量要求高可以用截图方案把每个.pagedjs_page元素用html2canvas或类似库转成图片再拼成 PDF。截图方案的好处是所见即所得坏处是文字不可选中、文件体积大。两种方案按场景取舍。6. 进阶技巧用 Handler 扩展 Paged.js 的能力边界Paged.js 的 Handler 机制是它最灵活的地方。除了前面用到的beforeRender、afterPageLayout、afterRendered还有beforePageLayout、afterParsed、beforeParsed等钩子覆盖了分页流程的各个阶段。你可以用 Handler 做很多内置功能之外的事情。比如给所有外部链接自动加上脚注标记在页脚显示链接地址。这个需求在打印稿里很常见因为纸上点不了链接。class LinkFootnoteHandler extends Paged.Handler { constructor(chunker, polisher, caller) { super(chunker, polisher, caller); this.linkIndex 0; } afterParsed(content) { const links content.querySelectorAll(a[href^http]); links.forEach(link { this.linkIndex; // 在链接后插入上标编号 const sup document.createElement(sup); sup.textContent [${this.linkIndex}]; sup.style.fontSize 0.7em; sup.style.color #0066cc; link.after(sup); // 把链接地址存到 data 属性供页脚使用 link.dataset.footnoteUrl link.href; }); } afterPageLayout(pageElement, page) { // 收集当前页上的链接脚注 const links pageElement.querySelectorAll(a[data-footnote-url]); if (links.length 0) return; const footnotes Array.from(links).map((link, i) { const num link.previousElementSibling.textContent; return ${num} ${link.dataset.footnoteUrl}; }).join( | ); // 把脚注插入页面底部的边距框区域 const footnoteArea pageElement.querySelector(.pagedjs_margin-bottom-center); if (footnoteArea) { footnoteArea.textContent footnotes; footnoteArea.style.fontSize 7pt; footnoteArea.style.color #888; } } } Paged.registerHandlers(LinkFootnoteHandler);这段代码在afterParsed阶段遍历所有外部链接给每个链接后面插入一个上标编号并把链接地址存到style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑