在销售报表、成绩单或指标看板中,数值的横向对比往往比数值本身更重要。若为每一列都插入图表,工作表会显得拥挤;而数据条、色阶与图标集可以在不占用额外行列的前提下,用条形长度、颜色深浅和图标形态,把单元格中的数值大小直接呈现出来。这三者同属 Excel 的条件格式(Conditional Formatting),在 Excel 界面中位于「开始 → 条件格式」之下。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成上述操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
本文介绍三个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。
数据条在单元格内绘制一条水平色带,色带长度与该单元格数值在所选区域内的相对大小成正比。Spire.XLS for JavaScript 通过 ConditionalFormats.Add 创建条件格式集合,用 AddRange 指定作用区域,再用 AddCondition 取得条件对象;将 FormatType 设为 ConditionalFormatType.DataBar 即可生成数据条,条形颜色由 DataBar.BarColor 控制。具体操作步骤如下:
ConditionalFormats.Add 新增条件格式,并通过 AddRange 绑定数据区域。AddCondition 添加条件,将 FormatType 设为 DataBar,并设置条形颜色。以下为完整的代码示例,演示如何在 React 中为销量数据应用数据条:
function App() {
const applyDataBars = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将字体和测试数据文件载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'SalesData.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// 加载工作簿,取第一个工作表
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
const sheet = workbook.Worksheets.get(0);
// 选中需要应用数据条的数据区域
const dataRange = sheet.Range.get("B2:E9");
// 新增条件格式,并将其绑定到该区域
const xcfs = sheet.ConditionalFormats.Add();
xcfs.AddRange(dataRange);
// 添加数据条条件,设置条形颜色
const format = xcfs.AddCondition();
format.FormatType = xlsModule.ConditionalFormatType.DataBar;
format.DataBar.BarColor = xlsModule.Color.get_CadetBlue();
// 保存工作簿
const outputFileName = "ApplyDataBars.xlsx";
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放 workbook 对象以释放资源
workbook.Dispose();
// 从 VFS 读取结果文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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>应用数据条</h1>
<button onClick={applyDataBars}>Start</button>
</div>
);
}
export default App;
运行后,在单元格区域中应用数据条的效果:
![]()
色阶用颜色的深浅表示数值的高低:单元格的数值在整个区域中越大,取到的颜色越靠近刻度的高端。与数据条相同,色阶同样由 ConditionalFormats 添加,区别仅在于将 FormatType 设为 ConditionalFormatType.ColorScale;未指定颜色时生成的是双色刻度,区域最小值取橙色,最大值取浅黄色,中间数值按比例在两端之间过渡。具体操作步骤如下:
ConditionalFormats.Add 新增条件格式,并通过 AddRange 绑定数据区域。AddCondition 添加条件,并将 FormatType 设为 ColorScale。以下为完整的代码示例,演示如何在 React 中为销量数据应用色阶:
function App() {
const applyColorScales = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将字体和测试数据文件载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'SalesData.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// 加载工作簿,取第一个工作表
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
const sheet = workbook.Worksheets.get(0);
// 选中需要应用色阶的数据区域
const dataRange = sheet.Range.get("B2:E9");
// 新增条件格式,并将其绑定到该区域
const xcfs = sheet.ConditionalFormats.Add();
xcfs.AddRange(dataRange);
// 添加色阶条件,颜色按数值高低自动过渡
const format = xcfs.AddCondition();
format.FormatType = xlsModule.ConditionalFormatType.ColorScale;
// 保存工作簿
const outputFileName = "ApplyColorScales.xlsx";
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放 workbook 对象以释放资源
workbook.Dispose();
// 从 VFS 读取结果文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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>应用色阶</h1>
<button onClick={applyColorScales}>Start</button>
</div>
);
}
export default App;
运行后,在单元格区域中应用色阶的效果:
![]()
图标集依照数值所在的区间,在单元格中显示不同的图标,例如用红、黄、绿三色交通灯标记低、中、高三档。使用的仍是同一套 API:将 FormatType 设为 ConditionalFormatType.IconSet,再通过 IconSet.IconSetType 选定图标样式;示例采用 IconSetType.ThreeTrafficLights1(三色交通灯)。具体操作步骤如下:
ConditionalFormats.Add 新增条件格式,并通过 AddRange 绑定数据区域。AddCondition 添加条件,将 FormatType 设为 IconSet,并指定图标集类型。以下为完整的代码示例,演示如何在 React 中为销量数据应用图标集:
function App() {
const applyIconSets = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将字体和测试数据文件载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'SalesData.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// 加载工作簿,取第一个工作表
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
const sheet = workbook.Worksheets.get(0);
// 选中需要应用图标集的数据区域
const dataRange = sheet.Range.get("B2:E9");
// 新增条件格式,并将其绑定到该区域
const xcfs = sheet.ConditionalFormats.Add();
xcfs.AddRange(dataRange);
// 添加图标集条件,图标样式设为三色交通灯
const format = xcfs.AddCondition();
format.FormatType = xlsModule.ConditionalFormatType.IconSet;
format.IconSet.IconSetType = xlsModule.IconSetType.ThreeTrafficLights1;
// 保存工作簿
const outputFileName = "ApplyIconSets.xlsx";
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放 workbook 对象以释放资源
workbook.Dispose();
// 从 VFS 读取结果文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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>应用图标集</h1>
<button onClick={applyIconSets}>Start</button>
</div>
);
}
export default App;
图标集按区域内的取值区间划分档次,因此同一个图标在不同区域中对应的数值范围并不相同。
运行后,在单元格区域中应用图标集的效果:
![]()
原因:数据条表达的是数值在区域内的相对大小,因此只有数值单元格会被着色,区域中的文本单元格会被自动跳过。即使作用区域包含产品名称列或表头行,这些单元格也不会显示数据条,得到的仍是只覆盖数值区域的条形。
解决:这是预期行为,无需额外处理;把作用区域限定为数值区域即可。若连数值区域也没有出现数据条,则应检查 AddRange 传入的区域是否与数据实际所在的区域一致。
原因:数据条的外观由条件对象的 DataBar 属性控制。条形填充色通过 DataBar.BarColor 指定;若只设置 FormatType 而不设置 BarColor,得到的是默认的蓝色数据条。数据条默认没有边框,直接设置 BarBorder.Color 不会生效。
解决:先通过 DataBar.BarBorder.Type 指定边框类型,再设置边框颜色,两者需配合使用:
// 先指定边框类型,边框颜色才会生效
format.DataBar.BarBorder.Type = xlsModule.DataBarBorderType.DataBarBorderSolid;
format.DataBar.BarBorder.Color = xlsModule.Color.get_Red();
// 条形填充色
format.DataBar.BarColor = xlsModule.Color.get_GreenYellow();
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
将 Word 文档转换为图片,是在线预览、生成缩略图以及限制内容被随意复制时最常用的处理方式,转换后的图片在任何设备上都能以一致的排版呈现。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接完成此转换,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript。以下示例默认已安装 Spire.Doc 并完成 WebAssembly 模块初始化。
文档页面转图片的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和目标 Word 文档载入 WASM 虚拟文件系统;然后实例化 Document 加载文档,调用 SaveImageToStreams 并传入 pageIndex 将指定页面渲染为图片,保存到 VFS 中;最后从 VFS 读取生成的图片文件,封装为 Blob 后生成下载链接。
import React from 'react';
function App() {
const ToImage = async () => {
// 获取 Spire.Doc WASM 模块
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 将字体文件载入虚拟文件系统 (VFS)
await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'ToImage.docx';
// 将目标 Word 文档载入 VFS
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// 创建 Document 实例并加载文档
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// 定义输出文件名
const outputFileName = "ToImage-result.png";
// 将第一页转换为图片流,并保存到 VFS
let img = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
img.Save(outputFileName);
// 释放资源
doc.Dispose();
// 从 VFS 读取生成的文件,封装为 Blob 对象
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { type: 'image/png'});
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>转换指定的页面到图片</h1>
<button onClick={ToImage}>
生成
</button>
</div>
);
}
export default App;
文档页面通过 SaveImageToStreams 转换后生成的 PNG 图片

