PDF 版式固定、跨端一致,适合分发合同、手册、报表等文档,不过一份资料往往由多份相互关联的 PDF 组成:产品手册之外还有报价单、技术规格书、常见问题等,逐个文件发送既零散又容易遗漏。PDF 的“文件包”(Portfolio)机制提供了标准的解决办法,把多份文档打包进同一个 PDF。接收方只需打开这一个文件,就能在查看器的文件包视图中看到、展开并另存各个成员文件,便于统一下发与归档。
Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端直接完成 PDF 的加载、绘制与保存,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。围绕文件包有两种常用操作:创建,用 PdfDocument 加载一份主文档,再通过文件集合的根文件夹 doc.Collection.Folders 调用 AddFile 把已载入 VFS 的成员文件逐个加入,必要时用 CreateSubfolder 建子文件夹对成员归类;识别,直接读取 doc.IsPortfolio 属性,判断某份 PDF 是否为文件包。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
创建 PDF 文件包
文件包的成员不限于 PDF,Word、Excel、图片等文件都可以加入,也可以用子文件夹归类。打包的思路是:先用 PdfDocument 加载一份主文档作为文件包的载体;把要打包的成员文件通过 FetchFileToVFS 载入虚拟文件系统;再遍历成员,用文件集合的根文件夹 doc.Collection.Folders 调用 AddFile({ filePath }) 逐个加入。若希望部分文件单独归到某个子文件夹,可先用 CreateSubfolder 建好子文件夹,再对它调用 AddFile,把文件加进去。所有成员加入完成后保存,即得到把主文档与各成员文件打包到一起的 PDF 文件包。
function App() {
const createPortfolio = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将作为文件包主文档的 PDF 载入 VFS
const mainFileName = '产品手册.pdf';
await window.spire.FetchFileToVFS(mainFileName, "", `${process.env.PUBLIC_URL}/data/`);
// 创建 PdfDocument 对象并加载主文档
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(mainFileName);
// 将放在文件包根目录下的成员文件逐个载入 VFS,并添加到文件集合的文件夹中
const rootFiles = ['报价单.pdf', '技术规格书.pdf', 'logo.png', '财务报表.xlsx'];
for (let i = 0; i < rootFiles.length; i++) {
await window.spire.FetchFileToVFS(rootFiles[i], "", `${process.env.PUBLIC_URL}/data/`);
doc.Collection.Folders.AddFile({ filePath: rootFiles[i] });
}
// 把要放进子文件夹的 Word 文档载入 VFS
await window.spire.FetchFileToVFS('test.docx', "", `${process.env.PUBLIC_URL}/data/`);
// 在文件集合中创建子文件夹“目录”,并把 Word 文档加入该子文件夹
const subFolder = doc.Collection.Folders.CreateSubfolder('目录');
subFolder.AddFile({ filePath: 'test.docx' });
// 定义输出文件名并保存文档
const outputFileName = '文件包.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
// 从 VFS 读取生成的文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Create PDF Portfolio</h1>
<button onClick={createPortfolio}>
Generate
</button>
</div>
);
}
export default App;
创建后得到的产品资料文件包

识别 PDF 文件包
判断一份 PDF 是否为文件包,用 PdfDocument.IsPortfolio 属性即可:LoadFromFile 载入文档后读取该布尔属性,返回 true 表示是文件包,false 表示是普通 PDF 文档。本示例加载一份文件包样例进行识别,把结论写入 txt 下载,并同步显示在页面下方,便于直接查看。
function App() {
const identifyPortfolio = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将待识别的 PDF 载入 VFS
const inputFileName = '文件包.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// 创建 PdfDocument 对象并加载 PDF 文档
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// 判断该 PDF 是否为文件包
const isPortfolio = doc.IsPortfolio;
const message = isPortfolio ? '该 PDF 是一个文件包' : '该 PDF 不是文件包';
doc.Close();
// 在页面下方显示判定结果
const resultEl = document.getElementById('identify-result');
if (resultEl) resultEl.innerText = message;
// 把判定结果写入 txt 并触发下载
const outputFileName = '识别结果.txt';
window.dotnetRuntime.Module.FS.writeFile(outputFileName, message);
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Identify PDF Portfolio</h1>
<button onClick={identifyPortfolio}>
Check
</button>
<p id="identify-result" style={{ marginTop: '20px', fontWeight: 'bold' }}></p>
</div>
);
}
export default App;
识别结果为“该 PDF 是一个文件包”

常见问题
创建文件包后如何确认它确实是文件包
原因:文件包只是把成员文件“打包”进了同一个 PDF,从外观上未必能一眼判断保存结果是否成功。
解决:用 doc.IsPortfolio 对保存结果做二次判断即可:重新载入生成的文件,若该属性返回 true,说明确实是文件包:
// 重新载入生成的文件并判断是否为文件包
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile('文件包.pdf');
const isPortfolio = doc.IsPortfolio;
文件包与普通 PDF 附件有什么区别
原因:文件包和附件都会把文件“塞进”PDF,容易混淆两者的用途与判断方式。
解决:PDF 附件(Attachment)把文件作为嵌入文件挂在文档的附件面板里,正文本身通常是一份独立文档;而文件包(Portfolio)基于文件集合(Collection)组织成员文件,成员可以是多份文档,打开后在文件包视图中作为独立文件分别展开与另存。判断时可用 doc.Attachments 查看附件、用 doc.IsPortfolio 判断是否为文件包,二者互不替代。
是不是只能把 PDF 文件加入文件包
原因:示例里先展示的是几个 PDF 成员,容易让人误以为文件包只能装 PDF。
解决:AddFile 加入的是虚拟文件系统里的任意文件,不限于 PDF。先通过 FetchFileToVFS 把目标文件载入 VFS,再以 { filePath: 文件名 } 加入即可;想让文件分组存放时,再用 CreateSubfolder 建一个子文件夹,把文件加入该子文件夹。Word、Excel、图片等文件都可以作为成员打包进文件包:
// 载入一份 Excel 文件并作为文件包成员加入
await window.spire.FetchFileToVFS('财务报表.xlsx', "", `${process.env.PUBLIC_URL}/data/`);
doc.Collection.Folders.AddFile({ filePath: '财务报表.xlsx' });
获取免费许可证
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。









