资讯详情

paged.js实战:用HTML+CSS实现可控分页与PDF打印排版

📅 2026/10/25 19:07:57 | 华诺云谱 👁 阅读
paged.js实战:用HTML+CSS实现可控分页与PDF打印排版
简介《pdf:关于paged.js的教程》是一份面向前端开发者的实用资料聚焦于使用 paged.js 将 HTML 内容高效转换为 PDF 并实现优雅分页。教程覆盖库的定位、安装引入、核心配置、复杂布局适配、导出与下载、Vue/React 等场景集成以及浏览器调试与优化技巧适合需要实现客户端打印、动态生成 PDF 报表或合同文档的 JavaScript 开发者。压缩包共 32401 个文件容量约 40.16MB以 PNG 截图/演示图片为主另有约 2918 个 JS 脚本以及 otf/eot/woff 等字体资源和 HTML/CSS 示例页面paged.js 本身高度依赖 CSS Paged Media 规范资源中的字体文件多为排版渲染所需整体结构便于对照阅读与二次使用。目前已有 585 人浏览学习。借助这套资料读者能快速掌握从普通网页到分页 PDF 的完整链路包括如何自定义页面尺寸、边距与打印样式如何处理 Grid/Flex 等复杂布局并将成果集成到真实项目中。1. paged.js 是什么为什么用 HTMLCSS 做 PDF 而不是直接打印做 web 页面 PDF 打印的人多半都遇到过这种场景页面在浏览器里看着挺正常一点打印预览表格被腰斩、标题孤零零落在页尾、页码从 1 开始但第一页是封面。paged.js 就是把这个烂摊子接过去的库——它把 CSS Paged Media 标准实现到浏览器里让开发者用 CSS 规则控制分页、页边距、页眉页脚和页码最终输出一份像正式排版过的 PDF 文档。它不是编辑器不是转换工具而是一个运行时渲染引擎。适合谁适合那些需要把动态生成的 HTML 报表、电子合同、课程讲义、产品手册稳定转成 PDF 的从业者。你不需要重新学 LaTeX也不需要抱着 iText7 写 Java 代码只要还在写 HTMLCSS就能把分页这件事从「玄学」变成可调的参数。2. 跑通最小示例把一份带样式的 HTML 变成分页 PDF2.1 引入 paged.js 的两种方式paged.js 最常见的用法是作为 polyfill 运行在浏览器里。先去 npm 装包或者直接用 CDN 引入编译好的文件二选一。我一般推荐用 npm 安装方便锁定版本也方便后续接自动化测试。命令如下npm install pagedjs --save装完之后在你的入口 HTML 里引入 polyfill 脚本。要注意这里不是普通的script引入因为 paged.js 需要在 DOM 加载完成后接管页面。常见做法是放在 body 末尾script src/node_modules/pagedjs/dist/paged.polyfill.js/script然后在页面加载完成后触发预览window.addEventListener(load, () { window.PagedPolyfill.preview(); });preview()是核心方法它会读取当前文档的所有样式重新计算分页并在页面中插入生成的分页符和页边距盒。如果你只是想在浏览器里看效果这一段就够用了。如果不用 npm也可以从 unpkg 或 jsdelivr 拿编译产物但要注意引入路径里的paged.polyfill.js是浏览器可直接执行的版本另一个paged.js是模块化版本不能直接在非 module 场景里用。很多新手在这里踩坑引了paged.js结果报PagedPolyfill is undefined。2.2 理解分页机制CSS Paged Media 与浏览器打印的差别先弄清 paged.js 到底改了什么。原生浏览器打印也支持page但支持度很有限无法精确控制页眉页脚、页码位置、跨页时的断点规则。px 和 pt 的换算、默认边距、是否打印背景色每个浏览器都有自己的“小九九”。paged.js 做的事情是把 CSS Paged Media Level 3 规范里定义的page、top-center、bottom-right这些规则用 JavaScript 模拟实现然后生成一个分页后的渲染结果。这里的关键认知是paged.js 不是把 HTML“画”到 PDF而是让浏览器先按打印媒体重新排版再把排版结果交给打印引擎。所以你在屏幕上看到的预览其实是它动态生成的一层分页视图每个“页面”是一个独立的区块背景是白色四周是模拟的纸张边缘。这跟直接把网页扔进window.print()是两回事——前者每个页面元素都被约束在页面框内后者只是简单地把流式文档切分。最小示例到这里已经能跑通了。你可以先写一份简单的 HTML放一个标题和几段正文引入 polyfill调用preview()然后在浏览器里看效果。第一次跑通你会看到页面右侧出现滚动条每一页底部有灰色页码指示——这个页码不是输出到 PDF 里的页码只是预览时的辅助标记。真正的页码需要自己写 CSS 规则下一章会说。3. 页面布局核心尺寸、边距、页眉页脚与页码设置3.1 用 page 规则控制页面盒子paged.js 对page的支持比浏览器原生打印完整得多。你可以像下面这样定义页面尺寸、边距和页面背景page { size: A4; margin: 2cm 1.5cm; background: white; }size除了A4还支持A3、letter或是直接用长度单位21cm 29.7cm。注意它接收的是两个值第一个是宽度第二个是高度。如果你只写一个值默认是正方形页面这经常被忽略。margin用的单位你可以习惯性写cm或mm这在打印场景下比px更直观。真正需要注意的是margin和padding的区别page里写的是页边距页面内容会被约束在边距内部而如果你想给每一页加一个背景色或边框应该用page { background: ... }吗不行page的background只在部分浏览器生效。更稳妥的做法是把背景画在html或body上paged.js 会把背景铺到每个页面。如果你需要首页不同可以用伪页面page :first { margin-top: 3cm; } page :left { margin-left: 2.5cm; margin-right: 1.5cm; } page :right { margin-left: 1.5cm; margin-right: 2.5cm; }:left和:right对应书籍的双页排版奇偶页边距镜像。这个在打印电子书、双面文档时特别有用。但要注意paged.js 对:left/:right的判定是基于最终分页结果的页码如果你在预览前还没分页这些规则不会生效。页面盒子还有个容易被忽略的属性bleed。这是印刷用语指裁切出血线。如果你在生成的是需要交付印刷厂的 PDF需要加上page { size: A4; bleed: 3mm; }paged.js 会在页面四周留出 3mm 的出血但前提是你的内容背景已经延伸到那个区域。这个细节不做印刷的人可能一辈子用不上但遇到一次就知道有多救命。3.2 页眉页脚与页码counter(page) 和 position: running()页眉页脚是 paged.js 最值得吹的功能。原生浏览器打印想做“第 X 页 / 共 Y 页”基本要靠浏览器扩展而 paged.js 里只是几行 CSSpage { bottom-center { content: counter(page) / counter(pages); font-size: 9pt; color: #666; } top-left { content: 内部资料; font-size: 8pt; } }这里bottom-center是页面边距盒子的名称它定义在page规则内部。paged.js 支持顶部、底部、左侧、右侧各三个位置top-left、top-center、top-right、bottom-left、bottom-center、bottom-right再加上左右边距中的left-top、left-middle、left-bottom之类。常用的是顶部和底部那六个。counter(page)是当前页码counter(pages)是总页数。这两个计数器在分页完成后才会被正确填充所以不要在普通流式内容的 CSS 里引用它们只能在边距盒子内部用。如果页眉里想显示当前章节标题paged.js 提供了position: running()机制。先给章节标题定一个名字然后在页眉里引用h1 { string-set: chapter content(text); } page { top-center { content: string(chapter); } }这里的string-set属性会把h1的文本内容绑定到名为chapter的字符串之后在页面边距盒子里用string(chapter)读取。实际效果是每进入一个新章节页眉自动变成当前章节标题。这个特性做技术文档、合同模板时非常实用比如每页顶部显示当前合同条款的小节名。需要注意string-set不支持所有 CSS 取值函数想取元素的属性值、连接多个文本语法上有兼容坑。最稳妥的方式是取content(text)它就是元素的纯文本内容。页码从哪开始也是高频需求。封面不想显示页码或者正文从第 1 页开始。paged.js 里可以通过counter-reset结合命名页来做到.cover { page: cover; } page cover { bottom-center { content: none; } } .content { page: main; } page main { bottom-center { content: counter(page); } }但这里有个坑page: cover只是给元素指定了页面类型并不会让页码归零。要重置页码需要额外写page main { counter-reset: page 0; }不过这个重置方式在不同的 paged.js 版本里行为不一致有人用了有效有人发现页码从 1 继续。我一般更建议在封面的最后一个元素上手动设置break-after: page然后用counter-reset在内容区第一页重置。实践证明越是复杂的页码逻辑越需要把每一页的页面类型定义清楚而不是依赖隐式的连续分页。4. 内容分页控制标题、表格、图片怎么避免被拦腰截断4.1 break-before / break-after / break-inside 的选型分页控制的核心就是三个属性break-before、break-after、break-inside。它们分别控制元素之前、之后、内部是否允许分页。要注意page-break-before那套老属性在 paged.js 里也能用但新标准推荐用break-*两者混写时以新属性为准。实际项目里最常用的几个场景h1, h2 { break-after: avoid; } table, pre, figure { break-inside: avoid; } .new-page { break-before: page; }break-after: avoid的意思是标题后面不要紧跟分页符保证标题和它后面的第一段内容在同一页。这个规则能直接消灭“标题孤行”——标题在一页末尾正文在下一页开头。很多新手会写成page-break-after: avoid但其实老规范里page-break-after的avoid值支持度很差换成break-after更可靠。break-inside: avoid则用于防止表格、代码块、图片被切到两页。但要注意这个属性不是万能的当元素高度超过一整页时avoid无法生效浏览器必须允许它断开否则内容会被截断丢失。所以遇到超长表格你需要反过来处理下一节细说。如果想要“这个标题必须从新页开始”用break-before: page。注意 paged.js 里这个值是大小写敏感的写了Page或PAGE都无效。分组控制也有妙用。当你有一组卡片或条目希望保证至少两条在一起时可以这样.item { break-inside: avoid; } .item-container { break-before: avoid; }break-before: avoid表示本元素尽量不要作为一页的开头。这样两条.item出现在同一页的概率更高。这里要理解break-before和break-after是作用于两个元素之间边界的它们的avoid值会让分页算法尽量把断点移开这个边界。4.2 表格跨页时怎么保留表头与重复列表格是 PDF 生成里最让人头疼的部分。短表格用break-inside: avoid直接锁住但长表格一旦超过一页必须允许分割。分割时有两个刚需表头要重复行不能从中间劈开。paged.js 支持thead跨页重复显示但你需要显式地把border和background写到th上而不是写到table上因为多数浏览器对table级背景的跨页重复支持很弱。示例table { width: 100%; border-collapse: collapse; } th { background: #f0f0f0; border-bottom: 1px solid #999; } tr { break-inside: avoid; }tr { break-inside: avoid; }能保证一行不被切成两半压在页缝上。这是必须写的否则打印出来会看到某行文字上半截在页底、下半截在页顶。对于有多列且想要每页左侧重复项目名称的表格paged.js 本身没直接提供“重复列”的功能但可以用 CSS 伪元素模拟将第一列单元格的内容设置为position: running()绑定到一个字符串然后在页边距里显示。不过这东西实现起来很绕我一般更推荐在导出前改变表格结构——把重复列的数据放到thead里或者对表格做横向拆分。实际业务里横向超宽的表格更适合转成纵向堆叠的卡片式排版早转换早省心。图片跨页的问题相对简单。一张大图如果落在页缝附近会有两种表现被整体挪到下一页留下大面积空白或者被裁掉一部分。控制方式figure { break-inside: avoid; max-height: 100%; overflow: hidden; }max-height: 100%很关键它限制图片容器高度不超过当前页内容区。但要注意100%是相对于父元素高度如果你的图片在正文里父元素高度是无限的这个百分比会失效。更稳的做法是给图片设一个固定的最大高度比如针 A4 内容区高度 24.7cm那就写max-height: 24cm留一点余量。别写100%在分页上下文里它经常是无效值。5. paged.js 实战避坑5 个高频问题与排查方法5.1 现象打印预览里样式全部丢失只剩裸文本原因最常见是把media print里的样式误写成了media screen或者引入样式表时没有让 paged.js 在样式加载完成后才初始化。paged.js 是在当前文档样式基础上计算分页的如果preview()调用时样式表还没加载完它就会拿一份无样式 DOM 去分页生成的结果自然没有排版。解决在window.onload里调用preview()确保所有样式表和图片资源加载完毕。如果依然丢样式打开 DevTools 看document.styleSheets里有没有你的样式规则重点检查 CSS 文件是否因为跨域问题被浏览器拦截。多数情况是本地调试时用file://协议打开页面导致样式表跨域失败换成http://localhost就正常了。5.2 现象页码不显示或从第 0 页开始原因.preview()默认在页面内部生成分页视图但counter(page)这个计数器是在分页渲染时才建立的如果你把页码写到了普通内容元素的content里而不是page内部的边距盒子就不会生效。还有种情况是你在bottom-center里写了counter(page)但打印机的默认边距覆盖了页边距盒子导致页码被裁掉。解决检查你的page边距是否足够容纳页脚。比如margin-bottom: 1cm而页脚内容高度0.5cm应该没问题但如果margin-bottom: 0页脚就会跟正文重叠或者消失。把边距设大一些比如margin: 2cm先排查空间问题。页码从 0 开始则是因为某些浏览器 CSS 计数器默认从 0 计数paged.js 做了兼容但如果你手动写过counter-reset: page 0就会出现第一页页码是 0。删掉多余的重置或者改成counter-reset: page 1在非标准下有效测试你自己的版本。5.3 现象页面大小总差 1~2px导致出现空白页原因page { size: A4; }定义了页面尺寸但浏览器打印时默认缩放不是 100%或者系统显示缩放比例不是 100%导致 paged.js 计算出的页面高度与打印机实际渲染高度不一致结果内容溢出到多出的空白页。解决在调用preview()前强制设置文档的视口缩放。常见做法是PagedPolyfill.preview(() { document.documentElement.style.zoom 100%; });如果不行检查是否有transform: scale()在 body 或某个容器上。另一个原因是margin使用了小数单位比如1.2cm在像素换算时四舍五入导致误差。把 margin 取整到mm级别比如12mm能减少这类问题。这个坑在 Windows 高分屏下尤其明显血泪经验宁可全部用mm也不用cm因为cm与 px 的换算系数在不同系统里有差异。5.4 现象动态内容渲染完但分页没更新原因你的表格或数据是 AJAX 异步加载的内容插入 DOM 后没有重新触发分页。preview()只执行一次不会观察 DOM 变化。解决在数据更新完成后手动重新调用preview()async function updateData(newHtml) { document.getElementById(content).innerHTML newHtml; await window.PagedPolyfill.preview(); console.log(分页完成); }如果频繁更新先销毁老的分页再重建避免内存泄漏if (window.PagedPolyfill) { window.PagedPolyfill.afterPreview((flowed) { ... }); }更稳妥的做法是把动态渲染和分页完全分离先渲染内容到隐藏的 iframe等 iframe 加载完成后再把 iframe 的 body 通过document.adoptedStyleSheets或直接移动到主文档最后调一次preview()。paged.js 对动态插入的样式表支持不够好很多奇怪的错位都源于运行时加 style 标签但没触发重新计算。5.5 现象中文字体导致页面溢出原因浏览器对中文字符的换行行为与英文不同paged.js 在计算行高和断行时如果碰到长中文词比如连续数字、URL不会自动断行导致内容宽度超过页面容器撑破布局下一页出现多余溢位。解决给全局样式加body { word-break: break-all; line-height: 1.6; }用word-break: break-all强制任意字符间可断开适合中文和英文混排。如果你担心英文单词被拆得太碎可以用overflow-wrap: break-word替代但对防止溢出中文来说break-all更有效。另外中文字体的行高设定也很关键paged.js 在计算“剩余空间”时按 line-box 高度计算如果你的 font-size 和 line-height 比例过大页面底部会多出一条放不下的行预留边距时要把行高算进去。经验值内容区高度减去最后一行行高才是真正能安全放入文字的高度。6. 进阶把 paged.js 接进自动化 PDF 生成管线6.1 用 Puppeteer 无头浏览器渲染并导出 PDF在浏览器里调试好分页效果后最终要输出 PDF 文件通常的做法是交给 Puppeteer或 Playwright无头浏览器加载页面调用page.pdf()落地。这里有个关键参数必须禁用preferCSSPageSize吗恰恰相反要让 Puppeteer 使用你的page设定需要这样传参const puppeteer require(puppeteer); (async () { const browser await puppeteer.launch(); const page await browser.newPage(); await page.goto(http://localhost:8080/report.html, { waitUntil: networkidle0 }); await page.evaluate(async () { await window.PagedPolyfill.preview(); }); await page.pdf({ path: output.pdf, format: A4, printBackground: true, preferCSSPageSize: true }); await browser.close(); })();preferCSSPageSize: true让 Puppeteer 优先采用page里定义的尺寸否则会以format参数为准。两个地方都写 A4 最保险。printBackground: true是必须的否则表格表头背景色、页面背景全消失。这段代码里有两个等待goto的networkidle0等所有资源加载preview()等分页计算完成。少一个都会出现空页或不完整。6.2 校验分页结果如何快速检查页面边界与元素裁剪自动化生成的 PDF 不能只靠肉眼一个个翻页检查。我习惯在调试阶段给页面加辅助标尺media print { .pagedjs_page { outline: 1px dashed #ccc; } }paged.js 会给每个分出的页面加.pagedjs_page类名利用这个类你可以写脚本来检查元素是否越界。例如在 Puppeteer 里const overflow await page.evaluate(() { const pages document.querySelectorAll(.pagedjs_page); let results []; pages.forEach((pg, i) { const rect pg.getBoundingClientRect(); const children pg.querySelectorAll(*); children.forEach(el { const r el.getBoundingClientRect(); if (r.bottom rect.bottom 1 || r.right rect.right 1) { results.push(第${i1}页: ${el.tagName}.${el.className} 溢出); } }); }); return results; }); if (overflow.length) console.log(overflow);这个脚本能精准输出“第几页的哪个元素溢出”。它抓的是几何越界但要注意某些伪元素、绝对定位的装饰性元素可能故意溢出你需要排除它们。我一般在生产前跑一遍看到溢出列表就按图索骥去修 CSS。还有一个验证技巧生成 PDF 后用pdf-lib或其他库读取页数、页宽高确认总页数与你的预期一致再抽查第二、最后几页的文字是否出现在正确位置。这一步能拦截大量因字体加载延迟导致的分页错乱——字体在 preview() 之后才加载完浏览器重新排版但 paged.js 没收到通知最终 PDF 和预览不一致。解决办法是在调用preview()前用document.fonts.ready等待字体就绪await document.fonts.ready; await window.PagedPolyfill.preview();这个细节救过我很多次。现在我做 PDF 生成已经养成了固定习惯先本地起服务再用 Puppeteer 跑一份带调试标记的 PDF检查溢出脚本最后才上生产。paged.js 把分页从玄学变成了可调试的 CSS 问题但前提是你要像对待普通 CSS 一样对待分页规则——哪里不对就去 DevTools 里看那个元素的几何盒子而不是盲目改参数。这套流程跑顺之后再做报表、合同、讲义的 PDF 输出就只剩下内容填充的体力活了。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