除了整页转换,实际项目中还常需要将文档中的某个元素单独导出为图片,例如为表格生成预览图,或把文档中的形状提取出来作为独立素材。段落、表格、表格行、表格单元格和形状都可以通过 Clone 方法复制到新建的 Document 中,再调用 SaveImageToStreams 渲染为图片。转换结果统一写入 VFS 中的输出目录,最后使用 JSZip 打包为一个 ZIP 文件供用户下载。
需要注意的是,形状(Shape)无法直接加入新建文档的段落中,示例中先将文档保存到内存流再重新加载,让形状在新文档中获得完整的布局上下文后再渲染。
import React from 'react';
import JSZip from "jszip";
function App() {
const ToImage = async () => {
// 获取 Spire.Doc WASM 模块
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 将字体文件载入虚拟文件系统 (VFS)
await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 将目标 Word 文档载入 VFS
const inputFileName = "ConvertObjectToImage.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// 在 VFS 中定义并创建输出目录
const outputDirectoryName = "outputFolder/";
await window.dotnetRuntime.Module.FS.mkdirTree(outputDirectoryName);
// 创建 Document 实例并加载文档
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// 获取第一节及其正文
let section = doc.Sections.get_Item(0);
let body = section.Body;
// 获取第一个段落并转换为图片
let paragraph = body.Paragraphs.get_Item(0);
let imageStream1 = ConvertParagraphToImage(paragraph, docModule);
let imageFile1 = outputDirectoryName + "ConvertParagraphToImage.png";
window.dotnetRuntime.Module.FS.writeFile(imageFile1, imageStream1.Save());
// 获取第一个表格并转换为图片
let table = body.Tables.get_Item(0);
let imageStream2 = ConvertTableToImage(table, docModule);
let imageFile2 = outputDirectoryName + "ConvertTableToImage.jpg";
window.dotnetRuntime.Module.FS.writeFile(imageFile2, imageStream2.Save());
// 获取第一个表格的第一行并转换为图片
let row = table.Rows.get_Item(0);
let imageStream3 = ConvertTableRowToImage(row, docModule);
let imageFile3 = outputDirectoryName + "ConvertTableRowToImage.bmp";
window.dotnetRuntime.Module.FS.writeFile(imageFile3, imageStream3.Save());
// 获取第一行的第一个单元格并转换为图片
let cell = row.Cells.get_Item(0);
let imageStream4 = ConvertTableCellToImage(cell, docModule);
let imageFile4 = outputDirectoryName + "ConvertTableCellToImage.png";
window.dotnetRuntime.Module.FS.writeFile(imageFile4, imageStream4.Save());
// 遍历段落,将其中的形状转换为图片
for (let i = 0; i < section.Paragraphs.Count; i++) {
let para = section.Body.Paragraphs.get_Item(i);
for (let j = 0; j < para.ChildObjects.Count; j++) {
let docObj = para.ChildObjects.get_Item(j);
if (docObj.DocumentObjectType == docModule.DocumentObjectType.Shape) {
let imageStream5 = ConvertShapeToImage(docObj, docModule);
let imageFile5 = outputDirectoryName + "ConvertShapeToImage-" + j + ".png";
window.dotnetRuntime.Module.FS.writeFile(imageFile5, imageStream5.Save());
i++;
}
}
}
// 释放资源
doc.Dispose();
// 将输出目录下的所有图片打包为 ZIP 文件
const zip = new JSZip();
const addFilesToZip = async (folderPath, zipFolder) => {
let items = await window.dotnetRuntime.Module.FS.readdir(folderPath);
items = items.filter((item) => item !== "." && item !== "..");
for (const item of items) {
const itemPath = `${folderPath}/${item}`;
try {
const fileData = await window.dotnetRuntime.Module.FS.readFile(itemPath);
zipFolder.file(item, fileData);
} catch (error) {
const zipSubFolder = zipFolder.folder(item);
await addFilesToZip(itemPath, zipSubFolder);
}
}
};
await addFilesToZip(outputDirectoryName, zip);
const zipBlob = await zip.generateAsync({ type: "blob" });
// 从 VFS 读取生成的文件,封装为 Blob 对象
const url = URL.createObjectURL(zipBlob);
const a = document.createElement('a');
a.href = url;
a.download = "ConvertObjectToImage_out.zip";
a.click();
URL.revokeObjectURL(url);
};
// 将段落转换为图片
function ConvertParagraphToImage(paragraph, docModule) {
let doc = new docModule.Document();
let section = doc.AddSection();
section.Body.ChildObjects.Add(paragraph.Clone());
let imageStream = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
doc.Close();
return imageStream;
}
// 将表格转换为图片
function ConvertTableToImage(table, docModule) {
let doc = new docModule.Document();
let section = doc.AddSection();
section.Body.ChildObjects.Add(table.Clone());
let imageStream = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
doc.Close();
return imageStream;
}
// 将表格行转换为图片
function ConvertTableRowToImage(tableRow, docModule) {
let doc = new docModule.Document();
let section = doc.AddSection();
let table = section.AddTable();
table.Rows.Add(tableRow.Clone());
let imageStream = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
doc.Close();
return imageStream;
}
// 将表格单元格转换为图片
function ConvertTableCellToImage(tableCell, docModule) {
let doc = new docModule.Document();
let section = doc.AddSection();
let table = section.AddTable();
table.AddRow().Cells.Add(tableCell.Clone());
let imageStream = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
doc.Close();
return imageStream;
}
// 将形状转换为图片
function ConvertShapeToImage(shape, docModule) {
let doc = new docModule.Document();
let section = doc.AddSection();
section.AddParagraph().ChildObjects.Add(shape.Clone());
let memoryStream = new docModule.Stream();
doc.SaveToStream({ stream: memoryStream, fileFormat: docModule.FileFormat.Docx });
doc.LoadFromStream({ stream: memoryStream, fileFormat: docModule.FileFormat.Docx });
let imageStream = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
memoryStream.Close();
doc.Close();
return imageStream;
}
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>转换元素到图片</h1>
<button onClick={ToImage}>
生成
</button>
</div>
);
}
export default App;
文档对象转换后打包生成的 ZIP 文件中的图片

