合并域有两种「不做替换也能改输出」的用法,常被忽略。
一种是域开关:域代码里跟在域名后面的 \b、\f、\* Upper 之类,能让同一个值以不同形态落进文档——加个前缀、后缀,或者统一转成大写。它是模板的一部分,改模板就能改行为。
另一种是条件域:文档中不写入任何固定语句,而是插入一个 IF 域,由 Word 在保存时依据当前数据判断应显示哪一句。这类域由代码动态构造。
Spire.Doc for JavaScript 在浏览器端处理这两件事:域开关由模板中的域代码直接描述,条件域则由代码构造后再交由 Word 求值。整个过程基于 WebAssembly 运行,用虚拟文件系统(VFS)管理模板文档,不需要后端服务。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript 的方法。以下示例默认已安装 Spire.Doc 并完成 WebAssembly 模块初始化。
在合并域里使用域开关
准备模板
为了把各个开关的输出摆在一起对照,模板里让同一个域 XX_Name 重复出现九次,每一处挂上不同的开关:
联系人
默认:«XX_Name»
\b "Mr ":«XX_Name»
\b Mr:«XX_Name»
\f " (Author)":«XX_Name»
\* Upper:«XX_Name»
\* Lower:«XX_Name»
\* FirstCap:«XX_Name»
\* Caps:«XX_Name»
\* MERGEFORMAT:«XX_Name»
在 Word 里展开域代码后,每一行大致是这样({ } 是 Word 的域括号,实际编辑时按 Ctrl + F9 输入):
{ MERGEFIELD XX_Name \b "Mr " \* MERGEFORMAT }
{ MERGEFIELD XX_Name \b Mr \* MERGEFORMAT }
{ MERGEFIELD XX_Name \f " (Author)" \* MERGEFORMAT }
{ MERGEFIELD XX_Name \* Upper \* MERGEFORMAT }
…
域名与开关之间、开关与开关之间用空格分隔;开关自己的文本参数如果含空格,要用双引号括起来(\b "Mr "),此时引号里的空格是参数的一部分。
代码
function App() {
const MailMergeSwitches = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 将示例模板载入虚拟文件系统(VFS)
let inputFileName = "MailMergeSwitches.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// 新建文档并载入模板
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// 只提供一个域的值,模板里九处都用它
let fieldNames = ["XX_Name"];
let fieldValues = ["jack tang"];
// 执行邮件合并
doc.MailMerge.Execute(fieldNames, fieldValues);
// 定义输出文件名
const outputFileName = "MailMergeSwitches-result.docx";
// 保存文档
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
doc.Dispose()
// 读取保存的文件并转换为 Blob 对象
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={MailMergeSwitches}>开始</button>
</div>
);
}
export default App;
代码侧没有任何开关相关的 API——开关全部写在模板的域代码里,Execute 只负责给 XX_Name 一个值。这正是域开关的用法:数据不变,形态由模板决定。
这里故意把值写成全小写的 "jack tang"。如果传 "Jack Tang",几个大小写类开关的输出会和默认行长得一样,看不出区别。
效果
| 模板里的写法 | 输出 |
|---|---|
«XX_Name» |
jack tang |
«XX_Name \b "Mr "» |
Mr jack tang |
«XX_Name \b Mr» |
Mrjack tang |
«XX_Name \f " (Author)"» |
jack tang (Author) |
«XX_Name \* Upper» |
JACK TANG |
«XX_Name \* Lower» |
jack tang |
«XX_Name \* FirstCap» |
Jack tang |
«XX_Name \* Caps» |
Jack Tang |
«XX_Name \* MERGEFORMAT» |
jack tang |
几个值得留意的点:
\b "Mr "与\b Mr的差别就是那对引号。 前者的参数是带尾随空格的Mr,输出Mr jack tang;后者的参数是没有空格的Mr,会紧贴在值前面变成Mrjack tang。要让文字和工作式之间留空,要么在引号里带上空格,要么把参数写成\b "Mr "这种形态。\f是后缀,\b是前缀,两者可以同时出现在一个域里。\* FirstCap只把第一个字符大写(Jack tang),\* Caps把每个单词的首字母都大写(Jack Tang)。这两个开关的名字很像,效果不同。\* MERGEFORMAT不影响文字,它保的是域的格式设置,Word 默认生成合并域时就会带上,留着无害。
构造并执行条件域
思路
条件域(IF 域)的结构是「一个域,里面嵌一个合并域作为判断依据,后面跟两个结果文本」:
{ IF { MERGEFIELD Count } > "1" "Greater than one" "Less than one" }
拆开看,这一段里包含四类元素:
- 一个
IfField,域类型设为FieldType.FieldIf; - 一个合并域节点(
FieldType.FieldMergeField),即条件里被比较的那个值; - 运算符与两个分支文本,作为普通文字片段追加;
- 一个
FieldMark,类型为FieldMarkType.FieldEnd,负责收尾——并要把它挂回IfField.End,域才闭合。
代码
这份文档没有任何输入模板,两个 IF 域都是从头建出来的:
function App() {
const ExecuteConditionalField = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 将 ARIALUNI.TTF 字体载入虚拟文件系统(VFS)
await window.spire.FetchFileToVFS('ARIALUNI.TTF','/Library/Fonts/',`${process.env.PUBLIC_URL}/static/font/`);
// 新建文档,添加一节两段
let doc = new docModule.Document();
let section = doc.AddSection();
let paragraph = section.AddParagraph();
let ifField1 = new docModule.IfField(doc);
ifField1.Type = docModule.FieldType.FieldIf;
ifField1.Code = "IF ";
paragraph.Items.Add(ifField1);
paragraph.AppendField("Count", docModule.FieldType.FieldMergeField);
paragraph.AppendText(" > ");
paragraph.AppendText("\"1\" ");
paragraph.AppendText("\"Greater than one\" ");
paragraph.AppendText("\"Less than one\"");
let end1 = doc.CreateParagraphItem(docModule.ParagraphItemType.FieldMark);
end1.Type = docModule.FieldMarkType.FieldEnd;
paragraph.Items.Add(end1);
ifField1.End = end1;
paragraph = section.AddParagraph();
let ifField2 = new docModule.IfField(doc);
ifField2.Type = docModule.FieldType.FieldIf;
ifField2.Code = "IF ";
paragraph.Items.Add(ifField2);
paragraph.AppendField("Age", docModule.FieldType.FieldMergeField);
paragraph.AppendText(" > ");
paragraph.AppendText("\"50\" ");
paragraph.AppendText("\"The old man\" ");
paragraph.AppendText("\"The young man\"");
let end2 = doc.CreateParagraphItem(docModule.ParagraphItemType.FieldMark);
end2.Type = docModule.FieldMarkType.FieldEnd;
paragraph.Items.Add(end2);
ifField2.End = end2;
// 为条件域里的合并域提供值
let fieldNames = ["Count", "Age"];
let fieldValues = ["2", "30"];
doc.MailMerge.Execute(fieldNames, fieldValues);
// 让域在保存时求值
doc.IsUpdateFields = true;
// 定义输出文件名
const outputFileName = "ExecuteConditionalField-result.docx";
// 保存文档
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
doc.Dispose()
// 读取保存的文件并转换为 Blob 对象
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={ExecuteConditionalField}>开始</button>
</div>
);
}
export default App;
几个必须按顺序来的细节:
ifField.Code = "IF "只是域类型标记。真正组装域主体的是随后的AppendField与若干AppendText——它们会被依次排进这个域里。- 运算符和两个分支文本都用
AppendText追加,其中的引号要写进字符串本身("\"1\" ")。IF 域要求两个结果文本都带引号,靠的就是这些引号字符。 - 顺序固定为:比较值 → 运算符 → 比较基准 → 真值分支 → 假值分支。假值分支的引号后面不需要再加空格。
FieldEnd是域的闭合标记,必须用document.CreateParagraphItem(wasmModule.ParagraphItemType.FieldMark)建出来、设好end.Type、追加到段落,然后赋给ifField.End。少了任何一步,域都不完整。document.IsUpdateFields = true决定域要不要在保存时求值,见下文。
效果
Count = "2"、Age = "30" 时,两个条件域分别取到真值分支与假值分支:
Greater than one
The young man
把值换掉就能翻转结果,模板与代码都不用改:
| 域值 | 第一段(IF Count > "1") |
第二段(IF Age > "50") |
|---|---|---|
Count = "2"、Age = "30" |
Greater than one |
The young man |
Count = "0"、Age = "70" |
Less than one |
The old man |
常见问题
条件域保存出来是两段空白
原因:没有设置 document.IsUpdateFields = true。域在文档里只是一个「待求值」的指令,不打开这个开关,保存时不会执行它,两个分支的文本都不会输出——连假值分支的文字也一并消失,所以看起来是空的。
解决:在 Execute 之后、SaveToFile 之前加上:
document.MailMerge.Execute(fieldNames, fieldValues);
document.IsUpdateFields = true; // 让域在保存时求值
document.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Docx2013 });
\b 加的前缀和名字粘在一起了
原因:开关的文本参数不自带空格。\b Mr 的参数就是三个字符 Mr,输出时的前后间距完全由参数本身决定。
解决:想让名字与前缀之间留空,就把空格写进参数并用引号括起来:
{ MERGEFIELD XX_Name \b "Mr " \* MERGEFORMAT }
\f 同理。也可以反过来把空格写在域外面:在 Word 里让域后面跟一个空格,但那属于段落文字,和开关是两回事。
\* FirstCap 和 \* Caps 有什么区别
\* FirstCap 只把第一个字符转成大写,其余保持原样:jack tang → Jack tang。
\* Caps 把每个单词的首字母都转成大写:jack tang → Jack Tang。
要验证这两个开关,域名对应的值必须是小写或者大小写混合的;如果值本身就是 Jack Tang,两行输出会和默认行完全一样。
某些开关写上去没有任何效果
不是所有 Word 域开关都参与邮件合并的文本替换。\* MERGEFORMAT 保的是域的格式设置,本来就不改文字;\m、\v、\# 0.00(数字格式)之类的开关在合并域上使用时,对当前这种纯文本值也没有可见影响。实际使用中建议只用本文验证过的那几个:\b、\f、\* Upper、\* Lower、\* FirstCap、\* Caps。
域名、开关、参数之间的空格怎么算
- 域名与第一个开关之间:至少一个空格;
- 开关之间:至少一个空格;
- 开关的文本参数:与开关字母之间也要有空格(
\b与Mr之间),参数自身带空格或特殊字符时用双引号括起来。
多写几个空格不影响解析,但少写空格会让参数被吞进上一步——例如把 \* Upper 写成 \*Upper,某些解析路径下开关会不生效。
修改 Count、Age 的值以后输出没变
原因:条件域的结果文本在文档里是缓存的,只有 IsUpdateFields 打开时才会重新求值。同时要确认传给 Execute 的域名与 AppendField 里写的名字一致(区分大小写)。
解决:确认 fieldNames 与 AppendField 的第一个参数完全一致,并保持 IsUpdateFields = true。
获取免费许可证
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。







