← dompdf.js Studio

微前端架构集成 dompdf.js 指南

微前端把单体应用拆成多个独立部署的子应用,每个子应用有自己的技术栈、构建产物与运行时,这给 PDF 导出带来了独特的问题:导出模块应该放在哪个应用里?样式隔离会不会影响渲染结果?内容来自多个子应用时,快照如何跨应用收集?dompdf.js 纯前端、无服务依赖的特性恰好适合微前端场景——它不要求统一的构建体系,任何子应用都可以独立引入;渲染基于真实 DOM 与 CSS,只要快照内容完整,样式隔离不构成障碍。本文先分析微前端下 PDF 导出的典型挑战,再给出共享导出能力的三种架构方案与代码示例,然后讲解跨应用快照、样式与字体处理、构建配置,最后总结最佳实践,把导出能力做成可复用的公共设施。

微前端场景的导出挑战

微前端下最常见的导出问题是职责归属:导出逻辑放在主应用,子应用的页面内容无法直接访问;放在每个子应用,同样的代码重复实现、重复维护,版本还容易漂移。dompdf.js 的轻量接入特性让两种方案都可行,但更好的做法是把导出能力抽成独立的公共模块,由主应用统一加载,子应用通过消息或共享事件发起导出,职责边界清晰。

第二个挑战是样式隔离:子应用的 CSS 通常被 scoped 或沙箱化,导出的 HTML 如果只取内容而不带样式,PDF 就会丢失版式。解决思路是让快照携带计算后的样式,或者让导出模板独立于页面样式自包含定义,两者结合可以保证 PDF 观感与页面一致,用户不会抱怨导出结果和看到的页面不一样。

第三个挑战是跨应用内容:一份报表可能由多个子应用的组件拼成。此时需要把各子应用的 DOM 片段收集到统一的导出容器里,再由导出模块统一渲染;收集过程要处理影子 DOM 与 iframe 的边界,把内容平铺到快照中,这一步是微前端导出的核心工作量,也是方案设计时最需要提前规划的环节。

代码示例:三种共享导出架构

方案一:主应用内置导出模块,子应用通过自定义事件触发。导出逻辑只写一份,子应用零依赖,适合子应用技术栈杂、不便统一引入库的场景;代价是跨应用传内容需要序列化,大内容有性能开销,事件名与消息格式要提前约定并写进文档,避免各子应用自行发挥。

方案二:导出能力做成独立 npm 包,各子应用按需引入。依赖显式、类型完整,适合团队统一技术栈的场景;注意所有子应用使用同一版本,避免多个 DomPDF 版本并存造成的行为差异。方案三:主应用把导出模块通过 module federation 暴露为远程模块,子应用运行时拉取,兼具方案一的单一实现与方案二的显式依赖,是大型团队的首选。

// 方案一:主应用注册全局导出服务,子应用通过事件触发
// 主应用代码
import { DomPDF } from 'dompdf.js';

window.addEventListener('app:export-pdf', async (event) => {
  const { html, filename } = event.detail;
  const pdf = new DomPDF();
  pdf.addPage(html, { format: 'A4' });
  pdf.save(filename || 'export.pdf');
});

// 子应用代码:发起导出
window.dispatchEvent(new CustomEvent('app:export-pdf', {
  detail: {
    html: document.querySelector('#report').innerHTML,
    filename: 'report.pdf',
  },
}));

跨应用快照收集

跨应用快照的收集顺序很重要:先让各子应用把需要导出的 DOM 片段克隆到共享容器,再统一生成 PDF。克隆时用 cloneNode(true) 深拷贝,避免导出过程影响原页面;iframe 内的内容需要先访问其 contentDocument 再克隆,影子 DOM 的内容则要穿透 shadowRoot 逐层收集,两种边界都要处理。

收集到的片段要携带样式:优先内联关键样式,包括宽度、字体、颜色、边距,或者把子应用的样式表内容一并注入快照的 style 标签。样式注入后,PDF 渲染与页面呈现一致,不会出现导出结果和页面不一样的经典投诉,这也是微前端导出最容易踩的坑,值得在联调阶段重点验证。