原因:WASM 虚拟文件系统中缺少渲染所需的字体文件。SaveImageToStreams 渲染文本时需从 VFS 中读取字体,若未预加载则文字区域会留白或显示为乱码。
解决:转换前通过 FetchFileToVFS 将字体文件载入 VFS:
await window.spire.FetchFileToVFS(
'ARIALUNI.TTF', '/Library/Fonts/',
`${process.env.PUBLIC_URL}/static/font/`
);
原因:SaveImageToStreams 每次调用只渲染 pageIndex 指定的单个页面,示例中固定传入 0,因此多页文档只会输出第一页的图片。
解决:通过 PageCount 获取文档总页数并逐页遍历,为每一页生成独立的图片文件:
for (let i = 0; i < doc.PageCount; i++) {
let img = doc.SaveImageToStreams({
pageIndex: i, type: wasmModule.ImageType.Bitmap
});
img.Save(`ToImage-page-${i + 1}.png`);
}
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
实际工作中更常见的情况是:文档已经写好并带有完整的章节结构,只是当初没有生成目录。此时无需重排内容,只要在原有的标题样式基础上补一个目录域,即可得到带页码和跳转的完整目录。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接打开和编辑 Word 文档,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
与新建文档相比,为已有文档添加目录多了两个关键步骤:一是通过 LoadFromFile 从 VFS 载入原有文档,二是通过 Paragraphs.Insert 把目录段落到文档最开头,而不是默认追加到末尾。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript。以下示例默认已安装 Spire.Doc 并完成 WebAssembly 模块初始化。
为已有文档添加默认目录的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和待处理的 Word 文档载入 WASM 虚拟文件系统;然后实例化 Document 并通过 LoadFromFile 载入文档,新建一个段落并用 AppendTOC 插入目录域,再通过 Paragraphs.Insert(0, tocPara) 将其移动到文档最开头;最后调用 UpdateTableOfContents 填充条目与页码,保存文档后从 VFS 读取生成的文件,封装为 Blob 后触发浏览器下载。
示例使用的输入文档 AddTocToExisting.docx 是一份带有三章十二个多级标题、但尚未生成目录的技术报告。
function App() {
const AddTableOfContentsToExistingDocument = async () => {
// 获取 Spire.Doc WASM 模块
const docModule = window.wasmModule?.spiredoc;
// 确保 WASM 模块完全加载完成
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 将字体和已有 Word 文档载入 VFS
await window.spire.FetchFileToVFS('msyh.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'AddTocToExisting.docx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// 创建文档实例并载入已有文档
const doc = new docModule.Document();
doc.LoadFromFile({ fileName: inputFileName });
// 获取文档的第一节
let section = doc.Sections.get_Item(0);
// 新建段落并插入目录域,收集 Heading 1 至 Heading 3 的条目
let tocPara = section.AddParagraph();
tocPara.AppendTOC(1, 3);
// 将目录段落移动到文档最开头
section.Paragraphs.Insert(0, tocPara);
// 更新目录,填充条目与页码
doc.UpdateTableOfContents();
// 定义输出文件名并保存
const outputFileName = "为已有文档添加默认目录.docx";
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
// 释放资源
doc.Dispose();
// 从 VFS 读取生成的文件,触发下载
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
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>点击以下按钮为已有文档添加默认目录</h1>
<button onClick={AddTableOfContentsToExistingDocument}>
生成
</button>
</div>
);
}
export default App;
通过 LoadFromFile 载入已有文档并插入目录域后,目录被放置在文档最开头,原有章节内容与版式保持不变。

AppendTOC 生成的目录使用 Word 默认的域开关,当需要控制目录的具体行为时,可以改为直接构造 TableOfContent 对象并指定开关串。与上一个功能点的区别在于插入方式:需要手动将目录对象加入段落,并补齐域分隔符与域结束标记,同时将该对象赋给 document.TOC。常用的域开关及其含义如下:
| 开关 | 说明 |
|---|---|
\o "1-3" |
按内置标题样式收集条目,此处表示收录 Heading 1 至 Heading 3 |
\h |
将目录条目设为超链接,点击即可跳转到对应章节 |
\z |
在 Web 版式视图中隐藏页码与制表符前导符 |
\u |
按段落的大纲级别收集条目 |
如果希望目录单独占一页并与正文分开,可以在插入目录段落之后,再向同一节添加一个分页符:
tocPara.AppendBreak(docModule.BreakType.PageBreak);
function App() {
const CustomizeTableOfContent = async () => {
// 获取 Spire.Doc WASM 模块
const docModule = window.wasmModule?.spiredoc;
// 确保 WASM 模块完全加载完成
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 将字体和已有 Word 文档载入 VFS
await window.spire.FetchFileToVFS('msyh.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'AddTocToExisting.docx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// 创建文档实例并载入已有文档
const doc = new docModule.Document();
doc.LoadFromFile({ fileName: inputFileName });
// 获取文档的第一节
let section = doc.Sections.get_Item(0);
// 构造带自定义域开关的目录对象
let toc = new docModule.TableOfContent(doc, "{\\o \"1-3\" \\h \\z \\u}");
// 将目录对象加入段落
let tocPara = section.AddParagraph();
tocPara.Items.Add(toc);
// 补齐域分隔符与域结束标记
tocPara.AppendFieldMark(docModule.FieldMarkType.FieldSeparator);
tocPara.AppendText("TOC");
tocPara.AppendFieldMark(docModule.FieldMarkType.FieldEnd);
// 将该目录绑定到文档
doc.TOC = toc;
// 将目录段落移动到文档最开头
section.Paragraphs.Insert(0, tocPara);
// 更新目录,填充条目与页码
doc.UpdateTableOfContents();
// 定义输出文件名并保存
const outputFileName = "为已有文档添加自定义目录.docx";
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
// 释放资源
doc.Dispose();
// 从 VFS 读取生成的文件,触发下载
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
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>点击以下按钮为已有文档添加自定义目录</h1>
<button onClick={CustomizeTableOfContent}>
生成
</button>
</div>
);
}
export default App;
通过 TableOfContent 对象与自定义域开关生成的目录,条目层级、超链接与页码表现均由开关串决定。

原因:AddParagraph 默认把新段落追加到所在节的末尾,直接在其上插入目录域,目录自然也会出现在文末。已有文档的内容已经排好,因此必须显式指定插入位置。
解决:先创建目录段落,再用 Paragraphs.Insert 把它移动到文档最开头:
let tocPara = section.AddParagraph();
tocPara.AppendTOC(1, 3);
section.Paragraphs.Insert(0, tocPara);
原因:目录域按标题样式收集条目。如果原文档中的章节标题只是手动加粗、放大了字号,而没有应用 Heading1 至 Heading3 等内置标题样式,更新后目录中不会出现任何条目。
解决:先检查原文档的标题是否使用内置标题样式。若没有,可以在载入文档后重新为这些段落应用样式:
let heading = section.Paragraphs.get_Item(2);
heading.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
原因:AppendTOC 只是插入了目录域本身,域内容需要显式更新。若在保存前未调用 UpdateTableOfContents,生成的目录只有域代码,没有条目和页码。
解决:在 SaveToFile 之前调用更新方法:
doc.UpdateTableOfContents();
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
文档常常是拼出来的:封面、正文、附件分别来自不同环节,交付前才发现封面被排到了第二章后面,或者某几页的先后需要调换。要改的只是页面顺序,内容一个字都不用动,但身边没有 PDF 编辑器时,这件事就卡住了——把文件传到服务端处理,等于让它离开用户的设备。本文将介绍如何使用 Spire.PDF for JavaScript 在浏览器端按指定顺序重排 PDF 页面。它基于 WebAssembly 加载、修改与保存 PDF 文档,页面顺序的调整全部在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
页面顺序由 PdfPageCollection.ReArrange() 一次性改写,传入的序号数组既决定每一页的新位置,也决定文档的页数——数组里放几个索引,输出就是几页,所以要保留全部页面,就得把 0 到页数减一的索引一个不落地排进去。
function App() {
const rearrangePages = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将待处理的 PDF 文件载入 VFS
const inputFileName = 'Number.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// 创建 PdfDocument 对象并加载 PDF 文档
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// 新的页面顺序:把第 3 页提到最前,其余页面顺延;索引从 0 开始
const newOrder = [2, 0, 1, 3, 4];
doc.Pages.ReArrange(newOrder);
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>重排 PDF 页面顺序</h1>
<button onClick={rearrangePages}>
开始重排
</button>
</div>
);
}
export default App;
第 3 页被移到最前、其余页面依次顺延后的文档:

原因:ReArrange() 是按传入的数组重建页面顺序,数组里没有出现的索引对应的页面不会进入结果文档。把 5 页文档写成 ReArrange([1, 0]),输出的就是 2 页。
解决:页数要保持不变,数组长度必须等于 doc.Pages.Count,且 0 到 doc.Pages.Count - 1 每个索引都出现一次:
// 索引从 0 开始,5 页文档对应 0、1、2、3、4
const pageCount = doc.Pages.Count;
const fullOrder = Array.from({ length: pageCount }, (_, i) => i);
doc.Pages.ReArrange(fullOrder);
ReArrange() 时提示 "The page has existed."原因:序号数组里出现了重复的索引,同一个原页被指定到了两个位置。一页无法同时占据两个位置,Spire.PDF 会直接抛错。
解决:确认数组是原页索引的一个排列——每个索引只出现一次,且不要超出页数范围,5 页文档的有效索引是 0 到 4。
原因:ReArrange() 处理的始终是整份文档的顺序,没有"只换两页"的简化重载。
解决:仍然按完整顺序数组来写,未涉及的位置保持原索引即可。下面这行把第 1 页与第 2 页对调:
// 交换前两页:0 与 1 互换,其余不动
const swappedOrder = [1, 0, 2, 3, 4];
doc.Pages.ReArrange(swappedOrder);
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
一批文档要看起来像同一套,最省事的做法是给每页铺上统一的品牌底色或信纸底纹。过去要么回到源文件逐份改版式,要么手工往每页叠一张图——前者要求手上还有可编辑的原始文档,后者稍不注意就压到正文。
在浏览器里直接改 PDF 就能绕开这两点:Spire.PDF for JavaScript 基于 WebAssembly 加载、修改与保存 PDF 文档,底色和底图都写进已有页面的 BackgroundColor、BackgroundImage 属性,绘制层级在正文下方;文件读写通过虚拟文件系统(VFS)完成,无需后端配合。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
整份文档要统一底色,逐页给 BackgroundColor 赋一个 Color 就行,该属性默认只按 0.25 的不透明度叠加,需要实色时把 BackgroudOpacity 设为 1。
function App() {
const setBackgroundColor = 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);
// 自定义底色:ARGB 四通道,这里取浅蓝
const backgroundColor = pdfModule.Color.FromArgb(255, 226, 240, 253);
// 逐页设置背景颜色
for (let i = 0; i < doc.Pages.Count; i++) {
const page = doc.Pages.get_Item(i);
page.BackgroundColor = backgroundColor;
// 默认叠加不透明度为 0.25,置为 1 得到实色
page.BackgroudOpacity = 1;
}
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>设置 PDF 背景颜色</h1>
<button onClick={setBackgroundColor}>
开始设置
</button>
</div>
);
}
export default App;
设置浅蓝色背景后的两页文稿,底色前后保持一致:

