将文档中的占位符替换为 HTML 内容或另一个文档的段落,是文档自动化中非常实用的需求——例如将富文本编辑器(如 TinyMCE、Quill)中编排好的 HTML 内容填入 Word 模板的占位符位置;或从标准条款库文档中提取指定段落,替换到合同模板中的对应位置。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接完成此类替换操作,通过虚拟文件系统(VFS)管理字体及文档文件,无需后端服务支持。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript。以下示例默认已安装 Spire.Doc 并完成 WebAssembly 模块初始化。
占位符替换为 HTML
将占位符替换为 HTML 的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件、HTML 文件和目标 Word 文档载入 WASM 虚拟文件系统;然后创建临时 Section,通过 AppendHTML 将 HTML 字符串渲染为文档对象并收集到替换列表中,再调用 FindAllString 查找所有 [#placeholder] 占位符,对匹配位置排序后逐一用 ChildObjects.Insert 插入替换内容并移除原文本;最后移除临时 Section,保存文档,从 VFS 读取生成的文件,封装为 Blob 后触发浏览器下载。
import React, { useState } from 'react';
function App() {
// 定义占位符替换逻辑
function ReplacedWithHTML(location, replacement) {
let textRange = location.Text;
let index = location.Index;
let paragraph = location.Owner;
let sectionBody = paragraph.OwnerTextBody;
let paragraphIndex = sectionBody.ChildObjects.IndexOf(paragraph);
let replacementIndex = -1;
if (index === 0) {
paragraph.ChildObjects.RemoveAt(0);
replacementIndex = sectionBody.ChildObjects.IndexOf(paragraph);
} else if (index === paragraph.ChildObjects.Count - 1) {
paragraph.ChildObjects.RemoveAt(index);
replacementIndex = paragraphIndex + 1;
} else {
let paragraph1 = paragraph.Clone();
while (paragraph.ChildObjects.Count > index) {
paragraph.ChildObjects.RemoveAt(index);
}
let i = 0;
let count = index + 1;
while (i < count) {
paragraph1.ChildObjects.RemoveAt(0);
i += 1;
}
sectionBody.ChildObjects.Insert(paragraphIndex + 1, paragraph1);
replacementIndex = paragraphIndex + 1;
}
for (let i = 0; i <= replacement.length - 1; i++) {
sectionBody.ChildObjects.Insert(replacementIndex + i, replacement[i].Clone());
}
}
function TextRangeLocation(TextRange) {
this.Text = TextRange;
this.Owner = this.Text.OwnerParagraph;
this.Index = this.Owner.ChildObjects.IndexOf(this.Text);
this.CompareTo = function (other) {
return -(this.Index - other.Index);
};
}
const ReplaceWithHTML = async () => {
// 获取 Spire.Doc WASM 模块
const docModule = window.wasmModule?.spiredoc;
// 检查模块是否就绪
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the ARIALUNI.TTF font file into the virtual file system (VFS)
await window.spire.FetchFileToVFS('msyh.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 将 HTML 文件和 Word 文档载入 VFS
let HTMLName = 'InputHtml1.txt';
await window.spire.FetchFileToVFS(HTMLName, '', `${process.env.PUBLIC_URL}/data/`);
const HTML = window.dotnetRuntime.Module.FS.readFile(HTMLName);
let inputFileName = 'ReplaceWithHtml.docx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);
// 加载文档
let doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// 创建临时 Section 并渲染 HTML
let replacement = [];
let tempSection = doc.AddSection();
let par = tempSection.AddParagraph();
const decoder = new TextDecoder('utf-8');
const HTMLString = decoder.decode(HTML);
par.AppendHTML(HTMLString);
// 收集渲染后的文档对象
for (let i = 0; i < tempSection.Body.ChildObjects.Count; i++) {
let docObj = tempSection.Body.ChildObjects.get_Item(i);
replacement.push(docObj);
}
// 查找所有占位符并排序
let selections = doc.FindAllString('[#placeholder]', false, true);
let locations = [];
for (let selection of selections) {
locations.push(new TextRangeLocation(selection.GetAsOneRange()));
}
locations.sort();
// 逐个替换
for (let location of locations) {
ReplacedWithHTML(location, replacement);
}
// 移除临时 Section
doc.Sections.Remove(tempSection);
// 定义输出文件名并保存
const outputFileName = 'ReplaceWithHtml_output.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 文档中的占位符替换为 HTML</h1>
<button onClick={ReplaceWithHTML}>
生成
</button>
</div>
);
}
export default App;
文档中的 [#placeholder] 占位符被 HTML 渲染后的富文本内容替换
![文档中的 [#placeholder] 占位符被 HTML 渲染后的富文本内容替换](https://cdn.e-iceblue.cn/images/tutorials-images/replace-placeholder-with-html-or-paragraph-zh-1.webp)
占位符替换为另一个文档的段落
将占位符替换为另一个文档的段落的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和两个 Word 文档载入 WASM 虚拟文件系统;然后分别加载主文档和来源文档,调用 FindAllPattern 通过正则表达式查找占位符(如 [MY_DOCUMENT]),遍历来源文档的所有 Section 及 Paragraphs,通过 ChildObjects.Insert 将每个段落逐条插入到主文档的对应位置,最后移除原占位符文本;最后保存文档,从 VFS 读取生成的文件,封装为 Blob 后触发浏览器下载。
import React, { useState } from 'react';
function App() {
const ReplaceContentWithDoc = async () => {
// 获取 Spire.Doc WASM 模块
const docModule = window.wasmModule?.spiredoc;
// 检查模块是否就绪
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 将两个 Word 文档载入 VFS
let inputFileName1 = 'ReplaceContentWithDoc.docx';
await window.spire.FetchFileToVFS(inputFileName1, '', `${process.env.PUBLIC_URL}/data/`);
let inputFileName2 = 'Insert.docx';
await window.spire.FetchFileToVFS(inputFileName2, '', `${process.env.PUBLIC_URL}/data/`);
// 加载主文档
let document1 = new docModule.Document();
document1.LoadFromFile(inputFileName1);
// 加载来源文档(包含待插入的段落)
let document2 = new docModule.Document();
document2.LoadFromFile(inputFileName2);
// 获取主文档的第一个 Section
let section1 = document1.Sections.get_Item(0);
// 创建正则表达式查找占位符
let regex = new docModule.Regex('\\[MY_DOCUMENT\\]', docModule.RegexOptions.None);
// 查找所有匹配的占位符
let textSections = document1.FindAllPattern({ pattern: regex });
// 遍历每个匹配位置
for (let i = 0; i < textSections.length; i++) {
let selection = textSections[i];
let para = selection.GetAsOneRange().OwnerParagraph;
let textRange = selection.GetAsOneRange();
let index = section1.Body.ChildObjects.IndexOf(para);
// 将来源文档的所有段落插入到占位符位置
for (let i = 0; i < document2.Sections.Count; i++) {
let section2 = document2.Sections.get_Item(i);
for (let j = 0; j < section2.Paragraphs.Count; j++) {
let paragraph = section2.Paragraphs.get_Item(j);
section1.Body.ChildObjects.Insert(index++, paragraph.Clone());
}
}
// 移除原占位符文本
para.ChildObjects.Remove(textRange);
}
// 定义输出文件名并保存
const outputFileName = 'ReplaceContentWithDoc_output.docx';
document1.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
// 释放资源
document1.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={ReplaceContentWithDoc}>
生成
</button>
</div>
);
}
export default App;
主文档中的 [MY_DOCUMENT] 占位符被来源文档的全部段落替换
![主文档中的 [MY_DOCUMENT] 占位符被来源文档的全部段落替换](https://cdn.e-iceblue.cn/images/tutorials-images/replace-placeholder-with-html-or-paragraph-zh-2.webp)
常见问题
HTML 内容中的格式未正确显示
原因:AppendHTML 方法支持的 HTML 标签范围有限,仅能识别基础的块级和行内标签(如 <p>、<b>、<i>、<table> 等),复杂的 CSS 样式、JavaScript 代码或 HTML5 新标签会被忽略。
解决:确保输入的 HTML 仅使用基础标签,并通过内联样式(如 style="color:red")而非 CSS 类名来定义格式:
<p style="font-size:14pt; color:#2E75B6;">这是蓝色标题文本</p>
<ul><li>项目一</li><li>项目二</li></ul>
插入的段落顺序与预期不符
原因:多个占位符替换时,未对匹配位置进行排序处理,从前往后依次替换导致后续位置的索引发生偏移,使段落插入到了错误的位置。
解决:先对所有匹配位置按索引降序排序(从文档尾部向前替换),或记录每个位置的原始索引偏移量:
let locations = [];
for (let selection of selections) {
locations.push(new TextRangeLocation(selection.GetAsOneRange()));
}
locations.sort(); // 降序排列,从后往前替换
获取免费许可证
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。







