当页面内容通过 addPage 收集完毕,save 就是最后一步:它触发实际的渲染与下载,把内存中的文档变成用户磁盘上的 PDF 文件。这个 API 看似简单——一个文件名参数而已——但实际使用中却藏着不少细节:文件名能否包含中文、如何根据数据动态命名、批量导出时如何避免浏览器拦截、下载失败时如何反馈用户。这些细节直接影响功能是否可用,也决定用户体验是否流畅。本文深入讲解 save 的工作机制与触发时机,覆盖静态文件名、动态模板命名、多页文档的命名规范,以及批量下载、失败兜底等真实场景,帮助你把这最后一公里做扎实,让导出功能从能用变成好用,让用户每一次点击导出都有确定的、令人满意的结果。并给出文件名清洗函数与降级矩阵等可直接落地的工具,让下载环节万无一失。
save(filename) 接收一个文件名参数,内部流程分两段:先执行渲染——把之前所有 addPage 收集的内容交给 WASM 内核生成 PDF 字节;再把结果作为文件下载到本地。也就是说,addPage 只负责收集内容,真正的重活全部发生在 save 阶段,文档越大,这一阶段耗时越长。
理解这个时序很重要:如果在 save 之前页面资源(图片、字体)尚未就绪,渲染结果就会缺内容;而 save 一旦触发,整个文档会一次性渲染完成。对长文档,这一阶段耗时明显,UI 线程需要保持响应,必要时给出加载提示,避免用户以为页面卡死而反复点击。
从调用方式看,save 直接触发下载,不需要额外创建 a 标签或 Blob URL——这些都已被封装在内部实现里。如果你需要拿到 PDF 的 Blob 自行处理(比如上传到服务器、在线预览),可以结合官方提供的底层能力完成,save 本身专注于下载这一职责,职责单一、使用简单。
从用户视角看,save 是导出按钮按下到文件出现在下载栏的完整过程。为了让这个过程更可靠,建议在调用 save 前检查页面状态:确认数据已加载、内容非空、模板已渲染,任何一项不满足都提前拦截,而不是等到下载结束后才发现文件是空的。
文件名可以是固定字符串,比如 save('report.pdf'),适合单一用途的导出按钮;也可以是模板字符串,把业务数据拼进文件名,比如 save(`销售月报-${month}.pdf`),适合报表、订单、发票等需要区分版本的场景,每次导出都能生成带标识的文件。
动态命名的价值在于可追溯:下载到本地后,用户无需打开文件就能从文件名判断内容对应的单据、时间与版本,大量下载时也不会互相覆盖。命名规范建议与业务系统保持一致:类型前缀加业务编号加日期,例如发票-INV20260818-001.pdf,格式统一检索方便。
中文文件名在浏览器下载场景下通常可以正常保存,但跨平台传输(邮件附件、网盘同步)时偶有编码问题。稳妥做法是文件名尽量使用通用字符,或保留中文的同时避免过长的文件名——Windows 与部分网盘对文件名长度有限制,过长会导致保存失败,命名时就要控制长度。
动态命名的实现建议放在数据层而不是 UI 层:在拿到业务数据时就把文件名算好,随数据一起传递。这样导出函数只负责接收文件名并保存,命名逻辑与展示逻辑解耦,批量导出时也更容易保证命名规则的一致性,测试时也只需要验证数据层。
import { DomPDF } from 'dompdf.js';
// 单张导出:动态文件名
function exportInvoice(invoice) {
const pdf = new DomPDF({ format: 'A4', margin: '20mm' });
pdf.addPage(renderInvoice(invoice));
pdf.save(`发票-${invoice.no}-${invoice.date}.pdf`);
}
// 批量导出:串行执行,每份独立命名
async function exportAll(invoices) {
for (const inv of invoices) {
exportInvoice(inv);
await new Promise(r => setTimeout(r, 300)); // 留出间隔,避免被浏览器拦截
}
}
exportAll([{ no: 'INV001', date: '2026-08-18' }]);
连续多次触发下载时,浏览器可能会拦截部分下载,这是浏览器的防骚扰机制,与 dompdf.js 无关。逐个下载之间留出间隔、或由用户点击逐个触发,能有效规避拦截;一次循环里密集触发多个 save,很容易出现只下载了前几个文件的现象,用户会以为功能有 bug。
更稳妥的批量方案是逐个生成、逐个下载,并控制节奏。把下载动作放在用户事件回调里(点击按钮后),浏览器对用户手势触发的下载更宽容;异步流程中由代码自动触发的连续下载则更容易被拦截,注意给用户留出感知与操作空间,不要一股脑触发。
如果业务需要一次性导出多份文档(如批量证书),建议在 UI 上提供逐个导出与全部导出两个入口:逐个导出每次只下载一个文件,稳定可靠;全部导出则串行执行,每完成一个立即下载,配合进度提示,用户体验与成功率都能兼顾,也符合浏览器安全策略的预期。
如果浏览器拦截了下载,用户界面上要给出明确提示,而不是让用户反复点击导出按钮。一个简单的做法是记录连续下载次数,达到阈值后提示用户浏览器已限制自动下载、请稍候再试,既尊重浏览器策略,也避免了用户无意义的重复操作。
下载是用户可见的最后一步,失败必须可感知、可恢复。常见失败原因包括:渲染异常导致 save 抛错、浏览器拦截下载、磁盘空间不足。建议把导出流程整体包进 try/catch,失败时提示用户重试,而不是静默失败,让用户以为导出成功却没有文件。
对关键业务(合同、发票),可以在下载之外增加重新生成与在线预览两个出口:预览让用户在下载前确认内容正确,重新生成则覆盖渲染失败的场景。多一层兜底,用户就少一次因为导出失败而产生的工单,业务信任度也会更高。
长文档渲染耗时长,建议在 save 前显示进度状态,save 完成后关闭;如果渲染失败,明确提示原因并保留用户已填写的数据,避免用户重填表单再导出的糟糕体验。这也是导出功能易用性的关键一环,处理得当能显著降低用户流失。
降级方案的另一层是环境感知:在支持浏览器原生下载的环境里用 save,在不支持的环境(比如部分 WebView)里改用复制链接或邮件发送的方式交付文件。为导出功能预留环境分支,兼容性会好很多,线上问题也会更少,用户始终能拿到文件。
文件名是文档的元数据,值得像代码一样定规范。建议统一格式:业务类型-主体标识-日期.扩展名,例如 合同-CT2026-0818.pdf。规范命名后,用户本地文件检索、团队共享、归档管理都会顺畅很多,也便于自动化脚本按文件名归类,长期维护成本更低。
避免使用文件系统保留字符(如 / \ : * ? " < > |)与前后空格,这些字符在部分平台会导致保存失败或文件名异常。生成文件名时统一做一次清洗,把非法字符替换为下划线,是成本极低收益明显的防御性编程,批量场景尤其值得做。
最后,把命名逻辑收敛成独立函数,比如 buildFileName(type, data),在项目里统一调用。这样命名规则变更时只需改一处,测试也方便;批量导出、单张导出、邮件附件等所有入口共用同一套命名,产品体验的一致性会明显提升,维护成本也大幅下降。
命名规范的落地需要配套检查:在测试用例里加入文件名断言,验证非法字符被清洗、长度未超限、格式符合规范。自动化测试把命名规则固化成契约,任何一次改动破坏了规范都能被及时发现,而不是等到用户反馈文件名异常才去修。
下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:
这是由 dompdf.js 渲染的示例 PDF 内容。