整页铺一张底图则交给 BackgroundImage:它接收一个在虚拟文件系统里打开的图片流,并自动拉伸铺满页面内容区,浓淡同样由 BackgroudOpacity 控制。
function App() {
const setBackgroundImage = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将待处理的 PDF 文件与背景图载入 VFS
const inputFileName = '模板.pdf';
const imageFileName = 'Background.png';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS(imageFileName, "", `${process.env.PUBLIC_URL}/data/`);
// 创建 PdfDocument 对象并加载 PDF 文档
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// 从 VFS 打开背景图文件流,各页共用同一个流
const imageStream = new window.spire.Stream(imageFileName);
// 逐页铺上底图,图片会拉伸铺满页面内容区
for (let i = 0; i < doc.Pages.Count; i++) {
const page = doc.Pages.get_Item(i);
page.BackgroundImage = imageStream;
page.BackgroudOpacity = 1;
}
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>设置 PDF 背景图片</h1>
<button onClick={setBackgroundImage}>
开始设置
</button>
</div>
);
}
export default App;
同一张底图铺满每一页,正文文字依旧清晰:

原因:BackgroudOpacity 属性(拼写里少一个 n)默认是 0.25,背景会按 25% 的不透明度叠到页面上,深色也会被冲成浅色。
解决:需要实色就把 BackgroudOpacity 设为 1;想保留一点淡化效果,也可以取 0.3 到 0.8 之间的值。
page.BackgroundColor = backgroundColor;
// 置为 1 得到实色,调小则是淡化叠加
page.BackgroudOpacity = 1;
原因:背景只绘制在页面的内容区(ClientSize)内。载入的 PDF 一般没有页边距,背景就是整页;而用 Pages.Add() 新建的页面默认带 40 磅页边距,这一圈不会被着色。
解决:新建页时把页边距设为 0,背景即可覆盖整页:
// 第二个参数传零页边距对象,页面内容区与整页等大
const page = doc.Pages.Add(pdfModule.PdfPageSize.A4(), new pdfModule.PdfMargins());
原因:BackgroundColor 与 BackgroundImage 都是页面级属性,只对赋值的那个页面生效,不会自动应用到整份文档。
解决:按页号单独取页设置即可,不必遍历:
// 只处理第 1 页,其余页面保持原样
const page = doc.Pages.get_Item(0);
page.BackgroundColor = pdfModule.Color.FromArgb(255, 226, 240, 253);
page.BackgroudOpacity = 1;
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
为长文档创建目录,既能让读者快速定位章节内容,也便于在文档结构变动后重新同步条目与页码。本文以一份《Spire.Doc 开发指南》为例,介绍如何从零新建一份带有多级标题的 Word 文档,并为其添加内容目录。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接构建和编辑 Word 文档,通过虚拟文件系统(VFS)管理字体资源,无需后端服务支持。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript。以下示例默认已安装 Spire.Doc 并完成 WebAssembly 模块初始化。
在 Word 中,目录本质上是一个 TOC 域,其条目来自文档中应用了标题样式的段落。创建默认目录的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件载入 WASM 虚拟文件系统;然后实例化 Document 并通过 AddSection、AddParagraph 构建内容,对需要出现在目录中的段落调用 ApplyStyle 应用标题样式,再通过 AppendTOC 在文档开头插入目录域;最后调用 UpdateTableOfContents 填充条目与页码,保存文档后从 VFS 读取生成的文件,封装为 Blob 后触发浏览器下载。
示例文档共包含三章、十二个多级标题,涵盖 Heading 1 至 Heading 3 三个层级,便于观察目录对多级标题的收录效果。
function App() {
const AddTableOfContentsToNewDocument = async () => {
// 获取 Spire.Doc WASM 模块
const docModule = window.wasmModule?.spiredoc;
// 确保 WASM 模块完全加载完成
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 将字体载入 VFS
await window.spire.FetchFileToVFS('msyh.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 创建文档实例并添加节
const doc = new docModule.Document();
let section = doc.AddSection();
// 在文档开头插入目录域,收集 Heading 1 至 Heading 3 的条目
let tocPara = section.AddParagraph();
tocPara.AppendTOC(1, 3);
// 添加文档标题
let characterFormat = new docModule.CharacterFormat(doc);
characterFormat.FontName ="微软雅黑";
let title = section.AddParagraph();
let titleRun = title.AppendText("Spire.Doc 开发指南");
titleRun.ApplyCharacterFormat(characterFormat);
titleRun.CharacterFormat.FontSize = 24;
title.Format.HorizontalAlignment = docModule.HorizontalAlignment.Center;
// 第一章 概述(一级标题)
let p = section.AddParagraph();
p.AppendText("第一章 概述").ApplyCharacterFormat(characterFormat);;
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
section.AddParagraph().AppendText("Spire.Doc for JavaScript 让开发者可以直接在浏览器中创建、编辑与保存 Word 文档,整个过程无需任何后端服务参与。").ApplyCharacterFormat(characterFormat);;
// 1.1 什么是 Spire.Doc for JavaScript(二级标题)
p = section.AddParagraph();
p.AppendText("1.1 什么是 Spire.Doc for JavaScript").ApplyCharacterFormat(characterFormat);;
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
section.AddParagraph().AppendText("它是一套基于 WebAssembly 的 Word 文档处理库,通过虚拟文件系统(VFS)管理字体与文档文件,并提供与 .NET 版一致的 API 形态。").ApplyCharacterFormat(characterFormat);;
// 1.2 适用场景(二级标题)
p = section.AddParagraph();
p.AppendText("1.2 适用场景").ApplyCharacterFormat(characterFormat);;
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
section.AddParagraph().AppendText("适用于在线合同签署、报表批量生成、简历模板填充等需要在客户端完成文档处理的场景。").ApplyCharacterFormat(characterFormat);;
// 第二章 核心能力(一级标题)
p = section.AddParagraph();
p.AppendText("第二章 核心能力").ApplyCharacterFormat(characterFormat);;
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
section.AddParagraph().AppendText("Spire.Doc for JavaScript 覆盖了文档处理的完整链路,从内容构建、版式调整到格式导出均可在浏览器端完成。").ApplyCharacterFormat(characterFormat);;
// 2.1 文档处理(二级标题)
p = section.AddParagraph();
p.AppendText("2.1 文档处理").ApplyCharacterFormat(characterFormat);;
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
section.AddParagraph().AppendText("支持段落、样式、表格、图片、页眉页脚等常见文档元素的创建与修改,并保留原有的版式信息。").ApplyCharacterFormat(characterFormat);;
// 2.1.1 段落与样式(三级标题)
p = section.AddParagraph();
p.AppendText("2.1.1 段落与样式").ApplyCharacterFormat(characterFormat);;
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading3 });
section.AddParagraph().AppendText("通过 AddParagraph 新增段落,配合 ApplyStyle 应用内置样式,即可快速搭建结构清晰的文档骨架。").ApplyCharacterFormat(characterFormat);;
// 2.1.2 表格与图片(三级标题)
p = section.AddParagraph();
p.AppendText("2.1.2 表格与图片").ApplyCharacterFormat(characterFormat);;
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading3 });
section.AddParagraph().AppendText("表格与图片既可以直接写入指定段落,也可以嵌套在文本框内,满足复杂版式的排版需求。").ApplyCharacterFormat(characterFormat);;
// 2.2 格式转换(二级标题)
p = section.AddParagraph();
p.AppendText("2.2 格式转换").ApplyCharacterFormat(characterFormat);;
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
section.AddParagraph().AppendText("通过 SaveToFile 可以将文档转换为 PDF、HTML、Markdown 等多种格式,整个转换过程均在浏览器端完成。").ApplyCharacterFormat(characterFormat);;
// 2.3 批量处理(二级标题)
p = section.AddParagraph();
p.AppendText("2.3 批量处理").ApplyCharacterFormat(characterFormat);;
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
section.AddParagraph().AppendText("结合 WebAssembly 的运行效率,可以一次加载多份文档并顺序完成处理,避免频繁的文件上传与下载。").ApplyCharacterFormat(characterFormat);;
// 第三章 快速上手(一级标题)
p = section.AddParagraph();
p.AppendText("第三章 快速上手").ApplyCharacterFormat(characterFormat);;
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
section.AddParagraph().AppendText("本章介绍在 React 项目中接入 Spire.Doc for JavaScript 并完成第一次文档生成所需的准备工作。").ApplyCharacterFormat(characterFormat);;
// 3.1 环境准备(二级标题)
p = section.AddParagraph();
p.AppendText("3.1 环境准备").ApplyCharacterFormat(characterFormat);;
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
section.AddParagraph().AppendText("在 React 项目中安装 Spire.Doc for JavaScript,并将字体文件与 WASM 资源放入 public 目录即可开始使用。").ApplyCharacterFormat(characterFormat);;
// 3.2 第一个示例(二级标题)
p = section.AddParagraph();
p.AppendText("3.2 第一个示例").ApplyCharacterFormat(characterFormat);;
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
section.AddParagraph().AppendText("初始化模块后创建 Document 实例,添加内容并保存,最后从 VFS 读取结果文件触发浏览器下载。").ApplyCharacterFormat(characterFormat);;
// 更新目录,填充条目与页码
doc.UpdateTableOfContents();
// 定义输出文件名并保存
const outputFileName = "在 Word 文档中创建默认目录.docx";
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
// 释放资源
doc.Dispose();
// 从 VFS 读取生成的文件,触发下载
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
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>点击以下按钮在 Word 文档中创建默认目录</h1>
<button onClick={AddTableOfContentsToNewDocument}>
生成
</button>
</div>
);
}
export default App;
通过 AppendTOC 插入目录域并更新后,文档开头生成了收录三级标题、带页码与超链接的默认目录。

