Once your content has been collected through addPage, save is the final step: it triggers the actual render and download, turning the in-memory document into a PDF file on the user's disk. The API looks deceptively simple — just a filename argument — but real usage hides a surprising amount of detail. Can filenames contain Chinese characters? How do you name files dynamically from data? How do you avoid the browser silently blocking downloads during batch export? And how do you give users a sensible path when a download fails? These details determine whether the feature works at all and how smooth the experience feels. This guide digs into how save works and when it fires, covers static and dynamic template naming, naming conventions for multi-page documents, and walks through batch downloading and failure fallbacks. We also look at how to verify pre-save state and how to structure naming logic so it lives in the data layer. By the end you will have made that last mile solid, turning your export feature from merely functional into genuinely pleasant to use. We also include a filename sanitization routine you can adopt directly, an audit-friendly naming layer, and a fallback matrix that guarantees every failure class has an exit. The guiding question throughout is equally simple: what does the user actually receive, and can we make sure it is exactly the right file, named exactly right, every single time?
save(filename) takes a filename argument and internally runs two phases: first it renders — handing all content previously collected through addPage to the WASM core to produce the PDF bytes — and then it delivers the result as a file download to the local machine. In other words, addPage only collects content; the heavy lifting happens entirely inside save. The larger the document, the longer this phase takes, and planning around that latency is part of building a good export feature.
This timing has practical consequences. If page resources (images, fonts) are not ready before save runs, the rendered output will be missing content; and once save fires, the entire document renders in one pass. For long documents this phase is noticeably slow, so the UI thread needs to stay responsive — show a loading indicator rather than leaving users staring at an apparently frozen page and clicking the button repeatedly.
From the caller's perspective, save triggers the download directly. There is no need to create an anchor element or a Blob URL yourself — that plumbing is encapsulated internally. If you need the PDF Blob for other purposes, such as uploading to a server or opening an in-app preview, combine save's download responsibility with the lower-level capabilities documented by the official project. save stays single-purpose and simple, and you stay in control of the rest of the pipeline.
One subtle but useful consequence of the two-phase design: validation of the final document is only meaningful after save completes. If you want to confirm page count, spot-check content, or record render duration, hook into the moments before and after save — before to capture start time, after to capture outcome — and you get a complete audit trail of the export for free.
From the user's perspective, save is the entire journey from pressing the export button to the file appearing in the download bar. To make that journey reliable, check page state before calling save: data loaded, content non-empty, template rendered. Intercept any failing condition early instead of discovering after the download that the file is empty — a small preflight that eliminates a whole category of silent failures.
The filename can be a fixed string — save('report.pdf') — which fits single-purpose export buttons. Or it can be a template string that embeds business data, such as save(`sales-report-${month}.pdf`), which suits reports, orders, and invoices where each export must be distinguishable from every other. The dynamic form is what turns a generic download button into a versioned document delivery system.
Dynamic naming earns its keep through traceability: after download, users can tell from the filename alone which record, time period, and version the file corresponds to — without opening it. Large collections never overwrite each other, and sharing files later stays unambiguous. Keep the naming scheme aligned with your business system: type prefix plus business identifier plus date, for example invoice-INV20260818-001.pdf, so filenames sort naturally and stay searchable.
Chinese filenames usually survive the browser download flow intact, but cross-platform transport (email attachments, cloud-sync folders) occasionally mangles encodings. The safe play is to keep filenames mostly in universal characters, or to keep Chinese filenames reasonably short — Windows and several cloud drives impose filename length limits, and exceeding them fails the save outright. A length cap enforced at naming time costs nothing and prevents an entire category of support tickets.
Whatever scheme you adopt, centralize it. One buildFileName(type, data) function used by every export entry point — single export, batch export, email attachments — means the naming rule changes in exactly one place, tests cover it once, and the product experience stays consistent everywhere the feature appears.
A structural recommendation: compute filenames in the data layer, not the UI layer. When you fetch the business record, derive the filename alongside it and carry both forward, so the export function simply receives a filename and saves it. Naming logic stays decoupled from presentation, batch loops enforce one consistent rule by construction, and testing only needs to cover the data layer.
A practical sanitization routine illustrates the principle: take the raw filename, replace filesystem-reserved characters with underscores, strip leading and trailing whitespace, collapse repeated separators, and truncate to a safe length while preserving the .pdf extension. Run every generated name through this routine inside buildFileName, so no call site can forget it. The routine itself is a pure function — trivially unit-testable and easy to reason about — and once it exists, the entire class of filename-related save failures disappears from the project. Users get clean, consistent, shareable filenames on every platform, and the support inbox stops hearing about files that would not save or arrived with mangled names. This is defensive programming at its cheapest: a few lines of code, one test file, and a whole category of production incidents retired for good.
This example shows dynamic naming for a single invoice and a serialized batch loop for many invoices. Note the small delay between iterations: browsers throttle automatic downloads, and spacing the calls out keeps every file from being silently dropped.
import { DomPDF } from 'dompdf.js';
// Single export: dynamic filename from business data
function exportInvoice(invoice) {
const pdf = new DomPDF({ format: 'A4', margin: '20mm' });
pdf.addPage(renderInvoice(invoice));
pdf.save(`invoice-${invoice.no}-${invoice.date}.pdf`);
}
// Batch export: serial execution, each file named independently
async function exportAll(invoices) {
for (const inv of invoices) {
exportInvoice(inv);
// small pause between downloads avoids browser throttling
await new Promise(r => setTimeout(r, 300));
}
}
exportAll([{ no: 'INV001', date: '2026-08-18' }]);
When downloads are triggered in rapid succession, the browser may silently block some of them. This is the browser's anti-nagging protection, not a dompdf.js defect. Leaving small gaps between downloads, or letting the user trigger each one by click, reliably avoids the block; a tight loop firing save after save often results in only the first few files actually downloading, which users read as a broken feature.
The more robust batch pattern is generate one, download one, and pace the loop. Downloads triggered inside a user event handler — after a button click — are treated more leniently than automatic downloads fired from arbitrary async code. Give users something to see and do between files, and browsers cooperate far more often than they do with an unbroken burst of programmatic downloads.
When the business genuinely needs one-click export of many documents (batch certificates, for example), offer two entry points in the UI: export one at a time, which downloads a single file per click and is maximally reliable, and export all, which runs serially and downloads each file the moment it finishes. Pair the second with a progress indicator. Reliability and experience both improve, and the design stays within what browsers will actually allow.
It is also worth documenting the expectation: batch downloads of dozens of files will take visible time, and some browsers show a permission prompt after a handful of downloads. A short helper note in the UI — 'this will download N files' — sets expectations, prevents users from closing the tab mid-run, and reduces the confusion that produces duplicate-click bug reports.
When the browser does throttle or block downloads, surface it in the UI instead of letting users hammer the export button. Track consecutive download attempts, and after a threshold, show a message such as 'the browser has limited automatic downloads, please try again in a moment'. Respecting the browser's policy and guiding the user beats silent retries every time.
Download is the last user-visible step, so failures must be perceivable and recoverable. Common causes include a render exception that makes save throw, the browser blocking the download, and insufficient disk space. Wrap the whole export flow in try/catch, and on failure tell the user what happened and how to retry — never fail silently, which leaves users believing the export succeeded and hunting for a file that does not exist.
For critical documents (contracts, invoices), add two escape hatches beyond the download itself: a preview that lets users confirm the content before downloading, and a regenerate action that covers render failures. Each extra fallback removes one support ticket and one moment of user frustration. Preview is especially valuable because it turns 'download and hope' into 'verify, then download' — a dramatically more trustworthy flow for legally significant documents.
Long documents render slowly, so show progress before save and close it after save completes. If rendering fails, state the reason clearly and preserve whatever data the user already entered; forcing users to re-enter a form before retrying an export is the kind of friction that quietly drives users away. Handling the failure path with the same care as the success path is the difference between a feature that works and a feature that feels reliable.
Finally, log failures with context: filename, page count, browser version, and the error message. When a user reports a failure, the log tells you instantly whether it was a template problem, a resource problem, or an environment problem — and with version numbers attached, whether a recent upgrade introduced a regression. Cheap to add, invaluable months later.
A further layer of fallback is environment awareness: where the browser supports native downloads, use save; in constrained environments such as some WebViews, deliver the file through a link or email instead. Branching the delivery path by environment keeps compatibility high and production incidents low, and users always walk away with the file one way or another.
A filename is document metadata, and it deserves a convention as deliberate as your code style. A uniform format — business type, entity identifier, date, extension, for example contract-CT2026-0818.pdf — makes local file search, team sharing, and archival management measurably smoother, and lets automation scripts classify files by name. The convention is a small up-front investment with compounding returns.
Avoid filesystem-reserved characters (/ \ : * ? " < > |) and leading or trailing spaces in names; on several platforms they cause the save to fail outright or produce mangled filenames. Sanitize generated filenames uniformly, replacing illegal characters with underscores. This is cheap, defensive programming with an outsize payoff — especially in batch flows where a single bad record could otherwise poison the whole export run.
Finally, funnel all naming through one helper function, such as buildFileName(type, data), used across the project. Naming rules then change in exactly one place and are trivially testable; batch export, single export, and email attachments share one naming vocabulary, product consistency improves visibly, and maintenance cost drops to near zero.
Taken together — a documented format, sanitization, and a single implementation point — filename handling stops being a source of bugs and becomes a quiet strength of the export feature. Users notice, because files that arrive named well are files they can trust at a glance.
Make the convention enforceable: add filename assertions to your test suite that verify illegal characters were sanitized, length stayed under the limit, and the format matches the spec. Automated tests turn the naming rule into a contract — any change that breaks it is caught immediately, instead of surfacing months later as a user complaint about a mangled filename.
下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:
这是由 dompdf.js 渲染的示例 PDF 内容。