图片与字体是快照的另外两个坑:跨域图片要确保 CORS 可访问,否则 PDF 里图片空白;字体要随快照声明,子应用用的自定义字体在导出容器里同样要可加载。快照收集完成后,用渲染预览先自查一遍,再交付给用户,把问题挡在发布之前,比事后补救成本低得多。

快照收集的时序也要统一:所有子应用都完成克隆之后,再统一触发导出,避免有的子应用内容还没就绪就开始渲染。实现上可以用 Promise.all 汇总各子应用的克隆结果,全部 resolve 后再调用导出模块;时序统一后,导出结果稳定可复现,不会出现这次有内容、下次缺内容的诡异问题,跨应用协作的可靠性就有了保障。

样式隔离与字体处理

微前端的样式隔离在生产页面有效,但导出时快照脱离原环境,隔离机制全部失效。因此导出模板必须自包含:所有样式写进模板的 style 标签,不使用外部样式表依赖;颜色、字体、尺寸显式声明,不依赖继承上下文,PDF 在任何环境渲染结果一致,这是微前端导出稳定性的基石。

字体处理建议统一策略:导出模板统一使用内置思源黑体加一两个品牌字体,子应用不再各自声明字体。这样快照体积小、加载快,PDF 观感统一;确实需要品牌字体的子应用,把字体文件作为公共资源由主应用提供,子应用只引用,避免每份快照重复内联几 MB 字体,体积与加载时间都能控制住。

还有一个容易忽略的细节:页面级 CSS 变量在快照里不会自动带入。模板里用到的 var(--xxx) 要在 style 标签里显式定义,或者干脆用字面量值,否则导出结果会缺样式。检查清单里加上无外部依赖、无未定义变量两条,导出稳定性就能大幅提升,联调阶段少走弯路。

代码示例:module federation 共享导出模块

用 module federation 共享导出模块时,主应用作为提供方暴露模块,子应用作为消费方按需拉取。上面给出了两侧配置:提供方把导出模块暴露为 remote,消费方在 remotes 里声明地址,运行时自动加载,版本独立演进、互不阻塞,子应用发布不再依赖主应用。

module federation 的注意点:remoteEntry.js 要部署在可公开访问的地址且带版本管理,避免子应用拉到旧版本;共享依赖通过 shared 配置声明,避免重复加载。导出模块不依赖具体框架,作为纯函数模块暴露最干净,任何子应用都能消费,这也是把导出能力平台化的推荐形态。

// 主应用 webpack.config.js —— 暴露导出模块
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host_app',
      filename: 'remoteEntry.js',
      exposes: {
        './pdfExporter': './src/pdfExporter.js',
      },
    }),
  ],
};

// 子应用 webpack.config.js —— 消费导出模块
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'child_app',
      remotes: {
        host_app: 'host_app@https://host.example.com/remoteEntry.js',
      },
    }),
  ],
};

// 子应用业务代码
import('host_app/pdfExporter').then(({ exportPdf }) => {
  exportPdf(document.querySelector('#report').innerHTML, 'report.pdf');
});

最佳实践总结

总结微前端集成的四条原则:导出能力单一实现、集中维护;快照自包含样式与字体,不依赖运行环境;跨应用内容统一收集到共享容器后再导出;构建层面用公共包或 module federation 分发,版本统一管理。四条原则覆盖了架构、内容、样式与分发四个层面,缺一不可。

上线前建议做一次跨应用导出的联调用例:在主应用发起包含多个子应用内容的导出,验证样式、字体、图片、分页全部正确;把用例沉淀为自动化测试,每次子应用升级后自动回归,微前端体系变化频繁,回归测试是导出质量最可靠的保障,能拦截绝大多数回归问题。

性能方面,微前端导出同样遵循分段与队列原则:跨应用内容按子应用分块 addPage,导出任务走统一队列,避免多个子应用同时发起导出导致资源竞争。把导出做成平台的公共能力后,各子应用的功能开发与导出能力解耦,迭代效率与文档质量同步提升,团队的工程体验也会明显改善。

⚡ 现场演示(点击生成 PDF)

下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:

Hello from dompdf.js!

这是由 dompdf.js 渲染的示例 PDF 内容。