单页 PDF 只是入门,生产环境真正考验功力的是多页与批量:一份报告要封面、目录、正文、附件层层拼接;一个证书系统要一次导出几百份版式相同、数据各异的 PDF。这类需求对代码组织与性能都提出了更高要求——页面拼接顺序怎么保证?批量循环怎么避免卡死浏览器?渲染几十页要多久,用户等不等得起?dompdf.js 的 API 设计天然支持这类场景:addPage 可多次链式调用拼出多页文档,实例可复用支撑批量循环,WASM 内核保证长文档的渲染速度。本文从链式调用的组织方式讲起,覆盖批量证书、批量报表等真实场景,深入性能优化与进度反馈,帮你把批量导出做成稳定、快速、体验良好的功能,让大数据量的导出任务也能从容应对。同时给出批量任务的审计留痕与并发控制建议,让大规模导出也稳定可靠。
dompdf.js 的多页模型很直接:每次调用 addPage 追加一页,页面顺序即调用顺序。链式调用的价值在于把拼文档变成写代码:封面、目录、正文、附件各是一个 addPage,阅读代码就能看出文档结构,修改某一页只需定位对应调用,维护性远胜于在巨型 HTML 字符串里手工切页。
链式调用也方便混合内容形态:字符串模板页、DOM 元素页可以交替出现,数据驱动的动态页与静态页共存。比如报告第一页用模板字符串渲染封面,中间章节用 DOM 容器捕获实时图表,最后一页再追加数据附录,一条链式调用完整覆盖,各页各司其职。
组织链式调用的建议:按文档结构拆分渲染函数,每个函数只负责一个页面的 HTML 或元素,主流程里依次调用 addPage。这样单页函数可以独立测试、独立复用,拼接顺序一目了然,新增页面类型也不会打乱已有结构,代码的可读性与扩展性同时得到保障。
链式调用还有一个额外收益:单元测试友好。每个页面渲染函数接收数据、返回 HTML,纯函数容易断言;主流程只需要验证 addPage 的调用顺序与次数。测试覆盖了模板与组装两个层面,回归风险大幅降低,重构时也更有底气。
批量场景的典型形态是:数据数组加统一模板加循环导出。以证书为例,获奖名单几十上百人,每个人的姓名、奖项、编号不同,版式完全一致。把证书模板写成接收数据的函数,循环调用并各自 save,一份名单几分钟内就能导出全部证书,人工排版需要数小时的工作被压缩到几秒钟。
批量导出的两个关键点:一是每份文档独立实例还是复用实例。版式与配置完全相同的场景,可以循环内新建实例,保证每次导出互不影响;数据量大时复用配置工厂,减少重复初始化开销,两者结合性能与隔离性兼得,代码也更清晰。
二是输出策略:逐份下载(每份一个文件)还是合并成多页文档。证书、合同这类需要单独归档的单据适合逐份下载;报表、档案这类需要整体阅读的适合合并成多页文档。根据业务交付形态选择,不要为了技术统一而牺牲业务便利性,交付体验优先。
批量场景还要考虑数据来源的稳定性:接口分页拉取、增量更新、失败重试都需要在循环外处理。先把数据准备成完整、有序的数组,再进入渲染循环,渲染逻辑保持纯粹,数据问题与渲染问题互不干扰,排查时各查各的,效率更高。
import { DomPDF } from 'dompdf.js';
const winners = [
{ name: '张伟', prize: '一等奖' },
{ name: '李娜', prize: '二等奖' },
{ name: '王强', prize: '三等奖' },
];
// 证书模板:接收数据,返回 HTML 字符串
function certificateHTML(w) {
return `
<div style="text-align:center;padding-top:60mm">
<h1>获奖证书</h1>
<p style="font-size:18pt">${w.name} 同学:</p>
<p>荣获 2026 年编程大赛 ${w.prize},特发此证。</p>
</div>`;
}
// 批量导出:逐份下载,动态命名
for (const w of winners) {
const pdf = new DomPDF({ format: 'A4', margin: '20mm' });
pdf.addPage(certificateHTML(w));
pdf.save(`证书-${w.name}.pdf`);
}
批量场景的耗时主要花在渲染上:每份文档都要走一遍快照、分页、写入流程。优化从模板入手最划算:模板尽量精简,避免不必要的嵌套与复杂样式;同一批数据尽量复用同一份模板字符串,减少重复构建开销,渲染时间会随模板复杂度下降而明显缩短。
图片是另一个优化重点:证书里的 Logo、印章如果每份都重新加载编码,批量导出时开销成倍放大。建议把公共图片转为 base64 内联并复用同一 URL,让资源只编码一次;图片尺寸也控制在合理范围,过大的图片会让每份 PDF 都变慢,文件也更大。
渲染粒度也要考虑:如果用户只需要整体打包的多页文档,就用一个实例链式 addPage 后一次 save,避免多次初始化与多次下载;如果需要逐份文件,控制批量循环的节奏,必要时分批执行并给用户进度反馈,避免长时间无响应的等待,体验更可控。
模板层还有一个容易忽视的优化:把公共样式提取到 <style> 标签里复用,而不是在每个元素上重复写内联样式。模板字符串体积会显著下降,快照阶段要处理的样式也更少,批量渲染时收益被放大,维护起来也轻松很多,一举多得。
批量导出动辄数十秒,没有进度反馈会让用户反复确认是不是卡了。建议在循环里维护计数器:已导出份数除以总数,配合当前文件名显示在界面上,用户能直观看到进展,等待体验完全不同,耐心也会显著提升,重复点击与投诉都会减少。
进度之外还要处理失败:批量循环中某一份失败不应中断全部。逐份 try/catch,失败项记录原因并继续下一份,最后汇总成功 X 份、失败 Y 份,让用户明确知道结果,也方便针对失败项重试,整体体验比整体失败再重来可靠得多,数据也不会白做。
交互上,导出期间禁用重复触发入口,避免用户连点导致任务堆积;完成后给出明确的成功提示与文件位置说明。批量功能做得好不好,用户体验往往就体现在这些细节上,技术与交互配合到位,功能才算真正完成,而不是停留在能跑通的阶段。
进度反馈的设计上,建议区分整体进度与单项状态:进度条显示第 X/N 份,单项状态显示正在导出:证书-张三.pdf。两个维度的信息互补,用户既知道还要等多久,也知道当前在做什么,信息密度恰到好处,等待过程也不再焦虑。
Q: 批量导出时浏览器卡死?A: 通常是单次渲染内容过多或图片过大。先拆分内容为多次 addPage,再压缩图片体积;仍卡顿则分批执行,每批之间留出间隔,让浏览器有时间回收资源,任务队列也不会无限堆积,页面始终保持响应。
Q: 循环里 save 只有前几份成功?A: 浏览器对连续自动触发的下载有限制。逐个下载之间增加间隔,或改为用户点击逐个触发;批量导出建议串行加间隔策略,成功率会明显提升,也符合浏览器安全策略的预期,不要密集触发。
Q: 多页文档里某一页出错,整份失败?A: 排查该页对应的模板与数据,用最小复现单独渲染该页确认问题;生产环境建议在 addPage 阶段对每页内容做一次基础校验(非空、类型正确),尽早拦截错误,而不是等整份文档渲染完才发现,返工成本更低。
Q: 批量渲染的内存占用偏高?A: 长文档与大量图片是主要内存消耗源。渲染完一份及时释放引用(置空变量、移除临时 DOM),大图片先压缩再嵌入,必要时分批处理,内存峰值会明显回落,长时间批量任务更稳定,不会越跑越卡。
最后补充一个批量场景的架构建议:如果批量任务很重(上千份),可以考虑把渲染拆到空闲时间执行,或限制并发数量。前端资源有限,与其让浏览器一次性吃满,不如细水长流,稳定性优先,功能才能长期可靠地运行。
下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:
这是由 dompdf.js 渲染的示例 PDF 内容。