资讯动态

paged.js 实战:用 CSS 解决浏览器打印分页、页码与目录排版难题

发布时间:2026/10/15 22:57:36 来源:尧图企业网站定制
简介paged.js是前端领域的一款基于CSS Paged Media的HTML转PDF开源库可将连续网页内容拆分为规范的分页文档。这份教程面向希望在前端或客户端批量生成高质量PDF的JavaScript开发者系统介绍其核心概念、安装引入、基础分页、配置自定义、复杂响应式布局以及导出下载等方法并配套大量可运行示例与图文说明文档。资源以zip压缩包形式提供共32401个文件其中28814个png素材与2918个js脚本构成主体另有otf、eot、woff等多种字体文件用于保障分页后的样式渲染整体约40.16MB包内还包含MathJax相关字体与大量png页面素材便于对照分页效果。目前已有585人学习或下载过该教程。通过该教程可掌握用paged.js处理长文档分页、设置页面尺寸边距、导出blob并集成到工作流的完整流程对需要用网页快速生成PDF的开发者有直接参考价值。1. paged.js 是什么先搞懂它解决的是“分页”而不是“生成PDF”大部分开发者第一次被打印问题逼疯都是写模板时发现页码、页眉页脚、目录页码这些印刷排版里最基础的东西浏览器原生打印一个都不给你。paged.js 是一个把 CSS 分页媒体规范落到浏览器里的 JavaScript 库它做的不是“画一个 PDF 文件”而是先在屏幕上把你的 HTML 内容按页面尺寸重新分页再交给浏览器去打印或导出。它能解决页码自动计数、页眉页脚、章节强制换页、目录页码自动填充这些纯打印样式表搞不定的问题。适合三类人要交付 PDF 文档的前端开发者、正在做文档系统或报表打印模块的人以及所有被“为什么打印出来页码对不上”折磨过的从业者。2. 原理与选型为什么写打印样式表解决不了的页码问题它反而能解决2.1 分页媒体规范与浏览器打印的边界浏览器默认排版是流式的HTML 元素在一个连续画布上从上往下排遇到打印页面边界时直接“切一刀”。这刀切在哪里取决于页面高度和元素高度浏览器只对文本断行、段落断开这类基础行为有概念。但对于“当前是第几页”“总共有多少页”“页眉上的章节标题是什么”这种跨页面状态浏览器原生没有任何机制可以表达。CSS 里其实有一套专门描述这些能力的规范叫 CSS Paged Media分页媒体规范。它定义了page规则来代表一个物理页面定义了top-center、bottom-right这类“margin box”来放页眉页脚定义了counter(page)表示页码。问题是这套规范在浏览器原生打印里长期支持不全有些主流浏览器甚至到今天都不渲染 margin box。paged.js 的作用就是把这套规范在屏幕上模拟出来它读你的 HTML 和 CSS按page声明的尺寸重新排版把每一页的边界、页眉、页码、强制分页都算好再把你看到的分页结果交给打印。我一般会把这个过程理解成两层原生打印是“把长卷轴切成纸”paged.js 是“先按版面排好版再切成纸”。后者多了一步版面计算所以它能处理卷轴上根本不存在的信息比如页码。2.2 纯CSS打印样式表的边界在哪里写media print打印样式表能解决一部分问题隐藏侧边栏、把彩色界面调成黑白、调整字号、用page-break-before: always让每个章节从新页开始。这些都属于“对已有内容做打印适配”。但打印样式表天生处理不了三类需求第一状态类信息。页码是多少、总页数是多少这需要在整个文档排版完成后才能确定纯 CSS 没有跨页状态变量。第二引用类信息。目录里要填“第一章在第 3 页”这个第 3 页是另一个元素在排版后的位置需要跨元素查找打印样式表做不到。第三margin box。page { bottom-center { content: counter(page); } }这种写在页面边缘盒子里的内容在原生打印环境里长期不可靠有的浏览器直接忽略有的浏览器需要特定 flag 才支持。所以如果你的需求只是“打印出来别太丑”打印样式表够了如果你的需求是“出来的 PDF 页脚有页码、目录能自动填页码、封面不要页眉”纯 CSS 会卡在同一个地方它没有分页引擎。paged.js 补的就是这一层——它自己实现了分页引擎。2.3 与jsPDF/PDF-lib这类生成库怎么选经常有人问我要生成 PDF是不是直接用 jsPDF 更省事这类库和 paged.js 的定位完全不同。jsPDF、PDF-lib 是“在代码里用坐标画内容”你要自己把文字放在哪个页面、哪个位置都算清楚相当于用代码手绘 PDF。paged.js 是“让浏览器排版你只写 HTML 和 CSS”PDF 只是打印流程的副产品。我一般按三个条件判断如果内容本来就是 HTML 或 Markdown 渲染出来的 HTML且希望保持现有样式选 paged.js维护成本低得多。如果要从 API 返回的数据直接生成一个独立 PDF 文件没有 HTML 中间层选 jsPDF 这类。如果页面里有复杂表格、流程图、代码块且要求跨页不破版paged.js 因为复用浏览器排版能力处理这些远比手绘坐标省力。两者的工作方式差异也决定了排错路径不同。jsPDF 出问题你要查坐标和计算逻辑paged.js 出问题你查的是 CSS 规则这更接近前端工程师的直觉。对比项paged.jsjsPDF / PDF-lib输入HTML CSSJavaScript 代码排版浏览器自动排版手动计算坐标页码/目录CSS 声明式生成手动计算后写入适合场景已有 HTML 文档、打印模板数据直出 PDF、绘图维护成本改样式即可改排版逻辑3. 快速接入从引入库到跑通第一个分页页面3.1 最小接入自动模式跑通paged.js 提供了两种使用方式第一种是自动模式适合静态页面和快速验证。下载paged.polyfill.js放进项目里在 head 中引入页面加载完成后它自动接管排版。你不需要写初始化代码。!DOCTYPE html html langzh-CN head meta charsetUTF-8 / title合同打印页/title link relstylesheet hrefprint.css / script src./lib/paged.polyfill.js/script /head body h1模拟项目X服务合同/h1 p合同正文内容……/p /body /html这段代码的要点是把paged.polyfill.js放在页面 head 里让它能在 DOMContentLoaded 后尽早扫描文档。配合的 CSS 至少要先声明一个页面尺寸否则它会按默认 A4 处理page { size: A4; margin: 20mm; }size指定页面物理尺寸margin指定页边距这两个参数决定了后续所有分页计算的基础。如果margin不写paged.js 会按浏览器默认页边距计算但那样页眉页脚的 margin box 就没有落点所以第一步务必把page规则写完整。3.2 在构建项目里用模块方式接入如果你的页面是 SPA或者内容要在某个按钮点击后才出现自动模式不够用因为 polyfill 在文档加载时就跑完了动态渲染出来的内容它不会管。这个时候用模块方式手动控制渲染时机npm install pagedjsimport { Preview } from pagedjs; const preview new Preview(); preview .render(#report) .then(() { console.log(分页渲染完成); });render方法接收一个选择器把目标元素的内容作为文档进行分页排版。它也可以接收一个 URL 字符串此时会把远程页面加载后再分页。返回的是一个 Promise在渲染完成后 resolve。这个模式比自动模式多了一层控制权——你可以决定“什么时候开始分页”这对接异步数据加载场景很重要。3.3 第一批要掌握的CSS规则页面尺寸、边距、强制分页跑通最小接入后你需要的核心规则其实就那么几个。第一个是page里的size和margin上面已经写了。第二个是强制分页新的规范里用break-before替代了旧的page-break-before.chapter { break-before: page; } table { break-inside: avoid; }break-before: page表示这个元素之前强制插入一个分页符等于让每个章节从新页开始。break-inside: avoid表示这个元素内部尽量不被切开适合包住表格、卡片、图片这类不能破版的块。paged.js 对新的break-*属性支持比旧的page-break-*更好我一般建议直接用break-*不要在旧写法上纠结。4. 把页面调出印刷级效果页眉页脚、页码、目录的一次性打通4.1 page规则与命名页不同章节用不同版面一个文档往往需要多种版面封面页不要页眉页脚、正文页要有页码、章节首页可能需要更大的上边距。CSS Paged Media 规范里的命名页机制可以做到这一点——先定义一个命名的page规则然后通过page属性让某个元素归属到那个页面类型page { size: A4; margin: 20mm; } page cover { size: A4 portrait; margin: 0; } section.cover { page: cover; }page cover定义了一套独立的页面参数section.cover的page: cover告诉分页器这个元素所在的页面使用cover这一套版面规则。封面页把边距设为 0视觉上是出血效果但要注意页眉页脚是挂在页边距盒子上的边距为 0 时top-center这类 margin box 不会显示。如果你需要“封面无页码、正文从第 1 页开始”还要配合counter-reset重新初始化页码计数器否则封面会占掉一个页码位。命名页用在章首图、分隔页、附录页都很方便。我一般会把规则拆成三组默认页、封面页、章节首页各管各的边距和页眉避免在一个page里写一堆条件判断那样维护到后面很容易乱。4.2 页码与总页数counter(page)和counter(pages)页码是打印排版里最基础的需求也是原生打印最不给力的地方。paged.js 支持 CSS Paged Media 里的两个计数器counter(page)表示当前页码counter(pages)表示文档总页数。写法如下page { bottom-center { content: counter(page) / counter(pages); } }bottom-center是 margin box表示页面底部居中区域。content属性把页码和总页数组合成“1 / 12”这样一段文本中间用字符串拼接。margin box 一共有六个位置可用top-left、top-center、top-right、bottom-left、bottom-center、bottom-right。如果想页眉右侧放章节名、页脚居中放页码就分别写两个 margin box 规则。有一个细节要注意counter(pages)的值要在整个文档排版完成后才能确定。paged.js 内部会在所有内容分页结束后再回填这个值所以不用担心“第一次排版时还不知道总页数怎么办”。但这也意味着如果你手动改了 DOM 内容必须重新触发渲染否则总页数不会更新。4.3 目录页码自动填充target-counter的用法目录页是分页排版里第二个大痛点。传统做法是写死章节和页码但内容一旦增减页码全部错位。paged.js 实现了 GCPM 规范里的target-counter可以直接引用文档中另一个元素的页码nav idtoc a href#chapter-1第一章 绪论/a a href#chapter-2第二章 方法/a /nav section idchapter-1 h1第一章 绪论/h1 /section#toc a::after { content: target-counter(attr(href), page); float: right; }target-counter接收两个参数第一个是目标元素的引用这里用attr(href)取出a的href值作为选择目标第二个是计数器名称page表示目标元素所在的页码。这条规则会在目录链接后面生成一个数字表示该章节实际排版后的页码。float: right让页码靠右对齐形成目录常见的“标题居左、页码居右”版式。注意target-counter是 paged.js 渲染后才有效浏览器原生打印不认这个属性。所以你判断“目录页码对不对”一定要在 paged.js 渲染完成后看而不是直接在浏览器的打印预览里看。5. 避坑五个让排版翻车的常见问题与排查5.1 表格或卡片被硬切页面底部一整块空白现象一个表格恰好跨在页面边界上表格被从中间切开下半页出现一块没有内容的空白区域。 原因分页器默认只按页面高度切分内容它并不知道哪些盒子“不应该被切开”。表格、卡片、带边框的容器如果没有明确声明都会被当成普通流内容处理。 解决给不可切分的容器加break-inside: avoid同时给段落设定widows和orphans控制断行位置table, tr, .card { break-inside: avoid; } p { widows: 3; orphans: 3; }widows控制段落被切断时下一页顶部至少要保留多少行orphans控制上一页底部至少要保留多少行。设成 3 是印刷排版里比较常见的取值能减少孤行出现在页首或页尾的丑态。5.2 页眉页脚在浏览器打印预览里不显示现象CSS 里写了page { bottom-center { content: counter(page); } }屏幕上 paged.js 渲染的预览里页码是正常的但点开打印对话框预览里的页码消失了。 原因paged.js 的分页结果是在屏幕上渲染出来的一个“模拟页面”它已经把页码画进了那几块模拟纸里。但浏览器的打印对话框是重新走一遍原生打印流程不会读取 margin box所以看到的结果不一样。这个坑的根源是把“看预览”和“打印”两个动作搞混了。 解决页眉页脚是否正确的唯一判断标准是 paged.js 渲染后的屏幕结果。需要导出 PDF 时也用打印对话框的“另存为 PDF”功能但要以屏幕上 paged.js 分页后的效果为准。另外把打印样式直接写在样式表里不要包在media print条件里否则 paged.js 在屏幕上运行时根本不会读取那些规则。5.3 中文文档分页后目录页码对不上、总页数飘忽不定现象同一份文档多刷新几次目录页码有时对、有时错几页或者中文字体下某段落最后一行被挤到下一页导致后面所有页都顺延。 原因字体加载完成前paged.js 就开始排算了。中文字符宽度和西文字符宽度差异大字体没加载时用的是系统字体度量排版结果自然和最终字体不一致。这是字体的异步加载问题不是分页器的逻辑问题。 解决渲染前强制等待所有字体加载完成async function renderDocument() { await document.fonts.ready; const preview new Paged.Preview(); await preview.render(#report); }document.fonts.ready是一个 Promise在所有字体加载完成后 resolve。把渲染动作放在它后面可以保证分页计算用的是最终字体度量。如果你的页面里有自定义字体比如用font-face引入的中文字体这一步是必写的否则总页数会“漂移”。5.4 大图跨页被裁切出现半张残图现象一张高度接近页面底部的图片在分页时被从中间切开上一页底部是半张图下一页顶部是另外半张。有的图片设置了固定高度分页器想避让也避让不了。 原因img本身是替换元素在很多渲染环境下break-inside: avoid写在img上不生效需要包装它的父级容器来承担这个属性。另一个常见原因是图片设了固定高度或height: 100%超出了版心高度规则上无法完整容纳。 解决给图片包一层容器在容器上设置break-inside: avoid图片本身用max-height而非固定高度figure classimage-wrap img srcchart.png alt统计图表 / /figure.image-wrap { break-inside: avoid; } .image-wrap img { max-height: 180mm; width: auto; }max-height: 180mm是给图片活性空间图片自动按比例缩放不会超出单页承载能力。固定height是这里最大的坑分页器对固定高度元素没有避让余地。5.5 内容更新后重新导出页码还是上一版的现象第一次点击“导出 PDF”页码正确修改了正文内容后再点页码还是旧值或者总页数多出一页空白。 原因Preview实例保留着上一次的分页结果重新渲染时旧的分页 DOM 或者旧的计算状态没有被及时清理。 解决每次渲染前销毁旧实例用一个新实例来完成排版let currentPreview null; async function exportPdf() { currentPreview null; currentPreview new Paged.Preview(); await currentPreview.render(#report); window.print(); }把currentPreview置空让旧实例进入可回收状态再新建实例重新渲染。这种“先置空再新建”的做法比在实例上找什么重置方法更省事也避免把上一次的页面状态带进新排版。6. 进阶技巧用Preview手动控制渲染时机导出稳定复现的PDF到这一步你已经能把一版带页码、带目录的文档稳定导出。最后分享一个我一直在用的细节把“渲染”和“打印”完全拆开并对渲染加两道关卡。第一道关卡是字体。前面避坑里说过字体加载完再渲染。第二道关卡是图片。图片没有加载完就排版会导致图片区域高度变成 0后续所有内容全部上移页码整体错位。这两道关卡都要在调用render之前完成async function exportDocument() { await document.fonts.ready; const images Array.from(document.images); await Promise.all( images.map( (img) img.complete ? Promise.resolve() : new Promise((resolve) { img.addEventListener(load, resolve, { once: true }); img.addEventListener(error, resolve, { once: true }); }) ) ); const preview new Paged.Preview(); await preview.render(#report); window.print(); }这段代码里img.complete检查图片是否已经加载过已加载的直接跳过未加载的注册load和error两个事件监听任何一方触发都resolve避免某张图片挂了导致整个流程卡死。等图片和字体都就绪后再执行render保证分页计算输入的是完整内容。渲染完成后调window.print()是导出 PDF 的一种常见做法它会打开系统打印对话框由用户选择“另存为 PDF”。如果你要做全自动导出可以再往下接一层无头浏览器打印通过脚本化浏览器打开这个页面触发打印事件直接得到 PDF 文件。但无论走哪条路核心都是让 paged.js 在确定的状态下完成排版。我现在的习惯是每次改完打印样式都强制走一遍这条链路刷新页面、等字体、等图片、渲染、人工翻一遍每个分页边界。有一次某项目的合同模板里一个表格忘了加break-inside: avoid导出的 PDF 里出现整整一页白页客户拿到后以为模板坏了。从那以后我每次打开打印预览第一件事就是检查有没有不该出现的空白页再检查目录页码和实际页码是否一致。这个流程看起来多花一分钟但比起在一个“看起来正常”的 PDF 上返工划算得多。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