AppendTOC 生成的目录使用 Word 默认的域开关,当需要控制目录的具体行为时,可以改为直接构造 TableOfContent 对象并指定开关串。与上一个功能点的区别在于插入方式:需要手动将目录对象加入段落,并补齐域分隔符与域结束标记,同时将该对象赋给 document.TOC。常用的域开关及其含义如下:
| 开关 | 说明 |
|---|---|
\o "1-3" |
按内置标题样式收集条目,此处表示收录 Heading 1 至 Heading 3 |
\h |
将目录条目设为超链接,点击即可跳转到对应章节 |
\z |
在 Web 版式视图中隐藏页码与制表符前导符 |
\u |
按段落的大纲级别收集条目 |
例如将开关串改为 \o "1-2",目录就只收录一级和二级标题,三级标题不再出现在目录中;去掉 \h 则目录条目不再具备跳转能力。
function App() {
const CustomizeTableOfContent = async () => {
// 获取 Spire.Doc WASM 模块
const docModule = window.wasmModule?.spiredoc;
// 确保 WASM 模块完全加载完成
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 将字体载入 VFS
await window.spire.FetchFileToVFS('msyh.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 创建文档实例并添加节
const doc = new docModule.Document();
let section = doc.AddSection();
// 构造带自定义域开关的目录对象
let toc = new docModule.TableOfContent(doc, "{\\o \"1-2\" \\h \\z \\u}");
// 将目录对象加入段落
let tocPara = section.AddParagraph();
tocPara.Items.Add(toc);
// 补齐域分隔符与域结束标记
tocPara.AppendFieldMark(docModule.FieldMarkType.FieldSeparator);
tocPara.AppendText("TOC");
tocPara.AppendFieldMark(docModule.FieldMarkType.FieldEnd);
// 将该目录绑定到文档
doc.TOC = toc;
// 添加文档标题
let characterFormat = new docModule.CharacterFormat(doc);
characterFormat.FontName ="微软雅黑";
let title = section.AddParagraph();
let titleRun = title.AppendText("Spire.Doc 开发指南");
titleRun.ApplyCharacterFormat(characterFormat);
titleRun.CharacterFormat.FontSize = 24;
title.Format.HorizontalAlignment = docModule.HorizontalAlignment.Center;
// 第一章 概述(一级标题)
let p = section.AddParagraph();
p.AppendText("第一章 概述").ApplyCharacterFormat(characterFormat);
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
section.AddParagraph().AppendText("Spire.Doc for JavaScript 让开发者可以直接在浏览器中创建、编辑与保存 Word 文档,整个过程无需任何后端服务参与。").ApplyCharacterFormat(characterFormat);
// 1.1 什么是 Spire.Doc for JavaScript(二级标题)
p = section.AddParagraph();
p.AppendText("1.1 什么是 Spire.Doc for JavaScript").ApplyCharacterFormat(characterFormat);
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
section.AddParagraph().AppendText("它是一套基于 WebAssembly 的 Word 文档处理库,通过虚拟文件系统(VFS)管理字体与文档文件,并提供与 .NET 版一致的 API 形态。").ApplyCharacterFormat(characterFormat);
// 1.2 适用场景(二级标题)
p = section.AddParagraph();
p.AppendText("1.2 适用场景").ApplyCharacterFormat(characterFormat);
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
section.AddParagraph().AppendText("适用于在线合同签署、报表批量生成、简历模板填充等需要在客户端完成文档处理的场景。").ApplyCharacterFormat(characterFormat);
// 第二章 核心能力(一级标题)
p = section.AddParagraph();
p.AppendText("第二章 核心能力").ApplyCharacterFormat(characterFormat);
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
section.AddParagraph().AppendText("Spire.Doc for JavaScript 覆盖了文档处理的完整链路,从内容构建、版式调整到格式导出均可在浏览器端完成。").ApplyCharacterFormat(characterFormat);
// 2.1 文档处理(二级标题)
p = section.AddParagraph();
p.AppendText("2.1 文档处理").ApplyCharacterFormat(characterFormat);
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
section.AddParagraph().AppendText("支持段落、样式、表格、图片、页眉页脚等常见文档元素的创建与修改,并保留原有的版式信息。").ApplyCharacterFormat(characterFormat);
// 2.1.1 段落与样式(三级标题)
p = section.AddParagraph();
p.AppendText("2.1.1 段落与样式").ApplyCharacterFormat(characterFormat);
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading3 });
section.AddParagraph().AppendText("通过 AddParagraph 新增段落,配合 ApplyStyle 应用内置样式,即可快速搭建结构清晰的文档骨架。").ApplyCharacterFormat(characterFormat);
// 2.1.2 表格与图片(三级标题)
p = section.AddParagraph();
p.AppendText("2.1.2 表格与图片").ApplyCharacterFormat(characterFormat);
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading3 });
section.AddParagraph().AppendText("表格与图片既可以直接写入指定段落,也可以嵌套在文本框内,满足复杂版式的排版需求。").ApplyCharacterFormat(characterFormat);
// 2.2 格式转换(二级标题)
p = section.AddParagraph();
p.AppendText("2.2 格式转换").ApplyCharacterFormat(characterFormat);
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
section.AddParagraph().AppendText("通过 SaveToFile 可以将文档转换为 PDF、HTML、Markdown 等多种格式,整个转换过程均在浏览器端完成。").ApplyCharacterFormat(characterFormat);
// 2.3 批量处理(二级标题)
p = section.AddParagraph();
p.AppendText("2.3 批量处理").ApplyCharacterFormat(characterFormat);
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
section.AddParagraph().AppendText("结合 WebAssembly 的运行效率,可以一次加载多份文档并顺序完成处理,避免频繁的文件上传与下载。").ApplyCharacterFormat(characterFormat);
// 第三章 快速上手(一级标题)
p = section.AddParagraph();
p.AppendText("第三章 快速上手").ApplyCharacterFormat(characterFormat);
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
section.AddParagraph().AppendText("本章介绍在 React 项目中接入 Spire.Doc for JavaScript 并完成第一次文档生成所需的准备工作。").ApplyCharacterFormat(characterFormat);
// 3.1 环境准备(二级标题)
p = section.AddParagraph();
p.AppendText("3.1 环境准备").ApplyCharacterFormat(characterFormat);
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
section.AddParagraph().AppendText("在 React 项目中安装 Spire.Doc for JavaScript,并将字体文件与 WASM 资源放入 public 目录即可开始使用。").ApplyCharacterFormat(characterFormat);
// 3.2 第一个示例(二级标题)
p = section.AddParagraph();
p.AppendText("3.2 第一个示例").ApplyCharacterFormat(characterFormat);
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
section.AddParagraph().AppendText("初始化模块后创建 Document 实例,添加内容并保存,最后从 VFS 读取结果文件触发浏览器下载。").ApplyCharacterFormat(characterFormat);
// 更新目录,填充条目与页码
doc.UpdateTableOfContents();
// 定义输出文件名并保存
const outputFileName = "在 Word 文档中创建自定义目录.docx";
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
// 释放资源
doc.Dispose();
// 从 VFS 读取生成的文件,触发下载
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
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>点击以下按钮在 Word 文档中创建自定义目录</h1>
<button onClick={CustomizeTableOfContent}>
生成
</button>
</div>
);
}
export default App;
通过 TableOfContent 对象与自定义域开关生成的目录,条目层级、超链接与页码表现均由开关串决定。

