合并 PDF 文件是文档管理应用程序中的常见需求。例如,React 应用程序可能需要在文件归档或共享之前,将发票、报告、合同或扫描页面合并为一个 PDF。当源文档无需上传到服务器时,在浏览器中执行操作还可以简化工作流程。
在本教程中,您将学习如何使用 Spire.PDF for JavaScript 在 React 应用程序中合并 PDF 文档。第一个示例一次性合并多个完整的 PDF 文件。第二个示例提供了更精确的控制,从不同的 PDF 中选取指定页面并添加到新文档中。
本页内容:
在 React 项目中安装 Spire.PDF for JavaScript
在 React 项目的根目录中打开终端,安装 spire.office 包:
npm i spire.office
安装完成后,将安装包中的以下运行时文件和文件夹复制到 React 项目的 public 文件夹中:
public/
├── _framework/
├── spire.pdf.js
├── Spire.Pdf.Wasm.zip
├── spire.common.js
└── Spire.Common.Wasm.zip
JavaScript 加载器、WebAssembly 资源和配套框架文件在应用程序运行时必须作为静态资源保持可访问。有关详细的设置说明和确切的集成过程,请参阅 如何在 React 项目中集成 Spire.PDF for JavaScript。
对于本文中的示例,还需将输入的 PDF 文件放置在 public 文件夹中,以便应用程序可以通过 fetch() 获取它们:
public/
├── input_1.pdf
├── input_2.pdf
├── input_3.pdf
└── ...
在 React 中合并多个 PDF 文档
如果源文件的每一页都应出现在结果中,最直接的方法是使用 PdfMerger.Merge() 方法。它接受一个输入文件路径数组,按照数组中的顺序合并文件,并将结果写入 WebAssembly 虚拟文件系统。
以下 React 组件将 input_1.pdf、input_2.pdf 和 input_3.pdf 合并为一个名为 MergedPdf.pdf 的文档:
import React, { useState, useEffect } from 'react';
function App() {
const [wasmModule, setWasmModule] = useState(null);
const [isGenerating, setIsGenerating] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
useEffect(() => {
(async () => {
try {
const publicUrl = process.env.PUBLIC_URL || '';
const spireModule = await import(/* webpackIgnore: true */ `${publicUrl}/spire.pdf.js`);
const rawModule = spireModule.default || spireModule;
window.wasmModule = typeof rawModule === 'function'
? await rawModule({ locateFile: p => p.endsWith('.wasm') ? `${publicUrl}/${p}` : p })
: rawModule;
setWasmModule(window.wasmModule);
} catch (error) {
console.error('Failed to load spire.pdf.js:', error);
}
})();
}, []);
const loadPdfToVfs = async (fileName) => {
const publicUrl = process.env.PUBLIC_URL || '';
const response = await fetch(`${publicUrl}/${fileName}`);
if (!response.ok) {
throw new Error(`Failed to load ${fileName}: ${response.status}${response.statusText}`);
}
const fileBytes = new Uint8Array(await response.arrayBuffer());
const pdfHeader = String.fromCharCode(...fileBytes.slice(0, 4));
if (pdfHeader !== '%PDF') {
throw new Error(`${fileName} was loaded, but it is not a valid PDF file.`);
}
window.dotnetRuntime.Module.FS.writeFile(fileName, fileBytes, { flags: 'w+' });
return fileName;
};
const MergePdfs = async () => {
const wasmModule = window.wasmModule?.spirepdf;
if (!wasmModule || isGenerating) {
return;
}
setIsGenerating(true);
setErrorMessage('');
try {
const inputFiles = await Promise.all([
loadPdfToVfs('input_1.pdf'),
loadPdfToVfs('input_2.pdf'),
loadPdfToVfs('input_3.pdf'),
]);
const outputFileName = 'MergedPdf.pdf';
const mergeOp = new wasmModule.MergerOptions();
wasmModule.PdfMerger.Merge({
inputFiles,
outputFile: outputFileName,
pdfMergeOptions: mergeOp
});
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const modifiedFile = new Blob([modifiedFileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(modifiedFile);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) {
console.error('Failed to merge PDFs:', error);
setErrorMessage(error.message || 'Failed to merge PDFs.');
} finally {
setIsGenerating(false);
}
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Merge PDF Documents in React</h1>
<button onClick={MergePdfs} disabled={!wasmModule || isGenerating}>
{isGenerating ? 'Generating...' : 'Generate'}
</button>
{errorMessage && <p style={{ color: 'crimson' }}>{errorMessage}</p>}
</div>
);
}
export default App;
输出:

代码工作原理
该组件首先在 useEffect() 中加载 spire.pdf.js。由于模块是异步初始化的,因此 Generate 按钮在运行时就绪之前保持禁用状态。
loadPdfToVfs() 函数随后为每个源文档执行三项任务:
- 使用
fetch()从public目录获取 PDF。 - 检查前四个字节是否为
%PDF签名,以帮助捕获缺失文件或非 PDF 响应。 - 将文件字节写入 WebAssembly 虚拟文件系统,以便 Spire.PDF 可以访问它们。
所有三个文件加载完成后,PdfMerger.Merge() 按照 inputFiles 指定的顺序合并它们。输出从虚拟文件系统中读取,转换为 PDF Blob,并通过临时对象 URL 下载。
要更改合并顺序,只需重新排列数组中的条目即可。例如,以下顺序会将 input_3.pdf 放在最前面:
const inputFiles = await Promise.all([
loadPdfToVfs('input_3.pdf'),
loadPdfToVfs('input_1.pdf'),
loadPdfToVfs('input_2.pdf'),
]);
在 React 中合并来自不同 PDF 文档的选定页面
并非总是需要合并完整的文档。您可能需要从一个文件中提取封面页,从另一个文件中提取页面范围来创建新的 PDF。在这种情况下,将源文件加载为 PdfDocument 对象,并使用 InsertPage() 和 InsertPageRange() 来构建输出文档。
以下示例从 input_1.pdf 中提取第一页,追加 input_2.pdf 的所有页面,并将选定的内容保存为 MergedPdf.pdf:
import React, { useState, useEffect } from 'react';
function App() {
const [wasmModule, setWasmModule] = useState(null);
const [isGenerating, setIsGenerating] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
useEffect(() => {
(async () => {
try {
const publicUrl = process.env.PUBLIC_URL || '';
const spireModule = await import(/* webpackIgnore: true */ `${publicUrl}/spire.pdf.js`);
const rawModule = spireModule.default || spireModule;
window.wasmModule = typeof rawModule === 'function'
? await rawModule({ locateFile: p => p.endsWith('.wasm') ? `${publicUrl}/${p}` : p })
: rawModule;
setWasmModule(window.wasmModule);
} catch (error) {
console.error('Failed to load spire.pdf.js:', error);
}
})();
}, []);
const loadPdfToVfs = async (fileName) => {
const publicUrl = process.env.PUBLIC_URL || '';
const response = await fetch(`${publicUrl}/${fileName}`);
if (!response.ok) {
throw new Error(`Failed to load ${fileName}: ${response.status}${response.statusText}`);
}
const fileBytes = new Uint8Array(await response.arrayBuffer());
const pdfHeader = String.fromCharCode(...fileBytes.slice(0, 4));
if (pdfHeader !== '%PDF') {
throw new Error(`${fileName} was loaded, but it is not a valid PDF file.`);
}
window.dotnetRuntime.Module.FS.writeFile(fileName, fileBytes, { flags: 'w+' });
return fileName;
};
const MergePdfs = async () => {
const wasmModule = window.wasmModule?.spirepdf;
if (!wasmModule || isGenerating) {
return;
}
setIsGenerating(true);
setErrorMessage('');
try {
const [firstInputFile, secondInputFile] = await Promise.all([
loadPdfToVfs('input_1.pdf'),
loadPdfToVfs('input_2.pdf'),
]);
const outputFileName = 'MergedPdf.pdf';
const firstDocument = new wasmModule.PdfDocument();
const secondDocument = new wasmModule.PdfDocument();
const mergedDocument = new wasmModule.PdfDocument();
firstDocument.LoadFromFile({ fileName: firstInputFile });
secondDocument.LoadFromFile({ fileName: secondInputFile });
if (firstDocument.Pages.Count < 1) {
throw new Error('The first PDF does not contain any pages.');
}
if (secondDocument.Pages.Count < 1) {
throw new Error('The second PDF does not contain any pages.');
}
mergedDocument.InsertPage({ ldDoc: firstDocument, pageIndex: 0 });
mergedDocument.InsertPageRange(secondDocument, 0, secondDocument.Pages.Count - 1);
mergedDocument.SaveToFile({ fileName: outputFileName });
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const modifiedFile = new Blob([modifiedFileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(modifiedFile);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) {
console.error('Failed to merge PDFs:', error);
setErrorMessage(error.message || 'Failed to merge PDFs.');
} finally {
setIsGenerating(false);
}
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Merge PDF Documents in React</h1>
<button onClick={MergePdfs} disabled={!wasmModule || isGenerating}>
{isGenerating ? 'Generating...' : 'Generate'}
</button>
{errorMessage && <p style={{ color: 'crimson' }}>{errorMessage}</p>}
</div>
);
}
export default App;
输出:

理解页面选择逻辑
三个 PdfDocument 实例具有不同的角色:
firstDocument表示input_1.pdf。secondDocument表示input_2.pdf。mergedDocument是接收选定页面的新 PDF。
在本示例中,PDF 页面索引从 0 开始。因此,pageIndex: 0 指的是第一页:
mergedDocument.InsertPage({ ldDoc: firstDocument, pageIndex: 0 });
以下语句从 secondDocument 中插入连续范围。其起始索引为 0,结束索引为 secondDocument.Pages.Count - 1,因此会追加整个文档:
mergedDocument.InsertPageRange(
secondDocument,
0,
secondDocument.Pages.Count - 1
);
您可以更改这些索引,以仅合并应用程序所需的页面。例如,以下语句从 secondDocument 中插入第 2 至第 5 页,因为其从 0 开始的索引为 1 至 4:
mergedDocument.InsertPageRange(secondDocument, 1, 4);
在使用固定的页面索引之前,请确保源文档包含足够的页面。示例中已检查了空 PDF,但生产级应用程序还应对用户提供的起始和结束索引依据 Pages.Count 进行验证。
重要实现注意事项
保持运行时和输入路径正确
存储在 React public 目录中的文件在运行时通过 URL 请求。代码使用 process.env.PUBLIC_URL,以便在应用程序部署到非根公共路径下时能够正确构建路径。文件路径缺失或不正确可能返回 HTML 错误页面而非 PDF,这就是示例在将数据写入虚拟文件系统之前验证 %PDF 标头的原因。
等待 WebAssembly 初始化
Spire.PDF 在其运行时完全加载之前无法处理文档。wasmModule 状态控制按钮的禁用状态,而 isGenerating 可防止在当前合并完成之前重复启动同一操作。
验证页面范围
当动态选择页面时,请检查起始和结束索引是否为非负数、起始索引是否不超过结束索引,以及两个值是否都在源文档的页数范围内。这样可以避免无效范围错误,并便于在 React 界面中显示有用的消息。
释放下载 URL
URL.createObjectURL() 为生成的 Blob 创建一个临时 URL。在下载开始后调用 URL.revokeObjectURL(url) 可释放该 URL,防止其在浏览器内存中停留过长时间。
结论
Spire.PDF for JavaScript 使 React 应用程序能够通过基于 WebAssembly 的工作流合并 PDF 内容。当需要所有页面时,PdfMerger.Merge() 提供了一种简洁的方式来按定义好的顺序合并多个完整文档。当输出必须仅包含特定内容时,PdfDocument、InsertPage() 和 InsertPageRange() 提供了对结果的页面级控制。
将运行时文件配置在 public 目录后,这些技术可以集成到文档门户、报告工具、合同工作流以及其他需要在浏览器中直接组装 PDF 的 React 应用程序中。







