资讯动态

paged.js 教程:HTML 转 PDF 分页排版与页眉页脚实战

发布时间:2026/10/23 15:08:01 来源:尧图企业网站定制
简介面向前端开发者与需要实现网页转PDF、打印分页的工程人员这份教程围绕 paged.js 展开重点解决将连续 HTML 内容按 CSS Paged Media 规则切分为规范页面并导出 PDF 的问题适合具备 JavaScript 与 CSS 基础、希望补齐分页排版能力的读者。资源包共 32401 个文件压缩后约 40.16MB主要由 28814 个 png 页面与示意图素材、2918 个 js 脚本以及 otf、eot、woff、ttf 等网页字体和少量 html、md、json 文档构成便于对照示例结构、字体配置与脚本组织。内容覆盖 paged.js 的安装引入、基础转换、页面尺寸与边距配置、page 样式定制、Grid/Flexbox 复杂布局处理以及 toBlob、toDataURL 输出 PDF、与前端框架集成和模拟器调试等环节。已有 586 人学习可结合包内素材搭建演示页理解分页媒体规则排查分页断裂、样式适配与 PDF 导出问题形成可复用的前端 PDF 生成思路。1. 打印分页不该靠缘分paged.js 到底解决什么问题后台导出季度巡检报告第一版直接window.print()结果二十页的文档里第三章标题孤零零掉在页脚线上表格被拦腰截断页码只能靠position: fixed硬凑。后来换成 paged.js 才把这口气顺过来它是一个用 JavaScript 写的 CSS Paged Media 实现不负责生成 PDF只负责把一段 HTML 重新排成一张张带页眉、页脚、页码的页面 DOM屏幕上看到什么打印或导出就是什么。我手上这份 paged.js 教程是一份 PDF 资料从page规则一路讲到 polyfill 的渲染流程和 handler 扩展前半部分讲规范后半部分全是能直接跑的示例。它适合所有要做合同、检测报告、课程讲义、电子书导出的前端会写 CSS 就能上手不必去接 LaTeX 或后端排版引擎。2. 把 paged.js 跑起来从 polyfill 引入到 Headless 出 PDF2.1 先搞清楚它接管了浏览器的哪一段浏览器原生打印只认page里的size和margintop-center这种页边距框、counter(pages)这种总数计数器、target-counter()这种交叉引用它一概不理。paged.js 补的就是这段空白所以它叫 polyfill 而不是渲染器。工作过程大致分四步读取页面里的样式表并解析page规则把源内容切成 chunk把 chunk 逐个塞进页面模板里测量高度一旦溢出就用 breakToken 回退重新切。这意味着它的分页是「试排加重切」几百页的文档第一次渲染跑个几秒到十几秒都算正常不要以为是卡死了。渲染完成后原来的内容会被搬进一套固定结构的 DOM最外层.pagedjs_pages每一页是一个.pagedjs_page页内是.pagedjs_sheet再往下拆成.pagedjs_margin-top-left、.pagedjs_margin-bottom-center这类页边距框以及.pagedjs_page_content正文区。这个结构得先记住后面写选择器、做校验、改页码全靠它定位。引入方式有三条路代价各不一样我一般按内容是不是同步渲染来选引入方式适用场景需要注意script引dist/paged.polyfill.js静态页面、服务端模板直出DOMContentLoaded 之后就自动渲染异步注入的内容会跑空ESMimport { Previewer } from pagedjs打包进前端工程、需要多次渲染要自己管容器和清理不能指望自动PagedConfig.auto false加手动preview()内容异步、要拿总页数做事多写十几行但可控性最好教程里对第二种写法着墨最多但实际做后台导出第三种才是常态因为报告数据基本是接口拿回来再渲染的。2.2 一个能直接抄的最小页面先跑通再谈样式。下面这版把源内容、预览容器、样式表分开避免 paged.js 把预览 DOM 当源内容再排一遍!DOCTYPE html html langzh-CN head meta charsetutf-8 link relstylesheet href./report.css /head body !-- 源内容只放原始 HTML不参与预览 -- main idsource section classcoverh1季度巡检报告/h1p内部资料/p/section section h2 idsec-1一、概述/h2 p正文段落……/p /section /main !-- 预览容器paged.js 把生成的页面塞这里 -- div idpreview/div script src./vendor/pagedjs/paged.polyfill.js/script script // 关掉自动渲染等字体和异步数据都就绪再手动触发 window.PagedConfig { auto: false }; document.fonts.ready.then(async () { const styles document.querySelectorAll(link[relstylesheet]); const flow await window.PagedPolyfill.preview( document.querySelector(#source), // 源内容节点 styles, // 样式表节点列表注意是节点不是 URL document.querySelector(#preview) // 渲染目标容器 ); window.__pagedReady true; // 给外部脚本/自动化用的完成标记 console.log(总页数, flow.total); }); /script /body /htmlpreview(content, stylesheets, renderTo)三个参数都得留意。第一个是 DOM 节点不是选择器字符串第二个是样式表的 DOM 节点列表传 URL 字符串它会去 fetch跨域或路径不对就静默丢样式页眉页脚全不见第三个必须是一个独立容器如果你直接省掉它会渲染回document.body源内容被搬走之后二次渲染就抓不到东西了。flow对象里带页码总数和分页后的节点信息字段名在不同构建里可能叫total也可能要用pages.lengthconsole.log(flow)看一眼就清楚别硬猜。document.fonts.ready这一行不是装饰。paged.js 靠实际测量的行盒高度决定断点中文字体没加载完时用的是后备字体的度量行高算小了分页结果就会偏移表现出来是「刷新一次正常、再一次错位」这种玄学现象。这个坑在第 5 章还会展开。2.3 用 Headless 浏览器把预览锁成 PDF浏览器里按 CtrlP 打印等于让浏览器在 paged.js 已经分好的页上再分一次页出来的白纸能翻倍。生产环境我一般直接走 headless把「预览」和「出 PDF」合成一步# 先起一个静态服务file:// 下部分资源会被拦 npx serve ./dist -l 8080 # 另开一个终端装依赖 npm i puppeteer// render.js const puppeteer require(puppeteer); (async () { const browser await puppeteer.launch(); const page await browser.newPage(); await page.goto(http://127.0.0.1:8080/report.html, { waitUntil: networkidle0 // 等接口和字体都静下来 }); // 等我们自己的完成标记别等 .pagedjs_page // 那个类名第一页出来就有了提前截图会截到半成品 await page.waitForFunction(window.__pagedReady true, { timeout: 30000 }); await page.pdf({ path: report.pdf, preferCSSPageSize: true, // 关键采用 page 里写的 size否则强制 A4 printBackground: true, // 背景色、斑马纹表格靠它 margin: { top: 0, right: 0, bottom: 0, left: 0 } // 别和 page 的 margin 叠加 }); await browser.close(); })();preferCSSPageSize一定要开不然你在 CSS 里写的横版命名页会被压回纵向 A4。margin四个零也有讲究page.pdf()的页边距和page的margin是叠加关系两处都写 20mm 就变成 40mm正文被挤成窄条。printBackground不开的话表头底色和分隔线全丢报告看起来像半成品。3. page 与页边距框页眉、页脚、页码写到该在的位置3.1 size、margin 与命名页page是一切版式的入口最常用的是尺寸和边距。除了全局规则还可以定义命名页用page属性把某个区块挂上去这是做横版大表格和整页封面的正路/* 全局A4 纵向上边距留宽一点给页眉 */ page { size: A4; margin: 22mm 18mm 20mm 18mm; } /* 封面满版出血不要页边距 */ page cover { margin: 0; top-center { content: none; } bottom-center { content: none; } } .cover { page: cover; break-after: page; } /* 横版页宽表格专用 */ page wide { size: A4 landscape; } .wide-table { page: wide; } /* 首页单独处理paged.js 也认 :nth(AnB) */ page :first { top-center { content: none; } }size除了A4/A5/letter也可以写210mm 297mm这种自定义尺寸需要出血印刷时用size: A4; margin: 0再靠内边距撑内容。命名页的名字是自定义的和.cover这种类名不绑定只要page属性和page名对上即可。break-after: page是给封面收尾用的不加的话正文会紧贴着封面往下排。:nth(2)这类选择器是 paged.js 的扩展原生 CSS 里没有写的时候心里有数万一以后换渲染器要改。3.2 十六个页边距框与页眉页脚page内可用的页边距框一共十六个顶部top-left-corner、top-left、top-center、top-right、top-right-corner底部对称的五个左侧left-top、left-middle、left-bottom右侧对称的三个。内容用content属性填可以是字符串、计数器、string()取值也可以是element()引用页面里的元素。page { /* 页眉左侧固定文字右侧取当前章节标题 */ top-left { content: 内部资料 · 请勿外传; font-size: 8pt; color: #888; } top-right { content: string(chapter); font-size: 9pt; border-bottom: 0.5pt solid #ccc; /* 页眉下划线 */ } /* 页脚居中放页码 */ bottom-center { content: counter(page) / counter(pages); font-size: 9pt; } }有几个实测出来的边界。一是顶部边距的三个框默认按三等分宽度分配top-left里塞一句长中文会往中间挤和top-center打架所以文案要控制长度或者显式给width。二是页边距框的高度受margin-top限制字号调大就会被裁掉页眉想写两行就得把上边距加到 25mm 以上。三是这些框里的样式不继承正文的字体族字体得在page里重新声明一遍否则页脚会用默认衬线体和正文对不上。3.3 页码、总页数、章节标题与目录页码计数器分两类。counter(page)和counter(pages)是页面级的前者当前页后者总数只有 paged.js 渲染后才有值浏览器原生打印里pages是空的。章节标题靠string-set和string()这一对在标题元素上采集在页眉里取用h2 { string-set: chapter content(text); /* content() 取整段含标签content(text) 只要文字 */ } /* 目录页码a 的 href 指向目标 id自动补上目标页的页码 */ .toc a::after { content: target-counter(attr(href), page); float: right; }string-set的采集点是标题元素本身所以标题如果带编号span classnum三、/spancontent(text)会连编号一起带进去这通常正是想要的。target-counter做目录页码很省事前提是目录里的href写成#sec-1这种锚点形式并且目标元素真的有id写成相对路径的页面跳转就不生效了而且不会报错只是页码空着这种静默失败最费时间。页码重编号是另一处需要提前想清楚的地方。封面不想显示页码用命名页把bottom-center的content设成none就行但要让正文从 1 重新开始计数靠counter-reset在命名页上重置实测在不同构建里表现不一致有的版本认有的版本整篇页码都乱。稳妥做法是留到第 6 章用 handler 处理或者干脆接受「封面算第 1 页」这个设定把页码框在封面上隐藏掉读者感知不到差别。4. 分页行为的硬控制break 属性、表格与图片跨页4.1 break-before / break-after / break-inside自动分页永远会在最难看的位置断。控制手段就三个属性break-before、break-after、break-inside分别管元素前面、后面、内部能不能断。老写法page-break-*也认但新写法更通用混着写容易覆盖顺序出问题统一用新的/* 每章另起一页但标题后面不许断 */ h2 { break-before: page; break-after: avoid; } /* 小标题后面紧跟正文别让标题孤零零留在页尾 */ h3, h4 { break-after: avoid; } /* 图表说明、引用块整块保留 */ figure, blockquote, pre { break-inside: avoid; } /* 段落允许按行拆但至少留三行 */ p { orphans: 3; widows: 3; }break-after: avoid是解决「标题掉页尾」最直接的一招paged.js 会在排版时把这个标题和后面的内容当作一个整体处理放不下就一起挪到下一页。orphans和widows在 paged.js 里能生效但中文长段落的表现不如西文稳定因为它按行盒拆分中文字符宽度均等边界判定更依赖实际测量。我的经验是别指望这两个值精调真正要保住版式就上break-inside: avoid把整块锁死代价是可能留下大片空白得在两者之间权衡。还有一类反直觉的情况给一个比整页还高的元素加break-inside: avoid结果不是它被拆开而是它直接溢出到页面外面盖住页脚。paged.js 遇到无法安置的元素时不会强行裁剪所以这个属性只能用在确定小于页内容高度的元素上。4.2 表格跨页表头重复与列宽漂移长表格是最常见的翻车点。表头要在每页重复靠的是display: table-header-grouppaged.js 识别到这个声明后会在每一页的续表里克隆一份theadtable { width: 100%; border-collapse: collapse; table-layout: fixed; /* 固定列宽避免每页重新分配 */ } thead { display: table-header-group; } tfoot { display: table-footer-group; } /* 单行不许拆开否则一个单元格会横跨两页 */ tr { break-inside: avoid; } /* 超宽表格走横版命名页 */ page wide { size: A4 landscape; } table.wide { page: wide; font-size: 8pt; }坑在列宽。table-layout: auto时每一页的续表都会根据自己的内容重新分配列宽第一页三列是 3:4:3第三页变成 5:3:2翻页时肉眼可见地跳动。加上table-layout: fixed并配colgroup写死百分比各页就一致了。另一个细节是表头克隆出来的那份thead不带原来的id和事件如果原表头里有排序按钮或者id被 JS 依赖续页上会失效这种问题在导出场景里无所谓但在交互式预览里要留意。至于表格要不要整体避免跨页得看行数十来行的表直接break-inside: avoid让它整体挪页更整齐上百行的明细表必须允许拆否则一页放不下就会溢出。判断标准很简单估算一下行高乘行数有没有超过页内容高度。4.3 图片、代码块与长公式的裂页处理图片裂页的表现是两个半张图分别落在相邻两页原因是img默认允许在内部断。加break-inside: avoid能解决大部分情况但要配合max-height否则一张竖版长图超过页高就变成 4.1 说的溢出img { max-width: 100%; max-height: 230mm; /* 略小于 A4 内容区高度留出图注空间 */ height: auto; break-inside: avoid; } /* 图和它的说明文字绑在一起 */ figure { break-inside: avoid; margin: 0 0 12pt; } /* 图片紧跟标题时不要被拆散 */ h3 figure { break-before: avoid; }内容区高度怎么算A4 高 297mm 减去上下页边距比如22mm 20mm剩 255mm。max-height取这个值的九成左右比较安全还要给figcaption留位置。代码块同理pre加break-inside: avoid但一个超过两页的长代码块加了这个属性就会溢出这时候得手动在合适的位置插入分页标记比如在代码中间加一个div classpage-break/div配一条.page-break { break-after: page; }用人工干预换正确性。5. 避坑排查paged.js 落地最常翻车的五处5.1 渲染时序类看不见的错位都出在这里现象一同一份报告刷新两次排版结果不一样有时候最后一页多出一大块空白。原因是 Web 字体异步加载paged.js 第一次测量用的是后备字体的字符宽度等真字体到位行高和折行位置全变了但分页已经按旧数据切完。解决是把渲染触发挂到document.fonts.ready之后写法见 2.2用 headless 导出时再加一道waitUntil: networkidle0双保险。血泪经验是字体文件尽量本地化别用第三方字体服务网络抖动会让这个时序问题变得完全不可复现。现象二页面一片空白控制台没有任何报错。原因是数据接口返回后才往 DOM 里插内容而 polyfill 在DOMContentLoaded阶段就已经渲染完了它排的是空容器。解决是设window.PagedConfig { auto: false }等数据落地后再手动调preview()并且用一个全局标记就是示例里的__pagedReady通知外部流程。现象三改一次参数总页数涨一倍页面越渲染越多。原因是preview()会往renderTo容器里追加页面重复调用就重复追加源内容也被搬来搬去。解决有两条每次渲染前container.innerHTML 清空或者干脆每次new Previewer()一个新实例不复用。顺手把window.__pagedReady重置为false否则外部等待逻辑会拿到上一次的旧结果。5.2 输出与版式类预览对了不等于打印对现象四浏览器里预览 12 页CtrlP 打出来 24 页每页后面跟一张白纸。原因是.pagedjs_page自己有固定高度浏览器打印时又按纸张尺寸重新分一次页两个分页逻辑叠加就多出一张空白。解决是在打印媒体查询里把页间距清零并显式收尾media print { body { margin: 0; background: none; } .pagedjs_pages { display: block; } .pagedjs_page { margin: 0 !important; box-shadow: none; break-after: page; } .pagedjs_page:last-child { break-after: auto; } /* 最后一页不要追空白 */ }真正省事的做法是绕开打印对话框直接走 2.3 的 headless 导出让「分页」这件事只有一个执行者。现象五封面显示了页码正文页码从 5 开始。原因是counter(page)是全局递增的命名页上的counter-reset在部分构建里不生效。解决分两步封面用命名页把页边距框的content设成none页码其实还在只是不显示如果确实要求正文从 1 重排就进第 6 章的 handler在afterPageLayout里按页序重写页脚文本比跟 CSS 计数器较劲可靠得多。另外补一条容易忽略的源内容里如果有display: none的元素paged.js 不会渲染它但它占的 DOM 位置会影响string-set采集导致页眉取到一个空标题。排查时先querySelector看看取到的文本是不是空的。6. 用 Handler 接管分页过程一个页脚校验的小技巧CSS 能表达的分页规则有上限超出部分就得靠 handler。paged.js 暴露了一组生命周期钩子beforeParsing、afterParsing、afterPageLayout、afterRendered这些注册进去就能在每一页排版完成后做二次加工。最常见的用法是补页码、加防伪编号、给特定页打水印。// footer-handler.js class FooterHandler extends Handler { // 每排完一页调用一次pageElement 是当前页的 DOM afterPageLayout(pageElement, page, breakToken) { const box pageElement.querySelector( .pagedjs_margin-bottom-center .pagedjs_margin-content ); if (!box) return; // page.position 是当前页序号从 1 开始 const total pageElement.closest(.pagedjs_pages) ?.querySelectorAll(.pagedjs_page).length; // 封面不编号正文重新从 1 起算 if (page.position 1) { box.textContent ; return; } box.textContent 第 ${page.position - 1} 页 / 共 ${total - 1} 页; } } // 注册手写 Previewer 用 registerHandlers const previewer new Previewer(); previewer.registerHandlers(FooterHandler); previewer.preview(sourceNode, styles, targetNode); // 如果用的是 polyfill 全局对象走配置传数组更稳 // window.PagedConfig { auto: false, handlers: [FooterHandler] };参数上要盯住两个page.position是当前页在整篇里的序号从 1 开始做「封面不算」的减法就靠它total我是在容器里数.pagedjs_page的数量拿的也可以直接用flow.total但要注意 handler 执行时flow还没 resolve所以更稳的是在afterRendered里统一回填总页数。这套写法比纯 CSS 计数器啰嗦好处是逻辑透明出问题能打断点不像counter-reset那样不生效也不报错。验证环节我一般加一个小脚本把生成的每页页脚文本抓出来对一遍确认没有重复、没有空值、总数对得上再走导出。最开始做这类报告导出时我图省事直接window.print()结果客户收到的是每页后面跟一张白纸的 PDF来回解释了两轮才定位到是双重分页。从那以后我每次改完page或引入新字体都强制走一遍「headless 导出 → 用 pdf 解析库数页数 → 抽查三页页眉页脚」这条链路再没让半成品出过门。这份 paged.js 教程里第 3 章到第 5 章的示例文件我按上面这个顺序重新编过号拿到手从 2.2 那个最小页面开始跑比从头读规范快得多。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