原因:目录域按标题样式收集条目。如果段落没有应用 Heading1 至 Heading3 等内置标题样式,即使成功插入了目录域,更新后目录中也不会出现任何条目。
解决:对需要出现在目录中的段落调用 ApplyStyle 应用标题样式:
p.AppendText("第一章 概述");
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
原因:AppendTOC 的两个参数分别对应目录收录的起始与结束标题级别,若传入 AppendTOC(1, 2),三级标题将不会出现在目录中。使用自定义开关时,\o "1-2" 也会产生同样的结果。
解决:按需要收录的层级调整参数范围,例如收录一至三级标题:
tocPara.AppendTOC(1, 3);
原因:AppendTOC 只是插入了目录域本身,域内容需要显式更新。若在保存前未调用 UpdateTableOfContents,生成的目录只有域代码,没有条目和页码。
解决:在 SaveToFile 之前调用更新方法:
doc.UpdateTableOfContents();
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
Excel 除用于存放单元格数据外,也常被用作文件容器:报价单中可嵌入 Word 版合同条款,产品表中可附带 PDF 规格书,双击对象即可直接打开源文件。此类嵌入工作表的文件称为 OLE 对象(Object Linking and Embedding)。在 Excel 界面中插入 OLE 对象仅需「插入 → 对象」两步操作,但若需在浏览器端由程序批量完成,则须借助专门的 API。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成上述操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。
OleObjects.Add 用于将外部文件插入工作表,共接受三个参数:待嵌入的文件名、对象在工作表上显示的图标,以及链接方式——OleLinkType.Embed 表示将文件嵌入工作簿,OleLinkType.Link 表示以链接方式插入。对象插入后,用 Location 指定锚定到哪个单元格、用 ObjectType 声明嵌入的文件类型,Excel 据此确定双击时调用哪个程序打开。具体操作步骤如下:
OleObjects.Add 将该 Excel 文件嵌入工作表。Location 与 ObjectType。以下为完整的代码示例,演示如何在 React 中将 Excel 文件作为 OLE 对象插入工作表:
function App() {
const insertOleObject = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将中文字体载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 将被嵌入的 Excel 文件载入 VFS
const embeddedFileName = 'OLEObjects.xlsx';
await window.spire.FetchFileToVFS(embeddedFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
// 新建工作簿,并写入说明文字
const workbook = new xlsModule.Workbook();
const sheet = workbook.Worksheets.get(0);
sheet.Range.get("A1").Text = "这里是一个 OLE 对象。";
// 打开被嵌入的工作簿,把它的工作表渲染成图片,作为 OLE 对象的显示图标
const embeddedBook = new xlsModule.Workbook();
embeddedBook.LoadFromFile(embeddedFileName);
const embeddedSheet = embeddedBook.Worksheets.get(0);
embeddedSheet.PageSetup.LeftMargin = 0;
embeddedSheet.PageSetup.RightMargin = 0;
embeddedSheet.PageSetup.TopMargin = 0;
embeddedSheet.PageSetup.BottomMargin = 0;
const image = embeddedSheet.ToImage(1, 1, 19, 5);
embeddedBook.Dispose();
// 把 Excel 文件嵌入工作表,文件数据随工作簿一起保存
const oleObject = sheet.OleObjects.Add(
embeddedFileName,
image,
xlsModule.OleLinkType.Embed
);
// 把对象锚定到 B4 单元格,并声明它是一份 Excel 工作表
oleObject.Location = sheet.Range.get("B4");
oleObject.ObjectType = xlsModule.OleObjectType.ExcelWorksheet;
// 保存工作簿
const outputFileName = "InsertOLEObject.xlsx";
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放资源
workbook.Dispose();
// 从 VFS 读取结果文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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>插入 OLE 对象</h1>
<button onClick={insertOleObject}>Start</button>
</div>
);
}
export default App;
示例中的图标直接取自被嵌入工作表的渲染结果,因此 OLE 对象在工作表中显示的是其自身内容。将 ObjectType 改为 OleObjectType.WordDocument、OleObjectType.AdobeAcrobatDocument 等值,即可声明其他类型的嵌入文件。
运行后,插入 Excel 工作簿作为 OLE 对象的效果:

