
简介一套面向前端开发者的 paged.js 详细教程资源专门解决在浏览器端将 HTML 内容转换为 PDF、并按照 CSS 分页媒体规则实现优雅分页的问题。教程从库的背景与原理讲起逐步覆盖 npm 安装、项目引入、基础文档转换、页面尺寸与边距自定义、Grid/Flexbox 复杂布局适配、通过 toBlob/toDataURL 输出文件并集成到 React/Vue/Angular 工作流还介绍了浏览器模拟器调试与性能优化方法。资源包共32401个文件、约40.16MB主体为28814张操作与效果截图、2918个 JavaScript 示例脚本并包含 otf/eot/woff/ttf 字体文件、HTML 页面、CSS 样式及 Markdown 笔记等可配合实例逐步对照练习。已有585人学习下载适合希望自主生成高质量 PDF 报告、发票或电子书的前端开发者深入参考。1. paged.js 到底是什么把 HTML 切成规范 PDF 页面的一把手术刀做 Web 的人多少都被「导出 PDF」折腾过。浏览器自带的打印功能虽然能存成 PDF但对 CSS Paged Media 这套分页排版规范基本是无视的——page边距、页脚页码、命名页这些控制项原生打印一概不认长文档断页全靠玄学。paged.js 是一个用 JavaScript 在浏览器里实现 Paged Media 的库它把page、margin box、break-*这些规则真正跑起来让 HTML 文档按你定义的页面尺寸被切成一张张规格整齐的页面。适合做在线合同、电子书、课程讲义、简历导出这类项目的从业者也是我目前用过的 HTML 转 PDF 方案里排版控制力最强的一个。2. paged.js 分页原理CSS Paged Media 为什么在浏览器里没人管2.1 规范与实现Paged Media 被浏览器漏掉的部分CSS 规范的 Paged Media Level 3 模块专门管分页排版它定义了几组核心机制page规则声明页面盒子的尺寸、边距、出血。margin boxtop-center、bottom-right等页边距盒子用来放页码、页眉、注记。命名页page cover {}这种按用途区分页面类型的方式。断页控制break-before、break-after、break-inside以及孤儿行、寡妇行参数。这套机制设计的初衷是让「打印样式」和「页面结构」解耦。一份 HTML 用不同的page规则就可以输出为不同开本、不同边距、带不同类型页眉页脚的 PDF。设计很完整现实却很骨感主流浏览器只实现了打印时的基础分页page的 margin box、计数器、命名页这些重头戏统统缺席。paged.js 的切入点就是把浏览器漏掉的这部分用 JavaScript 补上。它的工作流程大致是读取样式表里的 Paged Media 规则 → 按每个元素的实际计算尺寸测量 → 决定每个元素落在哪一页 → 把页面 DOM 重新切分为分页结构。和用 PDF 库手动计算坐标的路线不同paged.js 保持 CSS 语法作为唯一的排版入口页面设计稿直接能被复用为分页脚本不用在 JS 里重新描述一遍布局。2.2 最小接入Polyfill 模式三行代码让页面长出分页安装方式看项目习惯。npm 项目装包后在入口文件引入或者直接把仓库里编译好的 dist 文件复制到静态目录。这两种方式最核心的加载差异是npm 引入适合配合打包器做依赖管理dist 文件适合快速验证和小项目里单独引用。最小 HTML 长这样!DOCTYPE html html head link relstylesheet hrefprint.css style page { size: A4; margin: 2cm; } /style /head body h1第一章 绪论/h1 p这里是正文内容。分页之后这段文字会被放进第一页的内容盒子中。/p section h21.1 背景/h2 p第二段文字如果前面内容放不下会按断页规则流到下一页。/p /section script src./dist/paged.js/script script // 分页逻辑页面加载完成后执行 window.addEventListener(load, function () { new Paged.Polyfill().init(); }); /script /body /htmlPaged.Polyfill().init()做了三件事解析页面里所有样式表中的 Paged Media 相关规则计算每个块级元素的实际高度与页面剩余空间的匹配关系按计算结果把内容切为一页一页并写入 DOM。切分后的每个页面高度等于page规则指定的内容区域高度浏览器出现纵向滚动条说明内容不止一页。提示init()必须在load事件之后调用否则脚本执行时图片、字体可能还没就位拿到的测量尺寸不对分页结果全是错的。这个细节本地开发时不明显部署到线上资源加载变慢就现出原形了。2.3 Polyfill 与 Previewer两种模式的取舍paged.js 的核心代码暴露了两个主要入口各有分工Paged.Polyfill正式分页处理器。init()执行完页面上看到的就是被真实切分后的页面形态适合配合浏览器打印、Puppeteer 导出等最终产物链路。Paged.Previewer预览器。它在页面现有内容之外叠加一层「页框」可视化视图方便在浏览器里直接检查断页是否合理、元素是否越界、margin box 是否生效不直接产出最终 PDF。开发流程里我通常先用 Previewer 观察分页边界调整 CSS 直到断页符合预期再切回 Polyfill 做输出。Previewer 的调用方式同样简单// 预览模式等待分页铺设完成后做后续动作 window.addEventListener(load, function () { const previewer new Paged.Previewer(); previewer.preview().then(function () { // 预览分页完成可以在此处截图或记录页数 }); });preview()返回 Promise拿到 resolve 再触发下一步比定时等待可靠。注意两种模式不能在同一次页面加载里同时调用它们会重复抓取 DOM、互相覆盖分页结果。如果你需要自动判断当前模式可以在初始化前检测全局变量里是否存在Paged.Previewer再决定走哪条初始化逻辑。3. 实战排版A4 页面、页码页眉与断页控制的标准写法3.1 page 参数详解尺寸、边距与命名页page规则是整份分页样式的入口。手动导出 PDF 时页面尺寸、边距和盒子结构都由它决定。/* 默认页面A4四周留白 */ page { size: A4; margin: 2cm 1.8cm; } /* 封面命名页不设边距 */ page cover { margin: 0; } .cover-page { page: cover; }size接受关键字或具体长度。关键字常用 A4、A5、Letter印刷场景直接给size: 210mm 140mm更精确。margin与普通盒子属性一致四值、双值、单值都能用实际值会被 paged.js 转成每个页面盒子的周边留白。命名页是容易被低估的功能。页面规则可以命名然后通过元素的page属性把它推进指定的页面类型里去。上面代码里的.cover-page元素进入cover命名页该页没有边距适合做封面、扉页。一个页面规则可以被多个元素引用连续两个使用同一命名页的元素会落在同一逻辑页序列里内容超过一页时沿续使用该规则。参数选型上有一条建议面向打印店的交付物size写具体毫米值面向屏幕阅读的 PDFsize: A4加margin: 1.5cm即可边距过大反而让阅读宽度变窄。bleed和marks这类印刷参数浏览器和 paged.js 的兼容情况不一致优先用size与margin表达页面约束不要上来就依赖出血位。3.2 页眉页脚margin box 与 counter(page)、string() 的配合Margin box 是 paged.js 最有生产力的部分。在page内部可以使用top-left、top-center、top-right、bottom-left、bottom-center、bottom-right六个位置以及左右页外侧的left-middle、right-middle。每个盒子用content属性放置文本或计数器。page { top-center { content: 内部资料 禁止外传; font-size: 9pt; color: #888; } bottom-right { content: 第 counter(page) 页 / 共 counter(pages) 页; } }counter(page)是 paged.js 内置的页码计数器每翻一页自动加 1counter(pages)是总页数。两者配合就能得到「第 3 页 / 共 12 页」这种常见的页脚格式。计数器语法只在 margin box 和伪元素的content参数里生效写到普通文本节点里不会解析。页眉显示章节标题是另一个高频场景用string-set实现h1 { string-set: chaptertitle content(); } page { top-left { content: string(chaptertitle); } }string-set: chaptertitle content()的含义是取h1元素的文本内容绑定到名为chaptertitle的字符串变量。之后所有页面的string(chaptertitle)都引用最近一个被设置的h1值。做长文档时这个机制比 JS 手动往页眉里注入 DOM 轻量得多且天然跟随内容流的位置标题换章节时页眉自动跟着换不需要额外写监听逻辑。3.3 分页控制break-before/after、orphans 与 widows 的参数取舍分页控制是排版观感的绝对主角。paged.js 支持的是 CSS Fragmentation 模块的标准属性break-*旧的page-break-*别名虽然兼容但优先级和边界行为略有不同新代码统一写break-*更省心。/* 每个章节从新页面开始 */ .chapter { break-before: page; } /* 标题不要孤悬在页面末尾 */ h2, h3 { break-after: avoid; } /* 代码块、表格尽量整体保全 */ pre, blockquote, table { break-inside: avoid; } /* 段落断页时上、下至少保留 3 行 */ p { orphans: 3; widows: 3; }四个属性的含义和参数取值如下break-before: page元素前强制分页常用于章节开头。break-after: avoid元素后不立即分页防标题出现在页面最后一行。break-inside: avoid元素整体不可被切断适用于代码块、表格、引用块。orphans/widows段落被切断时断点上下各保留的最少行数取 2 或 3 观感最稳妥取 1 会出现单行孤悬。注意break-inside: avoid对行内上下文不生效如果一个inline-block容器里的内容超过一页高度它照样会被切分。想让某个块强制不分页要同时确认它的父级没有display: contents之类的结构干扰。这里有个容易误用的点paged.js 对 flex 和 grid 容器的分页已经有基础能力但内部子项的换页行为仍以块级文档流为主复杂的双栏、卡片布局建议先做分页测试再投入产出不要指望它像原生文档流那样完全可预测。4. 避坑指南paged.js 实战中五个高频翻车点4.1 分页后内容被截断现象某几页正文底部被切断文字后半截凭空消失不报错也不换页。原因元素的实际高度超出了页面内容盒子高度。paged.js 在分页计算时把这个越界的元素按页面边界做了裁剪被裁剪的部分没有进入后续页面的待排队列。常见诱因是固定高度容器、图片、代码块没有留足页边距空间或者 flex 布局下子元素隐式最小尺寸把高度撑破。解决对有高度风险的容器加max-height: calc(100vh - 4cm)之类的上限同时给 flex 子项声明min-height: 0。排查时优先检查页面上有没有滚动容器和固定高度的元素这类元素占的视觉空间和文档流高度不一致最容易导致 paged.js 算出的可容纳高度失真。4.2 页脚页码全是 1现象写了content: counter(page)打开输出 PDF 后发现每一页的页码都显示 1页数没有递增。原因页码计数器只在页面上下文里递增。如果bottom-center块没有正确嵌套在page规则内或者样式选择器写成了page .bottom-center这类非标准形式counter 就不会被 paged.js 识别为页面计数器每次取值都拿到初始值 1。另外同一份文档里混用了多个page规则其中一部分规则没有声明 margin box这些页面的页脚空间继承了默认值。解决确认 margin box 的嵌套层级是page { bottom-center { content: counter(page); } }。多命名页场景下把公共 margin box 放在一个基础page规则里命名页只写差异项。还可以在 Previewer 模式下直接看页脚渲染区域是否出现样式被忽略时 Previewer 的页框视图里会直接空缺。4.3 动态 DOM 不参与分页现象脚本在页面加载后追加的一段内容没有出现在最终 PDF 里但浏览器预览里能看到。原因paged.js 的 Polyfill 在load事件后抓取 DOM 快照执行分页计算其后新插入的节点不会自动重排。很多「先渲染数据再分页」的前端项目都会踩到这个坑——数据请求完成时分页已经算完了。解决把数据获取和 DOM 注入提前到load事件之前让分页计算开始时所有节点已在 DOM 中。如果必须异步注入则需要在注入完成后手动调用Paged.Polyfill实例上的render()方法重排版。render()是全量重算会把页面滚回顶部不要在短时间内频繁调用一批数据注入完毕后调用一次即可。4.4 图片和字体加载晚导致分页错位现象本地开发打开分页完全正常部署到服务器或换成线上环境后页面断点全部偏移部分图片被切成跨页。原因分页计算依赖的是元素渲染后的实际尺寸。图片未加载完成时高度为 0自定义字体未加载时回退字体的行高与实际字体的行高不一致这两者都会让 paged.js 在load事件触发时拿到错误的测量数据。解决初始化分页前等待所有字体和图片就绪// 等待字体与图片全部加载完毕后再分页 Promise.all([ document.fonts.ready, Promise.all(Array.from(document.images).map(function (img) { if (img.complete) { return Promise.resolve(); } return new Promise(function (resolve) { img.addEventListener(load, resolve); img.addEventListener(error, resolve); }); })) ]).then(function () { new Paged.Polyfill().init(); });图片写进 HTML 时尽量带上width和height属性布局引擎能提前算出占位面积避免load时序波动。自定义字体建议在分页前用document.fonts.load主动加载需要的字重而不是被动等字体渲染完。4.5 表格跨页后表头丢失现象长表格跨页后第二页的接续部分没有表头列字段对应关系只能靠猜。原因浏览器打印引擎会把thead在跨页处自动复述但 paged.js 的 DOM 切分是整体按块移动不会自动为表格补全表头默认行为是内容流直接续排thead只存在于第一段。解决给表格的tr加break-inside: avoid再给thead保留display: table-header-group让表头组在结构上保持独立。更稳的方案是数据量可预估时直接用 JS 按行数切分表格每个子表格自带表头。对固定行数的数据表我一般直接按「每页 N 行」切分N 根据页面高度和行高反推这样输出的 PDF 每页表头整齐观感接近原生打印产物。5. 脚本化导出Puppeteer 等待分页完成再批量生成 PDF5.1 手动打印的正确姿势边距设无、缩放默认开发阶段最快速的验证方式是浏览器打印对话框。按Ctrl P后注意两个设置边距选「无」缩放保持「默认」。paged.js 已经在 HTML 层面把内容切成了页打印对话框如果再加上自己的边距和缩放计算内容会在原有分页基础上被二次压缩或裁剪。media print样式在这一步里扮演的角色是「输出微调」隐藏不必要元素、调整链接颜色、移除交互反馈。页面尺寸、边距、页眉页脚这些页面级参数一律建议放在常规样式或 paged.js 读取的样式表里不要在media print里再声明一份page两个来源同时出现时浏览器解析优先级和你预期的往往不一样调试起来很费时间。5.2 Puppeteer 完整流程等待>const puppeteer require(puppeteer); (async function () { const browser await puppeteer.launch({ headless: true }); const page await browser.newPage(); // 打开待导出页面网络资源全部加载完 await page.goto(file:///path/to/document.html, { waitUntil: networkidle0 }); // 等待 paged.js 分页完成data-paged 属性出现即分页已落定 await page.waitForSelector([data-paged], { timeout: 15000 }); await page.pdf({ path: output.pdf, format: A4, printBackground: true, preferCSSPageSize: true, margin: { top: 0, right: 0, bottom: 0, left: 0 } }); await browser.close(); })();关键在于page.waitForSelector([data-paged])。paged.js 分页完成后会在页面容器上写入>const fs require(fs); const buf fs.readFileSync(output.pdf); const matches (buf.toString(latin1).match(/\/Type\s*\/Page\b/g) || []); console.log(PDF 页数:, matches.length);第二个是文本层抽样。打开 PDF 搜索页脚里的页码、页眉里的章节名确认这些动态内容真的出现在每个对应页面上。这一步能抓出「命名页没有继承 margin box」「页脚显示默认值」这类肉眼不易察觉的问题。第三个是视觉抽查。把 PDF 转成图片挑首页、目录页、第一个跨页表格页、末页各看一遍重点看元素有没有越界、图片有没有被切断、页边距是否均匀。自动化校验只能挡逻辑问题视觉问题还是要人眼过一遍。建议把上面这份print.css模板和 Puppeteer 脚本存成固定的 tools/paged-print 目录接到新项目时只改page参数和内容 ID。从那以后我每次用 paged.js 出 PDF 都强制走一遍「页面数 → 文本层 → 视觉抽查」三条流程前后不到一分钟却挡掉了不止一次交付翻车。希望帮到你。本文还有配套的精品资源点击获取