In Vue projects, "export PDF" is practically a standard feature of every admin dashboard: order details, financial reports, contracts, and data reports all need an export button. Traditional options go through a backend renderer — slow responses and server pressure — or draw PDFs pixel by pixel with jsPDF, which costs a lot of development effort and reproduces styles poorly. dompdf.js renders the real DOM of your Vue page directly, producing a PDF that matches the on-screen layout, with support for Chinese text, tables, and pagination, and zero backend involvement. This guide walks through integrating dompdf.js into both Vue 2 and Vue 3 projects: installation, in-component usage, and wrapping a reusable export utility, with complete code examples and pitfalls to avoid. Because the library is framework-agnostic, the integration surface is tiny: one import, one instance, one save call — and the rest of the codebase stays untouched. Vue's reactivity also plays into the library's hands: when the reactive data behind a report changes, the export automatically reflects the new state, because the DOM is re-rendered before the snapshot is taken. The examples in this guide are written to be copy-pasteable, so most teams can go from npm install to a working export button in under half an hour.
In admin systems, order details, financial statements, and contract texts all need PDF export. Operations staff and customers receive uniformly formatted official documents rather than screenshots or print previews, which makes a real difference in professionalism. Exports are also a trust signal: customers and partners judge a system's maturity by whether it produces proper documents, and a well-formatted PDF carries more weight than a screenshot or a printed webpage.
In a frontend-backend separated architecture, routing every export through the backend means maintaining template engines and rendering services, slow response times, and concurrent server load. Pure frontend export completes instantly and consumes no server resources at all. Frontend export also fits agile delivery: a new export variant is a template change, not a backend release, so product teams can iterate on document design at their own pace.
Because page data is already reactive, rendering the current DOM or data directly on the frontend guarantees that what the user sees is exactly what gets exported. This what-you-see-is-what-you-get experience is the most direct expectation users have of an export feature. The reporting needs of admin systems are strikingly uniform — tables, headers, totals — which is exactly the shape of content a DOM renderer handles best.
Common export payloads in Vue admin systems include order confirmations, invoices, delivery notes, and settlement statements — all of which share a table-heavy layout that dompdf.js renders natively.
Another frequent need is batch export from filtered lists: 'export all matching orders' is a one-line loop over the filtered array, each item rendered through the same template, with auto pagination assembling the final document.
Because the export happens client-side, it also works in demos, prototypes, and offline kiosk deployments where no backend exists at all — a convenient side effect of the architecture.
Export is also a recurring theme in product demos: sales engineers who can produce a branded PDF from the live system in seconds leave a stronger impression than those who show slides, because the document is real evidence of what the product does.
Run npm install dompdf.js in the project root to install the package. It supports ES module import, so bundlers like Vite and Webpack tree-shake and bundle it normally without polluting the global scope. The package ships as standard ES modules with type declarations, so editors provide autocomplete for the API, and bundlers can statically analyze and tree-shake the imports.
Import it with import { DomPDF } from 'dompdf.js', then create an instance, add a page, and save the file — three steps complete one export. The library is completely decoupled from the framework. Version pinning is worth a moment: like any library, dompdf.js evolves, and locking the version in package.json protects the team from surprise behavior changes during upgrades.
For low-code platforms or CDN scenarios, load the built file from dist with a script tag and call the API from the global variable. Both approaches share the exact same API, so switching between them costs almost nothing. There is no configuration file, no plugin registration, and no build step — the import is the entire setup, which keeps onboarding friction close to zero.
If the project uses pnpm or Yarn workspaces, the library installs like any other dependency, and the same version is shared across all workspace packages — no special configuration required.
For teams that need to support older browsers, check the library's browser targets against the project's; if a mismatch appears, a simple Babel or esbuild transform of the dependency resolves it.
A quick smoke test after installation — create an instance, add a one-line page, save — verifies the WASM asset loads correctly in the target environment before any real integration work begins.
Grab the DOM region to export with ref and pass it directly to addPage. Both template strings and DOM nodes are accepted as input — strings suit pure data generation, DOM nodes deliver what-you-see-is-what-you-get. Pick per scenario or use both. Using a ref to the rendered region means the export reflects exactly what the user sees, including any conditional blocks and formatting that the current state produces.
Bind the export button to a click handler; after the PDF is generated, save triggers the browser download. For large payloads, add a loading state and disable the button so users cannot click repeatedly and produce duplicate files. The async/await pattern also makes error handling natural: wrap the export in try/catch and surface failures with a toast instead of leaving the user with a silent, dead button.
Vue 2's Options API works the same way: reach the node via this.$refs.content and put the logic in methods. The structure differs slightly, but the calling pattern is identical, so migrating between the two is trivial. Because the same instance can accept multiple addPage calls, a report with several sections is assembled by calling addPage once per section before a single save.
For pages where the export area is expensive to render, consider building a dedicated export-only DOM branch: a hidden container populated with a minimal, print-oriented markup that mirrors the screen layout without the interactive widgets.
When the export needs to include data that is not on screen — such as a footer with the user's department — the template string approach composes that extra content directly, without touching the visible page.
Vue's transition components and lazy-loaded images can momentarily produce an incomplete DOM; waiting for onMounted and for image load events before export avoids blank regions in the PDF.
When a page must export itself including dynamically injected styles (for example, theme colors chosen by the user), render the template as a string with those values interpolated; the PDF then matches the user's selected theme exactly.
<script setup>
import { ref } from 'vue';
import { DomPDF } from 'dompdf.js';
const contentRef = ref(null);
async function exportPdf() {
const pdf = new DomPDF();
pdf.addPage(contentRef.value, { format: 'A4', margin: '15mm' });
pdf.save('report.pdf');
}
</script>
<template>
<div>
<button @click="exportPdf">Export PDF</button>
<div ref="contentRef">
<h1>Monthly Business Report</h1>
<table>
<tr><th>Metric</th><th>This Month</th><th>MoM</th></tr>
<tr><td>Revenue</td><td>1.2M</td><td>+8%</td></tr>
</table>
</div>
</div>
</template>
When several pages need export, extract the logic into a composable such as usePdfExport. It centralizes instance creation, default configuration, and download naming, so components never repeat boilerplate code. A composable also gives the team a single place to enforce conventions, such as filename patterns, default paper sizes, and the loading-state contract shared by every export button.
The utility takes three arguments: the content source (HTML string or DOM node), page configuration (paper size, margins), and the filename, and returns a Promise. Callers only pass parameters, keeping usage clean and consistent. Because the utility returns a Promise, callers can await it and then navigate, notify, or log — the composable stays small while the calling code keeps full control of the flow.
Extracting a module makes testing and upgrades easier: later additions like headers and footers, custom fonts, or progress callbacks require a change in exactly one place, and every page picks it up automatically — no per-component edits. Default parameters keep the common case terse, while options still allow any page to override paper size or margins without forking the utility.
The same composable can be extended with an options object that maps directly to addPage arguments, so page-specific tweaks (landscape orientation, custom margins) are expressible at the call site without duplicating logic.
For applications with several export flavors, the composable can accept a template registry: callers pass a template key and data, and the composable resolves the HTML — keeping document logic out of components entirely.
Unit testing the export flow is straightforward: the composable takes plain inputs and produces a download, so tests can assert the filename and the generated Blob without mounting any component.
// composables/usePdfExport.js
import { DomPDF } from 'dompdf.js';
export function usePdfExport() {
async function exportPdf(source, options = {}, filename = 'export.pdf') {
const pdf = new DomPDF();
pdf.addPage(source, { format: 'A4', margin: '15mm', ...options });
pdf.save(filename);
}
return { exportPdf };
}
// in a component
// const { exportPdf } = usePdfExport();
// exportPdf(contentRef.value, { margin: '10mm' }, 'orders.pdf');
Vue's scoped styles attach data-attribute selectors to elements, and those styles still apply during DOM snapshot rendering, so usually nothing special is needed. If styles appear missing, first check whether the export area depends on global styles outside the component scope. A reliable trick for pagination debugging is to export a short version first: if the page break lands where expected on two pages, the longer document will follow the same rules.
Pagination is strongly tied to container width: A4 corresponds to 794px at 96 DPI. Matching the export area's width to the target paper size gives the most stable, predictable page breaks. When a report mixes wide tables and narrow columns, keep the export container width fixed and let the content wrap; letting the layout respond to the window size makes page breaks unpredictable.
Modals, navigation, and other page elements can interfere with the export area. Put the content to export in a dedicated container or a hidden print region so the layout stays clean and the exported result matches the design. Fonts are another subtle factor: if the export area inherits an unusual font stack, the PDF's line breaks can shift, so pinning the template's font family removes a variable.
Global CSS resets and framework UI libraries sometimes apply styles that distort the export area — for example, box-sizing changes or baseline grids. Containing the export region with explicit width and font settings neutralizes most of these effects.
If images inside the report load lazily, force them to load before export or render them into the template as data URLs; otherwise the PDF may capture placeholder boxes instead of the actual charts.
For reports that span many pages, spot-check the exported PDF after any template change; pagination is deterministic given the same container width, so a quick two-page inspection catches almost all regressions.
A practical checklist before release: verify the export with real data, check page breaks at the widest table in the system, and confirm the PDF opens in the viewers your customers actually use — Acrobat, browsers, and mobile previews.
Q: Does it work with SSR or server-side rendering? A: dompdf.js depends on browser DOM APIs, so under SSR it must only run on the client — for example inside onMounted or an event handler — and be skipped during the server render phase. In Nuxt, the same rule applies: guard the export call with a client-side check or run it inside a click handler, and the server render never touches the browser APIs.
Q: Will Chinese text be garbled? A: No. The built-in Source Han Sans SC font produces proper Chinese without any configuration. If your company mandates a specific typeface, embed it explicitly via font configuration. Note that canvas content is pixel data, so export charts at the intended display resolution; the surrounding text and tables remain vector.
Q: Large tables make export sluggish — what now? A: Enable pagination, match the container width to the paper, and avoid rendering oversized DOM at once. For very large datasets, add pages in batches to keep generation smooth. The rule of thumb is simple: anything that touches document or window belongs in the client lifecycle, never in the render phase.
Q: Can I export several independent regions into one PDF? A: Yes — call addPage once per region, then save once; the result is a single multi-page PDF, which is ideal for combined reports.
Q: What happens if a user exports while data is still loading? A: Guard the button with a loading flag bound to the request state; the click handler checks it and returns early, preventing blank exports and duplicate files.
Q: Does the export work while the dev server hot-reloads? A: Yes — export reads the real DOM, so hot reload only matters in that you should export after the template has settled, not mid-update.
下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:
这是由 dompdf.js 渲染的示例 PDF 内容。