ToImage 每次均需先打开工作簿、再按行列范围渲染,适用于需要让对象呈现自身内容的场景;若要为附件统一配图标,直接读取一张现成的图片更为简便,且同一张图片可复用于多个附件。OleObjects.Add 的第二个参数本身即同时接受这两种入参。具体操作步骤如下:
new xlsModule.Stream 将图标图片读取为流。OleObjects.Add 将附件以嵌入方式插入,并传入图标流。Location 与 ObjectType。以下为完整的代码示例,演示如何在 React 中插入带自定义图标的 PDF 附件:
function App() {
const insertOleObjectWithIcon = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将中文字体载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 将图标图片与附件载入 VFS
const iconFileName = 'OLEIcon.png';
const attachmentFileName = 'Attachment.pdf';
await window.spire.FetchFileToVFS(iconFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
await window.spire.FetchFileToVFS(attachmentFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
// 新建工作簿
const workbook = new xlsModule.Workbook();
const sheet = workbook.Worksheets.get(0);
// 把图标图片读成流,作为 OLE 对象在工作表上的显示图标
const iconStream = new xlsModule.Stream(iconFileName);
// 把 PDF 附件嵌入工作表
const oleObject = sheet.OleObjects.Add(
attachmentFileName,
iconStream,
xlsModule.OleLinkType.Embed
);
// 把对象锚定到 B4 单元格,并声明它是一份 PDF 文档
oleObject.Location = sheet.Range.get("B4");
oleObject.ObjectType = xlsModule.OleObjectType.AdobeAcrobatDocument;
// 保存工作簿
const outputFileName = "InsertOLEObjectWithIcon.xlsx";
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放资源
workbook.Dispose();
// 从 VFS 读取结果文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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>插入带自定义图标的 OLE 对象</h1>
<button onClick={insertOleObjectWithIcon}>Start</button>
</div>
);
}
export default App;
运行后,插入带自定义图标的 PDF 附件作为 OLE 对象的效果:

原因:OleObjects.Add 的第二个参数决定 OLE 对象在工作表上的显示图标。使用 ToImage 取图时,若指定的区域落在工作表已用范围之外,得到的将是一张空白图片,插入的对象也只会显示空白。
解决:将取图区域限制在表格有内容的范围内,或改用一张现成的图片文件:
// 用固定图片作为图标,不依赖工作表内容
const iconStream = new xlsModule.Stream('OLEIcon.png');
const oleObject = sheet.OleObjects.Add('Attachment.pdf', iconStream, xlsModule.OleLinkType.Embed);
oleObject.Location = sheet.Range.get("B4");
oleObject.ObjectType = xlsModule.OleObjectType.AdobeAcrobatDocument;
ObjectType 设为其他值会有什么影响?原因:ObjectType 并非注释,其取值会被写入工作簿的 progId 字段,Excel 双击对象时依据该标识查找对应的程序。同一份 PDF 附件,声明为 OleObjectType.AdobeAcrobatDocument 时写出的 progId 为 Acrobat Document;若声明为 OleObjectType.ExcelWorksheet,写出的则变为 Worksheet,Excel 会尝试用 Excel 本身打开该 PDF,对象将无法打开。
解决:按嵌入文件的实际类型设置 ObjectType。常用取值如下:
| 嵌入文件 | ObjectType |
|---|---|
| Excel 工作簿 | OleObjectType.ExcelWorksheet |
| Word 文档 | OleObjectType.WordDocument |
| PowerPoint 演示文稿 | OleObjectType.PowerPointSlide |
| PDF 文档 | OleObjectType.AdobeAcrobatDocument |
// 按文件真实类型声明,Excel 才能用正确的程序打开
oleObject.ObjectType = xlsModule.OleObjectType.AdobeAcrobatDocument;
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
带图的报表攒到一定数量,麻烦的往往不是文字而是图片:几百 KB 的产品图直接插进工作表,工作簿体积迅速膨胀,发送、归档都变得吃力;从别处复制过来的图片尺寸又不统一,大的盖住整块数据区,小的缩在角落里看不清。这些调整在 Excel 里逐张手动拖拽尚可应付,一旦换成程序批量处理就没有可用的入口。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成这些操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
本文介绍三个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。
Compress() 按指定的质量比例重新编码图片,传入 50 即把图片质量降到原来的 50%。质量越低,图片所占的数据量越小,工作簿也就越轻。压缩只作用于图片本身的数据,图片在工作表中的位置和显示尺寸都保持不变,因此适合在不改动版面的前提下给文件瘦身。具体操作步骤如下:
Compress 将每张图片压缩到 50% 的质量。下面是一个完整的代码示例,展示了在 React 中压缩 Excel 中的图片:
function App() {
const compressPictures = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将中文字体载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 将 Excel 文件载入 VFS
const inputFileName = 'ResizeAndMovePictures.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
// 加载工作簿
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile(inputFileName);
// 遍历所有工作表中的所有图片
for (const sheet of workbook.Worksheets) {
for (const picture of sheet.Pictures) {
// 将图片质量压缩到 50%
picture.Compress(50);
}
}
// 保存工作簿
const outputFileName = "CompressPictures.xlsx";
workbook.SaveToFile(outputFileName);
// 释放资源
workbook.Dispose();
// 从 VFS 读取结果文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
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>压缩 Excel 中的图片</h1>
<button onClick={compressPictures}>Start</button>
</div>
);
}
export default App;
运行后,压缩 Excel 中图片的效果:

图片在工作表中的显示尺寸由 Width 和 Height 两个属性决定,单位是像素。给这两个属性重新赋值即可把图片改到指定大小——缩小的图不再压住旁边的数据区,放大的图也能填满预留的图片位。具体操作步骤如下:
sheet.Pictures.get(0) 获取工作表中的第一张图片。Width 和 Height 调整图片大小。下面是一个完整的代码示例,展示了在 React 中调整 Excel 中图片的大小:
function App() {
const resizePicture = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将中文字体载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 将 Excel 文件载入 VFS
const inputFileName = 'ResizeAndMovePictures.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
// 加载工作簿
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile(inputFileName);
// 获取第一个工作表
const sheet = workbook.Worksheets.get(0);
// 获取工作表中的第一张图片
const picture = sheet.Pictures.get(0);
// 调整图片大小
picture.Width = 140;
picture.Height = 140;
// 保存工作簿
const outputFileName = "ResizePicture.xlsx";
workbook.SaveToFile(outputFileName);
// 释放资源
workbook.Dispose();
// 从 VFS 读取结果文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
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>调整 Excel 中图片的大小</h1>
<button onClick={resizePicture}>Start</button>
</div>
);
}
export default App;
Width 与 Height 分别对应图片的宽度和高度,两者独立生效,赋值后图片立即按新尺寸显示。需要注意的是,这里设置的是图片框的尺寸而非等比缩放——宽高与原图比例不一致时图片会被拉伸,处理办法见下文常见问题。
运行后,调整 Excel 中图片大小的效果:

图片在 Excel 中是被锚定在某个单元格上的浮动对象,仅设置尺寸并不能改变它出现的位置。Left 和 Top 两个属性以像素为单位,指定图片左上角相对于工作表左上角的距离,赋值后图片便会移动到新的坐标处,用于把图片挪出数据区、或统一对齐到同一列图片位。具体操作步骤如下:
sheet.Pictures.get(0) 获取工作表中的第一张图片。Left 和 Top 移动图片位置。下面是一个完整的代码示例,展示了在 React 中移动 Excel 中图片的位置:
function App() {
const movePicture = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将中文字体载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 将 Excel 文件载入 VFS
const inputFileName = 'ResizeAndMovePictures.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
// 加载工作簿
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile(inputFileName);
// 获取第一个工作表
const sheet = workbook.Worksheets.get(0);
// 获取工作表中的第一张图片
const picture = sheet.Pictures.get(0);
// 调整图片位置
picture.Left = 360;
picture.Top = 180;
// 保存工作簿
const outputFileName = "MovePicture.xlsx";
workbook.SaveToFile(outputFileName);
// 释放资源
workbook.Dispose();
// 从 VFS 读取结果文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
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>移动 Excel 中图片的位置</h1>
<button onClick={movePicture}>Start</button>
</div>
);
}
export default App;
Left 与 Top 描述的是图片左上角在工作表中的绝对坐标,与图片原先锚定在哪一行、哪一列无关。
运行后,移动 Excel 中图片位置的效果:

原因:Width 和 Height 是两个彼此独立的属性,分别赋值时不会保留图片原有的宽高比。若把一张长方形图片的宽和高都设成同一个值,图片就会被强制拉成正方形。
解决:先读取图片当前的宽高算出比例,再按比例换算另一个方向的值:
// 读取图片当前的宽高,算出宽高比
const picture = sheet.Pictures.get(0);
const ratio = picture.Height / picture.Width;
// 固定宽度,按比例算出高度,图片不会被拉变形
picture.Width = 140;
picture.Height = Math.round(140 * ratio);
IsLockAspectRatio 能保证图片不变形吗?原因:不能。这个属性默认就是 true,它设置的是图片的锁定标记,约束的是在 Excel 里手动拖拽时的行为;用代码给 Width / Height 赋值时,它不会替你换算另一条边。实测把 Width 改成 140,无论 IsLockAspectRatio 是 true 还是 false,Height 都停在原来的 300。
解决:等比缩放仍然要自己按比例算。这个属性可以读写,改完能随文件保存下来,需要与文件里的锁定状态对齐时再设置:
// 锁定标记:默认 true,改为 false 会写进文件,重新打开后读回仍是 false
picture.IsLockAspectRatio = false;
// 但它不参与宽高换算:只改 Width,Height 会停在原值
picture.Width = 140;
// 要等比缩放,还是先算比例再赋值
const ratio = picture.Height / picture.Width;
picture.Width = 140;
picture.Height = Math.round(140 * ratio);
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
数据写完之后,表格往往还差最后一步才能用:产品名称被窄列挤得只剩几个字,备注里的一整段话铺在一行上、末尾被右侧有内容的单元格截掉。手动拖列宽、拉行高不仅慢,列一多还容易漏掉几列。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成这些操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。
行高由 AutoFitRow 按指定行中的内容自动算出,列宽由 AutoFitColumn 按指定列中的内容自动算出。两者都只作用于目标的那一行、那一列,表格其余部分保持原样,适合只修某一处最碍眼的溢出。具体操作步骤如下:
AutoFitRow 自动调整该行的行高。AutoFitColumn 自动调整该列的列宽。下面是一个完整的代码示例,展示了在 React 中自动调整单行的行高和单列的列宽:
function App() {
const autoFitSingleRowColumn = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将中文字体载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 将 Excel 文件载入 VFS
const inputFileName = 'AutoFitRowsAndColumns.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
// 加载工作簿
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile(inputFileName);
// 获取第一个工作表
const sheet = workbook.Worksheets.get(0);
// 自动调整第 2 行的行高
sheet.AutoFitRow(2);
// 自动调整第 4 列的列宽
sheet.AutoFitColumn(4);
// 保存工作簿
const outputFileName = "AutoFitSingleRowColumn.xlsx";
workbook.SaveToFile(outputFileName);
// 释放资源
workbook.Dispose();
// 从 VFS 读取结果文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
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>自动调整单行行高和单列列宽</h1>
<button onClick={autoFitSingleRowColumn}>Start</button>
</div>
);
}
export default App;
运行后,自动调整单行行高和单列列宽的效果:

