A microfrontend splits a monolithic application into independently deployed sub-applications, each with its own tech stack, build output, and runtime. That architecture raises genuinely unfamiliar questions for PDF export. Where should the export module live? Sub-applications sandbox their CSS — does style isolation break rendering? A user initiates export in one sub-app, but the content may come from several sub-apps; how do you collect a snapshot across application boundaries? dompdf.js is a good fit for this environment precisely because it is pure frontend with no server dependency. It does not require a unified build system — any sub-application can import it independently. And because it renders real DOM and CSS, style isolation is not an obstacle as long as the snapshot carries complete content and styles. This guide first analyzes the characteristic export challenges of microfrontend architecture, then presents three architecture options for sharing export capability with code, then covers cross-application snapshot collection, style and font handling, Module Federation configuration, versioning of the shared module, and a best-practices summary. The goal is to make PDF export a reusable platform facility rather than duplicated, drifting code in every sub-application. The three architecture options — a global event service, a shared npm package, and a Module Federation remote — are presented with working code and honest trade-offs, followed by the cross-application snapshot mechanics that make multi-app export actually work, and the versioning and observability practices that keep the shared capability maintainable as the platform grows. The best-practices section condenses the whole article into an integration checklist you can take to a design review.
The most common problem is ownership: where does the export logic live? Put it in the host application and it cannot reach sub-application content directly; put it in every sub-application and the same code is reimplemented and maintained in parallel, drifting apart with every change. dompdf.js's lightweight integration makes either approach viable, but the better answer is to extract export into a shared module owned by the platform, with sub-applications triggering it through messages or shared events. Clear ownership prevents the duplication that otherwise accumulates quickly.
Style isolation is the second challenge. Sub-application CSS is typically scoped or sandboxed, so exporting raw innerHTML loses all styling and the PDF comes out unstyled. The fix is to make the snapshot carry the styles it needs: inline the critical computed styles, or define the export template self-contained with its own style block. Either way the PDF reflects the page the user saw, and the classic complaint that the export does not match the page disappears.
The third challenge is cross-application content. A report may assemble components from several sub-applications — a chart from one, a table from another, a header from a third. The architecture must collect DOM fragments from each sub-application into a shared export container, then render the combined snapshot. Collecting across shadow DOM and iframe boundaries, flattening content into one document, is the core engineering work of microfrontend export, and it deserves design attention before the first line of export code is written.
There is a fourth, subtler challenge: consistency of experience. Users do not care which sub-application owns the export button; they expect one download experience across the whole platform. A shared export module gives every sub-application the same progress indicator, the same filename conventions, and the same error handling, which is exactly the kind of platform-level consistency that builds user trust in a microfrontend system.
Performance planning completes the picture. A shared export module used by every sub-application becomes a shared bottleneck: concurrent export requests from different sub-apps can compete for CPU and memory. A platform-level queue serializes these requests, and the queue becomes the natural place to add rate limiting, priorities, and observability. What looks like a convenience feature at first is really platform infrastructure.
Option one: the host application registers a global export service, and sub-applications trigger it with a custom event. Export logic exists exactly once, sub-apps carry zero dependency on the library, and this suits heterogeneous tech stacks where standardizing on one import is impractical. The costs are the serialization of content across the event boundary and the need to document the event contract — the message format must be agreed upon and versioned, or sub-apps will improvise their own payloads.
Option two: publish export as a standalone npm package that sub-applications import directly. Dependencies become explicit and typed, which suits teams with a unified stack; the discipline required is version alignment — all sub-apps should run the same version to avoid behavioral divergence between exports. Option three: the host application exposes the export module through Module Federation as a remote, and sub-applications pull it at runtime. This combines option one's single implementation with option two's explicit dependency, and it is the recommended shape for larger organizations with independent release trains.
Whichever option you choose, define the shared interface first: the function signature, the accepted options, and the error contract. A stable interface lets sub-applications build against the capability while the implementation evolves underneath — swap the event-based implementation for Module Federation later without touching sub-app code, as long as the interface stays the same.
The interface should also hide environment concerns. Sub-applications should not need to know whether export runs on the main thread or in a worker, or whether fonts are self-hosted. Encapsulate those decisions inside the shared module; the sub-app passes content and options, and the module owns the rest. This encapsulation is what keeps the platform capability maintainable as the library underneath evolves.
// Option 1: the host app registers a global export service;
// sub-apps trigger it with a CustomEvent.
// Host application code
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');
});
// Sub-application code: request an export
window.dispatchEvent(new CustomEvent('app:export-pdf', {
detail: {
html: document.querySelector('#report').innerHTML,
filename: 'report.pdf',
},
}));
Snapshot collection has a strict order: sub-applications first clone the DOM fragments they want to export into a shared container, and only then does the export module render the combined snapshot. Cloning uses cloneNode(true) for a deep copy so the export never disturbs the live page. Content inside iframes requires reaching into the frame's contentDocument before cloning, and shadow DOM content must be collected by walking shadowRoot boundaries layer by layer — both boundaries are routine in microfrontend layouts and both need explicit handling.
Collected fragments must carry their styles. The pragmatic approach is to inline the critical styles — width, fonts, colors, margins — or to inject each sub-application's stylesheet content into the snapshot's style tag. Once styles are embedded, PDF rendering matches the on-page presentation, and the most common microfrontend export complaint disappears. This step deserves explicit verification during integration testing, because style loss is silent: the PDF is produced, it just looks wrong.
Images and fonts are the other two snapshot hazards. Cross-origin images must be CORS-accessible or they render blank in the PDF; fonts used by sub-applications must be loadable from the export container as well. A render preview after collection — before delivery to users — catches both problems cheaply. Add these checks to the integration test suite so regressions surface in CI rather than in customer-facing files.
Timing must be unified across sub-applications. All fragments should be cloned before export starts, or a slow sub-app can produce a partial snapshot. Promise.all over each sub-app's clone operation, with export triggered only after every promise resolves, makes the outcome deterministic — the same content always yields the same PDF, and the intermittent missing-section bug disappears.
Finally, treat snapshot collection as data, not magic. Log which sub-applications contributed fragments, how large each fragment was, and how long collection took. When a customer reports a missing section, the log answers the first question immediately — did the sub-app actually contribute its fragment? — and points the investigation at the right layer.
Microfrontend style isolation — scoped CSS, CSS Modules, sandboxing — works on the live page, but a snapshot leaves that environment, and the isolation mechanisms vanish with it. The export template must therefore be self-contained: every style lives inside the template's own style tag, with no dependency on external stylesheets, and colors, fonts, and dimensions are declared explicitly rather than inherited from context. A self-contained template renders identically in any environment, which is the foundation of deterministic microfrontend export.
Fonts deserve a platform-level policy. The default recommendation is a small standard set: the built-in Source Han Sans SC for Chinese body text plus one or two brand faces, declared once in the export template rather than re-declared by every sub-application. Snapshots stay small and load fast, and PDFs look consistent across the platform. Sub-applications that genuinely need their own brand fonts should reference platform-provided resources instead of inlining megabytes of font data into every snapshot.
CSS variables are a classic silent failure. A sub-application's template may use var(--brand-color), which resolves fine on the live page but is undefined in an isolated snapshot. Either define every variable explicitly in the template's style tag or use literal values. Adding two rules to the review checklist — no external style dependencies, no undefined variables — eliminates an entire class of export-styling bugs before they reach users.
Also consider responsive differences. The live page may render at a desktop width while the PDF page has its own dimensions. Explicitly set the export container's width to match the target page width, the same discipline as any dompdf.js project, so layout computed for the PDF matches what the template author intended rather than inheriting the sub-application's viewport.
With Module Federation, the host application exposes the export module as a remote, and sub-applications consume it at runtime. The configuration above shows both sides: the provider declares the exposed module in its ModuleFederationPlugin, and the consumer lists the remote's URL in its remotes block. Sub-applications load the module on demand, and the two release trains evolve independently — a sub-app can ship without waiting for the host, and the host can upgrade the export implementation without coordinating a simultaneous release.
Module Federation has its own operational concerns. remoteEntry.js must be deployed at a stable, publicly reachable URL with version management, or sub-applications silently keep loading stale versions. Shared dependencies should be declared through the shared configuration so frameworks like React or Vue are not duplicated per remote. Keeping the export module framework-agnostic — a pure function module with no UI — keeps it consumable by every sub-application regardless of their stacks.
Version management deserves explicit design. Because remotes resolve at runtime, the version actually served is whichever remoteEntry is live, not the version declared at build time. Coordinate deployment ordering: update the host's export module and verify the sub-application against it before cutting over. A smoke test that exercises the export path after every host deployment catches version drift early.
If the host and sub-applications are served from different origins, check the CORS and caching headers on remoteEntry.js carefully. A CDN-cached remoteEntry can pin all sub-applications to an old export implementation; configure cache invalidation aligned with the host's release cycle, and treat the remoteEntry as part of the deployment artifact set rather than an ordinary static file.
// Host application webpack.config.js — expose the export module
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host_app',
filename: 'remoteEntry.js',
exposes: {
'./pdfExporter': './src/pdfExporter.js',
},
}),
],
};
// Sub-application webpack.config.js — consume the export module
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'child_app',
remotes: {
host_app: 'host_app@https://host.example.com/remoteEntry.js',
},
}),
],
};
// Sub-application business code
import('host_app/pdfExporter').then(({ exportPdf }) => {
exportPdf(document.querySelector('#report').innerHTML, 'report.pdf');
});
Whatever sharing mechanism you choose — event service, npm package, or Module Federation — versioning the export capability is non-negotiable. Define a semantic version for the export module's interface, not just the underlying library version, and record it in the snapshot metadata. When a sub-application produces a PDF, the version stamp travels with the file, which makes support questions answerable: the file's version identifies the exact implementation that generated it.
Deprecation policy matters in a microfrontend world where consumers upgrade on their own schedule. Announce interface changes one release ahead, keep a compatibility window during which both the old and new signatures work, and log usage of the deprecated path so you know when it is safe to remove. Without this discipline, the shared export module becomes the platform's most feared dependency, and teams start reimplementing local export just to escape it.
Delivery mechanisms have different trade-offs worth revisiting at scale. The npm package gives the strongest type guarantees but the weakest runtime coupling: every sub-app builds with its own copy, so version skew is possible even with alignment policies. Module Federation gives the strongest runtime coupling — one implementation, always current — but adds the runtime dependency and its operational surface. Many platforms run both: the package for sub-apps that need build-time guarantees, the remote for sub-apps that want zero maintenance.
Observability belongs in the shared module itself. Emit a metric per export with the calling sub-application, the template used, the page count, and the duration. In a microfrontend system, these metrics reveal which sub-apps export most, which templates are slowest, and whether a sub-app's upgrade changed its export behavior — data that turns platform-level decisions from opinion into evidence.
Four principles summarize microfrontend export integration. First, implement export once and centralize its maintenance — duplication across sub-applications is the root of most divergence. Second, make snapshots self-contained: embedded styles and fonts, no reliance on the originating environment. Third, collect cross-application content into a shared container with unified timing before rendering. Fourth, distribute the capability through a package or Module Federation with versioned interfaces and explicit deprecation policies.
Before launch, run a cross-application integration test: initiate an export from the host that includes content from multiple sub-applications, and verify styles, fonts, images, and pagination end to end. Codify that test as an automated regression that runs whenever any sub-application upgrades. Microfrontend systems change constantly — a new sub-app, a framework upgrade, a CSS refactor — and the regression suite is the only reliable guard that export quality survives the churn.
Performance follows the same rules as a monolithic export, applied at platform scale: content from different sub-applications maps naturally to separate addPage blocks, and export jobs flow through a platform queue so concurrent requests from different sub-apps serialize instead of competing. Rate limiting and priorities live at the queue layer, keeping the shared capability stable under load.
Finally, treat the export module as platform infrastructure with an owner. It has an interface contract, a versioning policy, metrics, and a documented operating model. Sub-applications consume it like a service. When export is platform infrastructure rather than a feature each team reimplements, feature development and document quality improve together — sub-app teams focus on their product, and the platform guarantees that every PDF the system produces is consistent, correct, and maintainable.
Adopting the checklist does not require a big-bang migration: each principle can be introduced incrementally, starting with centralized ownership and self-contained snapshots, which deliver most of the benefit immediately. The remaining practices — federation, versioning, observability — layer on as the platform matures, and every layer reduces the duplication and drift that plague ad-hoc microfrontend export.
下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:
这是由 dompdf.js 渲染的示例 PDF 内容。