合同改稿、方案评审、制度修订,最终都要回答同一个问题:这一版与上一版究竟改了哪里。逐页对照速度慢,也容易漏看;把比较结果固化为修订标记,则可以在 Word 中逐条确认,再把差异整理成清单交给审阅人。
本文用 Spire.Doc for JavaScript 在浏览器中完成上述工作,示例使用同一份《“星澜”会员数据中台建设项目合作方案》的两个版本:Original.docx 为 10 月 15 日稿,Revised.docx 为 10 月 28 日评审稿,两版在编制日期、金额、条款标题与页眉等处共有十余处改动。整个过程基于 WebAssembly 在浏览器端完成,无需安装 Office,也不依赖任何后端,文档留在用户的设备上。
本文介绍三个功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript。下面的示例假定 Spire.Doc 已安装、WebAssembly 模块已初始化。
比较两个 Word 文档并生成修订版
比较两个 Word 文档并生成修订版,由 Document.Compare() 一次调用完成。该方法以「原件 + 修订件」的方式工作:调用方作为原件,参数传入的文档作为修订件,比较结果以修订标记写回调用方的文档。
| 角色 | 做法 | 说明 |
|---|---|---|
| 原件 | 调用 Compare() 的文档 |
比较结束后被改写为带修订标记的版本 |
| 修订件 | 作为参数传入的文档 | 只读取其中的内容,本身不被改写 |
| 作者 | 第二个参数传入的字符串 | 写入每一条修订的作者名 |
比较会改写原件对象本身。需要保留原始版本时,应重新加载该文件或另存副本,不要直接保存比较之后的原件对象。
具体操作步骤如下:
- 获取 Spire.Doc 的 WASM 模块,确认已就绪。
- 把字体文件与
Original.docx、Revised.docx载入 VFS。 - 分别创建两个
Document实例并加载两份文档。 - 以原件调用
Compare(),传入修订件与作者名。 - 保存结果文档,从 VFS 读出并触发浏览器下载。
比较两个 Word 文档的代码如下:
function App() {
const compareDocuments = async () => {
// 获取 Spire.Doc 的 WASM 模块
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 载入两份示例文档
await window.spire.FetchFileToVFS('Original.docx', '', `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS('Revised.docx', '', `${process.env.PUBLIC_URL}/data/`);
// 原件与修订件分别加载
const original = new docModule.Document();
original.LoadFromFile('Original.docx');
const revised = new docModule.Document();
revised.LoadFromFile('Revised.docx');
// 以原件调用 Compare,修订标记写入原件
original.Compare(revised, '合同审阅');
// 保存带修订标记的结果文档
const outputFileName = 'CompareResult.docx';
original.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
// 释放两个文档实例
revised.Dispose();
original.Dispose();
// 从 VFS 读取结果并触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], {
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={compareDocuments}>开始比较</button>
</div>
);
}
export default App;
结果文档在 Word 中打开即为修订视图,插入内容带下划线、删除内容带删除线,正文结构与页边距保持原样:

图中编制日期一栏同时出现删除线与下划线,对应的是一处「旧日期改为新日期」的修订;第四阶段整段为新增内容,因此整段带下划线;「报价不含服务器、存储等硬件采购费用」一段被删除,整段显示删除线。页眉中的「(评审稿)」属于新增内容,同样以修订形式记录。
用 HasChanges 判断两份文档是否存在差异
比较结束后,Document.HasChanges 返回文档中是否存在修订,可以在保存之前先做一次判断。两份文档的内容与格式完全一致时,该属性为 false,四个修订集合也都为空:
original.Compare(revised, '合同审阅');
// 没有差异时不必再保存结果文档
if (!original.HasChanges) {
alert('两份文档没有差异');
return;
}
按选项控制 Word 文档的比较粒度
按选项控制 Word 文档的比较粒度,由 CompareOptions 的四个成员决定。默认情况下,比较会把格式、表格与页眉页脚的差异一并记录,而实际场景往往只关心其中一部分:制度修订只关心条款文字,表格版本核对只看单元格内容,格式调整则可以先整体忽略。
| 成员 | 默认值 | 作用 |
|---|---|---|
IgnoreFormatting |
false |
忽略字符与段落格式差异 |
IgnoreTable |
false |
忽略表格,表格中的改动不参与比较 |
IgnoreHeadersAndFooters |
false |
忽略页眉与页脚 |
TextCompareLevel |
TextDiffMode.Word |
文本比较粒度,可改为 TextDiffMode.Char |
选项需要通过对象形式传入,即 Compare({ document, author, options })。若按「文档、作者、选项」的顺序以位置参数调用,会提示重载不唯一(Ambiguous call),改用对象形式即可。
具体操作步骤如下:
- 载入字体与两份文档,创建原件与修订件实例并分别加载。
- 创建
CompareOptions实例,按需开启忽略项。 - 以对象形式传入
document、author与options,调用Compare()。 - 保存结果文档,从 VFS 读出并触发浏览器下载。
按选项比较的代码如下:
function App() {
const compareWithOptions = async () => {
// 获取 Spire.Doc 的 WASM 模块
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
await window.spire.FetchFileToVFS('Original.docx', '', `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS('Revised.docx', '', `${process.env.PUBLIC_URL}/data/`);
const original = new docModule.Document();
original.LoadFromFile('Original.docx');
const revised = new docModule.Document();
revised.LoadFromFile('Revised.docx');
// 逐项设置比较范围
const options = new docModule.CompareOptions();
// 只关心文字变化,不记录字体、段落格式的调整
options.IgnoreFormatting = true;
// 表格内容另行核对,不纳入本次比较
options.IgnoreTable = true;
// 页眉页脚由模板统一维护
options.IgnoreHeadersAndFooters = true;
// 选项以对象形式传入
original.Compare({ document: revised, author: '合同审阅', options });
const outputFileName = 'CompareResultOptions.docx';
original.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
revised.Dispose();
original.Dispose();
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], {
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={compareWithOptions}>开始比较</button>
</div>
);
}
export default App;
同一份文档,默认比较与开启忽略选项后的对比如下:

左侧为默认比较的结果:页眉多出「(评审稿)」,文档信息表中的编制日期与「标签计算」一行的说明文字都以修订形式标出。右侧开启三个忽略项后,页眉恢复为原件的文字,表格中的两处改动也不再生成修订标记,表格内容保持原件的写法。
按字符比较文本差异
默认按词比较,一处文字替换以整词为单位记录:示例中金额由「268」改为「280」,结果记为「删除 268」与「插入 280」两条。改为按字符比较后,只有前后相同的字符保留为公共内容,同一处改动变成「删除 68」与「插入 80」;多行文字同时改写时,零散条目会明显增加。
const options = new docModule.CompareOptions();
// 文本按字符比较,差异定位到单个字符
options.TextCompareLevel = docModule.TextDiffMode.Char;
original.Compare({ document: revised, author: '合同审阅', options });
忽略格式后,结果文档沿用原件的格式
开启 IgnoreFormatting 后,结果文档中不再出现格式修订标记,相关段落沿用原件的字体;修订件里调整的字体不会写入结果。示例中的「七、其他约定」标题在修订件里由黑体改为楷体,开启该选项后,结果文档中仍是原件的黑体。
const options = new docModule.CompareOptions();
// 只比较文字,格式变化不生成修订标记
options.IgnoreFormatting = true;
original.Compare({ document: revised, author: '合同审阅', options });
需要在结果中体现修订件的版式时,不要开启该选项,让格式差异以修订形式保留下来。
提取并导出 Word 文档的差异清单
提取并导出 Word 文档的差异清单,基于 DifferRevisions 完成。修订标记适合在 Word 中逐条确认,批量场景则需要一份可检索的清单:DifferRevisions 按类型把比较产生的修订分组,可以分别取出插入、删除、移入与移出四类内容,再拼成文本导出。
| 成员 | 作用 |
|---|---|
new DifferRevisions(doc) |
基于比较后的文档创建修订集合 |
InsertRevisions |
插入内容的修订集合 |
DeleteRevisions |
删除内容的修订集合 |
MoveToRevisions |
移入内容的修订集合 |
MoveFromRevisions |
移出内容的修订集合 |
四个集合都提供 Count 属性与 get(i) 方法,按索引逐项取出元素。
具体操作步骤如下:
- 载入字体与两份文档,完成比较。
- 基于比较后的文档创建
DifferRevisions。 - 依次遍历四个修订集合,取出文本对象并读取其中的文字。
- 把结果拼成制表符分隔的文本,写入 VFS,同时保存比较结果文档。
- 分别下载结果文档与清单文件。
提取差异清单的代码如下:
function App() {
const extractDifferences = async () => {
// 获取 Spire.Doc 的 WASM 模块
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
await window.spire.FetchFileToVFS('Original.docx', '', `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS('Revised.docx', '', `${process.env.PUBLIC_URL}/data/`);
const original = new docModule.Document();
original.LoadFromFile('Original.docx');
const revised = new docModule.Document();
revised.LoadFromFile('Revised.docx');
original.Compare(revised, '合同审阅');
// 基于比较后的文档创建修订集合
const revisions = new docModule.DifferRevisions(original);
const rows = [];
const collect = (collection, label) => {
for (let i = 0; i < collection.Count; i++) {
const item = collection.get(i);
// 修订集合中同时存在段落与文本对象,这里只取文本对象
if (item.DocumentObjectType !== docModule.DocumentObjectType.TextRange) continue;
const text = docModule.TextRange.Convert(item).Text.trim();
if (text) rows.push(`${label}\t${text}`);
}
};
collect(revisions.InsertRevisions, '插入');
collect(revisions.DeleteRevisions, '删除');
collect(revisions.MoveToRevisions, '移入');
collect(revisions.MoveFromRevisions, '移出');
// 第一列为修订类型,第二列为修订内容
const report = `类型\t内容\r\n${rows.join('\r\n')}\r\n`;
window.dotnetRuntime.Module.FS.writeFile('CompareReport.txt', report);
// 比较结果与清单都基于同一次比较生成
original.SaveToFile({ fileName: 'CompareResult.docx', fileFormat: docModule.FileFormat.Docx2013 });
revisions.Dispose();
revised.Dispose();
original.Dispose();
// 依次下载结果文档与清单文件
const outputs = [
['CompareResult.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
['CompareReport.txt', 'text/plain;charset=utf-8'],
];
for (const [fileName, mime] of outputs) {
const fileArray = window.dotnetRuntime.Module.FS.readFile(fileName);
const blob = new Blob([fileArray], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
}
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>提取 Word 文档差异清单</h1>
<button onClick={extractDifferences}>提取差异</button>
</div>
);
}
export default App;
导出的差异清单如下,共 12 条插入、8 条删除:

清单中的条目按修订集合的顺序排列,先插入、后删除。可以看到,「2026 年 10 月 15 日」与「2026 年 10 月 28 日」分别列为删除与插入;条款标题的改写同样分成「删除 六、保密与知识产权」与「插入 六、数据安全与知识产权」两行;新增段落前的列表符号「●」与第四阶段正文也各占一行。
修订集合中的对象类型与文字提取
修订集合中的元素是文档对象,可能是 TextRange,也可能是 Paragraph。TextRange.Convert() 只接受文本对象,对段落对象会抛出类型转换错误;段落应改用 Paragraph.Convert() 读取文字。示例按 DocumentObjectType 只取文本对象,同一处改动因此只占一行:
for (let i = 0; i < collection.Count; i++) {
const item = collection.get(i);
// 文本对象:用 TextRange.Convert 读取文字
if (item.DocumentObjectType === docModule.DocumentObjectType.TextRange) {
const text = docModule.TextRange.Convert(item).Text.trim();
}
// 段落对象:用 Paragraph.Convert 读取文字
if (item.DocumentObjectType === docModule.DocumentObjectType.Paragraph) {
const paragraphText = docModule.Paragraph.Convert(item).Text.trim();
}
}
移动段落会以插入与删除的形式记录
把一段内容整体前移或后移时,比较结果通常记为一条删除加一条插入,MoveToRevisions 与 MoveFromRevisions 在实测中保持为空。清单因此以插入、删除两类为主,遍历移动类集合主要用于完整性检查,避免在产生移动修订时遗漏:
// 移动类集合可能为空,遍历前无需特殊判断,Count 为 0 时循环不执行
collect(revisions.MoveToRevisions, '移入');
collect(revisions.MoveFromRevisions, '移出');
常见问题
比较 Word 文档时,文件需要上传到服务器吗
本文的用法不需要上传。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端运行,两份文档通过虚拟文件系统(VFS)在本地读写,加载、比较、保存都在本地完成,库本身不会把文件发往服务器。文件最终是否会离开设备,取决于你在页面上另外接入的代码。
结果文档里的数字为什么连在一起显示,例如「268280」
原因:修订标记把删除的「268」与插入的「280」放在同一位置相邻显示。删除部分带删除线、插入部分带下划线,字号较小时容易看成连写,服务周期一栏的「68」也是同样的情形。
解决:改用差异清单逐条核对。清单把同一处改动拆成两行,原值与新值一目了然:
// 清单中同一处改动会拆成两行
// 删除 268
// 插入 280
比较要求两份文档的格式完全一致吗
不要求。默认比较会同时检查内容与格式,字体、段落间距等差异会作为格式修订一并记录,不影响文字差异的识别。若这些差异并非审阅重点,可以开启 IgnoreFormatting,让清单只保留文字改动。
const options = new docModule.CompareOptions();
options.IgnoreFormatting = true;
original.Compare({ document: revised, author: '合同审阅', options });
开启忽略格式后,结果文档的字体为什么仍是原件的样子
原因:IgnoreFormatting 只是不再记录格式修订,比较结果仍以原件为底稿,修订件中的字体调整不会写入结果。示例里「七、其他约定」由黑体改为楷体,开启该选项后结果文档中仍是黑体。
解决:需要在结果中体现修订件的版式时,保持该项为默认值,让格式差异以修订形式保留下来:
const options = new docModule.CompareOptions();
// 保留格式差异,交由审阅人逐条确认
options.IgnoreFormatting = false;
original.Compare({ document: revised, author: '合同审阅', options });
按字符比较后,清单里为什么出现「删除 68」「插入 80」这样的片段
原因:按词比较以词为单位记录改动,一处替换是「删除 268」与「插入 280」;按字符比较逐个比对字符,前后相同的「2」被当作公共内容保留,剩下的「68」与「80」就各自成了独立条目。同一段落里多行文字同时改写时,这类零散片段会更多。
解决:清单以可读性为主时,保持默认的 TextDiffMode.Word;确实需要定位到单个字符时,再改用 TextDiffMode.Char 比较:
const options = new docModule.CompareOptions();
// 粒度更细,差异定位到单个字符
options.TextCompareLevel = docModule.TextDiffMode.Char;
original.Compare({ document: revised, author: '合同审阅', options });
获取免费许可证
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。