整张表格都要收拾时,逐行逐列调用显然不现实。对一段单元格区域调用 AutoFitRows 或 AutoFitColumns,区域覆盖的每一行、每一列都会各自按内容重新计算尺寸,一次调用就能把整块数据排整齐,适合导出报表前的收尾。具体操作步骤如下:
AutoFitRows 自动调整范围内所有行的行高。AutoFitColumns 自动调整范围内所有列的列宽。下面是一个完整的代码示例,展示了在 React 中自动调整多行的行高和多列的列宽:
function App() {
const autoFitMultipleRowsColumns = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将中文字体载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 将 Excel 文件载入 VFS
const inputFileName = 'AutoFitRowsAndColumns.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
// 加载工作簿
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile(inputFileName);
// 获取第一个工作表
const sheet = workbook.Worksheets.get(0);
// 获取工作表中已使用的单元格范围
const range = sheet.AllocatedRange;
// 自动调整范围内所有行的行高
range.AutoFitRows();
// 自动调整范围内所有列的列宽
range.AutoFitColumns();
// 保存工作簿
const outputFileName = "AutoFitMultipleRowsColumns.xlsx";
workbook.SaveToFile(outputFileName);
// 释放资源
workbook.Dispose();
// 从 VFS 读取结果文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
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>自动调整多行行高和多列列宽</h1>
<button onClick={autoFitMultipleRowsColumns}>Start</button>
</div>
);
}
export default App;
运行后,自动调整多行行高和多列列宽的效果:

AutoFitRow(),行高却一点没变?原因:行高自适应只对需要折行的内容起作用。单元格没开自动换行时,文字始终排在一行内,行高跟着字号走,自适应算出来的结果和原行高一致,看上去就像没生效。
解决:先把 WrapText 设为 true,再调用行高自适应:
// 没开自动换行,行高自适应不会带来变化
sheet.Range.get("D2").Style.WrapText = false;
sheet.AutoFitRow(2);
// 开启自动换行后,行高会按折行后的行数重新计算
sheet.Range.get("D2").Style.WrapText = true;
sheet.AutoFitRow(2);
AutoFitColumns() 对合并单元格不起作用?原因:列宽自适应按区域内单个单元格的内容计算,合并单元格只有左上角那一格真正存放文字,区域内其余位置都是空的,算出来的宽度自然只够放下左上角的内容。
解决:合并单元格的列宽改用 ColumnWidth 手动指定:
// A7:D7 是合并单元格,自适应算不出合并后的总宽度
sheet.Range.get("A7:D7").Merge();
sheet.Range.get("A7:D7").AutoFitColumns();
// 改为手动指定列宽,让合并后的文字完整显示
sheet.Range.get("A7").ColumnWidth = 40;
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
扫描件方向不对、横版表格夹在一堆竖版页面里、打印前想把整份文档统一转个方向,这几种情况都不必重新排版。旋转角度是 PDF 页面自身的一个属性,改它只影响页面的显示方向,页面上的内容原样保留。
Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端直接加载、修改与保存 PDF 文档,页面旋转在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
整节页面要统一方向,在节上设一次就够:section.PageSettings.Rotate 取 PdfPageRotateAngle.RotateAngle90,该节所有页面顺时针转 90 度,RotateAngle180、RotateAngle270 可选,不设则不旋转。
function App() {
const createRotatedPdf = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将中文字体载入 VFS,供页面文字使用
await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 创建空白文档
const doc = new pdfModule.PdfDocument();
// 新建一个节,整节页面共用顺时针 90 度的旋转角度
const section = doc.Sections.Add();
section.PageSettings.Size = pdfModule.PdfPageSize.A4();
section.PageSettings.Rotate = pdfModule.PdfPageRotateAngle.RotateAngle90;
// 在该节中添加页面
const page = section.Pages.Add();
// 在页面上写一段说明文字
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 14 });
page.Canvas.DrawString({
s: '本页在创建时即设置为旋转 90 度',
font: font,
brush: pdfModule.PdfBrushes.get_Black(),
x: 40,
y: 60,
format: new pdfModule.PdfStringFormat({ alignment: pdfModule.PdfTextAlignment.Left })
});
// 保存并从 VFS 读回,触发下载
const outputFileName = '旋转新文档.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
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>新建页面时旋转</h1>
<button onClick={createRotatedPdf}>
开始创建
</button>
</div>
);
}
export default App;
若只是想在建页时顺带指定角度,
doc.Pages.Add(size, margins, rotation)这个重载同样可用。
创建时即设为旋转 90 度的 A4 页面:

已有文档要改方向,角度存在页面自己的 Rotation 属性里,先读出当前值,再累加本次的旋转量。要注意写进去的是 PdfPageRotateAngle 的枚举数值(不旋转为 0,90 度对应 1),而不是角度本身。
function App() {
const rotateExistingPage = 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 文档
const doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// 取文档第 1 页,读出当前角度并在其基础上累加本次的旋转量
const page = doc.Pages.get_Item(0);
let rotation = page.Rotation.value + pdfModule.PdfPageRotateAngle.RotateAngle90.value;
// 枚举数值只有 0~3,加到 4 表示转满一圈,回到不旋转
if (rotation === 4) {
rotation = 0;
}
page.Rotation = rotation;
// 保存并从 VFS 读回,触发下载
const outputFileName = '旋转指定页.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
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>旋转指定页面</h1>
<button onClick={rotateExistingPage}>
开始旋转
</button>
</div>
);
}
export default App;
只有第 1 页旋转 90 度,其余页面方向不变:

Rotation 赋值时提示 "Value is not an integer"原因:page.Rotation 的写入端只接受整数。直接传 PdfPageRotateAngle 的枚举对象会抛 Assert failed: Value is not an integer: PdfPageRotateAngle.RotateAngle90 (object)。
解决:取枚举的 value 再赋值:
page.Rotation = pdfModule.PdfPageRotateAngle.RotateAngle90.value;
原因:PdfPageRotateAngle 的枚举数值是 0、1、2、3,分别对应 0、90、180、270 度,写入端要的是这个数值,不是角度。写成 page.Rotation = 90 不会报错,但得到的不是 90 度;用 PdfPageRotateAngle.fromValue(90) 反查会直接抛 Invalid value for spirepdfPdfPageRotateAngle。
解决:换算成枚举数值再写入:
// 90 度
page.Rotation = pdfModule.PdfPageRotateAngle.RotateAngle90.value;
// 180 度
page.Rotation = pdfModule.PdfPageRotateAngle.RotateAngle180.value;
section.Pages.Add(...) 上带旋转参数为什么没效果原因:带旋转参数的重载 Add(size, margins, rotation) 只在 doc.Pages 上生效。写到 section.Pages 上时这个参数会被忽略,页面方向改由所在节的 PageSettings.Rotate 决定,结果是文件里的 /Rotate 仍是 0,页面没有旋转,也不会有任何报错。
解决:两种写法二选一,别把参数传给 section.Pages:
// 写法一:节级设置,作用于该节全部页面
section.PageSettings.Rotate = pdfModule.PdfPageRotateAngle.RotateAngle90;
section.Pages.Add();
// 写法二:在 doc.Pages 上建页并传入角度
const page = doc.Pages.Add(
pdfModule.PdfPageSize.A4(),
new pdfModule.PdfMargins(),
pdfModule.PdfPageRotateAngle.RotateAngle90
);
页面建好之后仍可单独调整,page.Rotation 会覆盖节级设置:
// 只把第 2 页改成 180 度,其余页面不受影响
doc.Pages.get_Item(1).Rotation = pdfModule.PdfPageRotateAngle.RotateAngle180.value;
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。