冰蓝科技
|
028-81705109
|
|
微信扫一扫
|

Spire.Cloud 纯前端文档控件

Spire.OfficeJS 11.6.5 现已发布。该版本升级了产品底层结构,并调整了发布包结构。详情如下。

优化:


获取Spire.OfficeJS 11.6.5,请点击:

https://www.e-iceblue.cn/Downloads/Spire-OfficeJS.html

Spire.Office for JavaScript 11.7.0 现已正式发布。该版本支持将 Word 直接保存为 XLSX 格式,以及操作格式修订信息。同时,还支持设置图表数据标签位置,并支持追加、修改或删除 SmartArt 图形。更多详情如下。

新功能:


获取 Spire.Office for JavaScript 11.7.0,请点击:

https://www.e-iceblue.cn/Downloads/Spire-Office-Javascript.html

将文档中的占位符替换为 HTML 内容或另一个文档的段落,是文档自动化中非常实用的需求——例如将富文本编辑器中编排好的 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 渲染后的富文本内容替换


占位符替换为另一个文档的段落

将占位符替换为另一个文档的段落的核心流程分为三个阶段:首先通过 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] 占位符被来源文档的全部段落替换


常见问题

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 天的临时许可证。

将文档中的特定占位文本替换为图片或表格,是实际开发中频率极高的需求——例如将合同尾部「(签章处)」替换为公司印章图片、将数据汇总占位符替换为统计表格等。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接完成此类替换操作,通过虚拟文件系统(VFS)管理字体、图片及文档文件,无需后端服务支持。

本文介绍两个核心功能点:

有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript。以下示例默认已安装 Spire.Doc 并完成 WebAssembly 模块初始化。


文本替换为图片

将文本替换为图片的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件、目标图片和 Word 文档载入 WASM 虚拟文件系统;然后调用 FindAllString 查找所有匹配的目标文本,遍历每个匹配位置,通过 DocPicture 加载图片并用 ChildObjects.Insert 插入图片,再用 ChildObjects.Remove 移除原文本;最后保存文档,从 VFS 读取生成的文件,封装为 Blob 后触发浏览器下载。

import React, { useState } from 'react';

function App() {
  const ReplaceWithImage = async () => {
    // 获取 Spire.Doc WASM 模块
    const docModule = window.wasmModule?.spiredoc;

    // 检查模块是否就绪
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // 将图片和 Word 文档载入 VFS
    let pngName = 'E-iceblue.png';
    await window.spire.FetchFileToVFS(pngName, '', `${process.env.PUBLIC_URL}/data/`);

    let inputFileName = 'Template.docx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

    // 加载文档
    const doc = new docModule.Document();
    doc.LoadFromFile(inputFileName);

    // 查找所有匹配的文本
    let selections = doc.FindAllString('E-iceblue', true, true);

    // 遍历每个匹配位置,将文本替换为图片
    for (let i = 0; i < selections.length; i++) {
      // 创建 DocPicture 对象并加载图片
      let pic = new docModule.DocPicture(doc);
      pic.LoadImage(pngName);
      let selection = selections[i];
      // 获取当前文本范围
      let range = selection.GetAsOneRange();
      // 获取 TextRange 在其所属段落 ChildObjects 中的索引
      let index = range.OwnerParagraph.ChildObjects.IndexOf(range);
      // 在 TextRange 位置插入图片
      range.OwnerParagraph.ChildObjects.Insert(index, pic);
      // 移除原 TextRange
      range.OwnerParagraph.ChildObjects.Remove(range);
    }

    // 定义输出文件名并保存
    const outputFileName = 'ReplaceWithImage_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文档中用图片替换文本</h1>
      <button onClick={ReplaceWithImage}>
        生成
      </button>
    </div>
  );
}

export default App;

文档中的目标文本被查找并替换为指定图片后的效果

文档中的目标文本被查找并替换为指定图片后的效果


文本替换为表格

将文本替换为表格的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和目标 Word 文档载入 WASM 虚拟文件系统;然后调用 FindString 查找指定文本,通过 GetAsOneRange 获取文本范围,再通过 OwnerTextBody.ChildObjects 获取段落索引,创建新表格后用 ChildObjects.Remove 移除原段落,并用 ChildObjects.Insert 在相同位置插入表格;最后保存文档,从 VFS 读取生成的文件,封装为 Blob 后触发浏览器下载。

import React, { useState } from 'react';

function App() {
  const ReplaceTextWithTable = async () => {
    // 获取 Spire.Doc WASM 模块
    const docModule = window.wasmModule?.spiredoc;

    // 检查模块是否就绪
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // 将字体文件载入 VFS
    await window.spire.FetchFileToVFS('msyh.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 将 Word 文档载入 VFS
    let inputFileName = 'Template_Docx_1.docx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

    // 加载文档
    const doc = new docModule.Document();
    doc.LoadFromFile(inputFileName);

    // 获取第一个章节
    let section = doc.Sections.get_Item(0);

    // 查找目标文本
    let selection = doc.FindString('Christmas Day, December 25', true, true);

    // 获取文本范围及所属段落
    let range = selection.GetAsOneRange();
    let paragraph = range.OwnerParagraph;

    // 获取段落所属的文本体,计算段落索引
    let body = paragraph.OwnerTextBody;
    let index = body.ChildObjects.IndexOf(paragraph);

    // 创建 3×3 表格
    let table = section.AddTable(true);
    table.ResetCells(3, 3);

    // 移除原段落,在相同位置插入表格
    body.ChildObjects.Remove(paragraph);
    body.ChildObjects.Insert(index, table);

    // 定义输出文件名并保存
    const outputFileName = 'ReplaceTextWithTable_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文档中将文本替换为表格</h1>
      <button onClick={ReplaceTextWithTable}>
        生成
      </button>
    </div>
  );
}

export default App;

文档中的目标段落被移除后,在相同位置插入指定规格的表格

文档中的目标段落被移除后,在相同位置插入指定规格的表格


常见问题

插入图片后文档中图片不显示

原因:图片文件未加载到 WASM 虚拟文件系统,或 FetchFileToVFS 加载路径不正确,导致 DocPicture.LoadImage 无法从 VFS 中读取图片数据。

解决:确保在调用 LoadImage 之前,已通过 FetchFileToVFS 将图片文件正确载入 VFS,并检查文件名称与路径是否一致:

await window.spire.FetchFileToVFS(
  'E-iceblue.png', '', `${process.env.PUBLIC_URL}/data/`
);

表格插入位置与预期不符

原因:通过 OwnerTextBody.ChildObjects.IndexOf 获取的段落索引不准确,或选中的文本跨多个段落导致索引偏移,使表格插入到了错误的位置。

解决:先确认 FindString 查找的文本位于单个段落内,然后通过 GetAsOneRange 获取准确的 OwnerParagraph,再获取其在 OwnerTextBody.ChildObjects 中的索引:

let range = selection.GetAsOneRange();
let paragraph = range.OwnerParagraph;
let body = paragraph.OwnerTextBody;
let index = body.ChildObjects.IndexOf(paragraph);

获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。

文本替换是 Word 文档处理中最常见的需求之一——批量替换合同中的占位符、统一修正术语、或者对特定格式的文本进行规范化处理,都可以通过查找替换功能高效完成。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接处理 Word 文档,通过虚拟文件系统(VFS)管理字体和文件资源,无需后端服务支持。

本文介绍两个核心功能点:

有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript 的方法。以下示例默认已安装 Spire.Doc 并完成 WebAssembly 模块初始化。


替换指定文本

替换指定文本是最基本的文本操作——将文档中的某个词或短语统一替换为另一个字符串,支持大小写敏感和全词匹配选项。核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和目标 Word 文档载入 WASM 虚拟文件系统;然后实例化 Document 加载文件,调用 Replace 方法匹配指定字符串并替换为新文本,可通过 caseSensitive 和 wholeWord 参数控制匹配规则;最后保存文档,从 VFS 读取生成的文件,封装为 Blob 后触发浏览器下载。

import React, { useState } from 'react';

function App() {

  const ReplaceWithText = async () => {
      // 获取 Spire.Doc WASM 模块
      const docModule = window.wasmModule?.spiredoc;

       // 检查模块是否就绪
       if (!docModule) {
        alert('Spire.Doc is not ready yet');
        return;
       }
        // 将示例文件载入 VFS
        let inputFileName = 'Sample.docx';
        await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

        // 创建文档实例
        const doc = new docModule.Document();

        // 从虚拟文件系统加载文档
        doc.LoadFromFile(inputFileName);

        // 替换文本:"word" → "ReplacedText",不区分大小写,全词匹配
        doc.Replace({ matchString: 'word', newValue: 'ReplacedText', caseSensitive: false, wholeWord: true });

        // 定义输出文件名并保存
        const outputFileName = 'ReplaceWithText_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 文档中的文本。</h1>
      <button onClick={ReplaceWithText}>
        Generate
      </button>
    </div>
    );
  };

export default App;

通过 Replace 方法替换指定文本后,文档中的目标字符串将被统一替换为新内容

通过 Replace 方法替换指定文本后的效果


使用正则表达式替换文本

当需要匹配不固定格式的文本时,正则表达式是最强大的工具——例如将所有以 # 开头的标签文本统一替换为固定内容。核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和目标 Word 文档载入 WASM 虚拟文件系统;然后创建 Regex 对象定义匹配模式,调用 Replace 方法将匹配到的文本统一替换为指定内容;最后保存文档,从 VFS 读取生成的文件,封装为 Blob 后触发浏览器下载。

import React, { useState } from 'react';

function App() {

  const ReplaceWithText = async () => {
      // 获取 Spire.Doc WASM 模块
      const docModule = window.wasmModule?.spiredoc;

       // 检查模块是否就绪
       if (!docModule) {
        alert('Spire.Doc is not ready yet');
        return;
       }
       // 将示例文件载入 VFS
        let inputFileName = 'Sample.docx';
        await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}static/data/`);

        // 创建文档实例
        const doc = new docModule.Document();

        // 从虚拟文件系统加载文档
        doc.LoadFromFile(inputFileName);

        // 创建正则表达式,匹配以 # 开头的单词
        let regex = new docModule.Regex('\\bword\\b', docModule.RegexOptions.None);

        // 使用正则表达式替换文本
        doc.Replace(regex, 'Spire.Doc');

        // 定义输出文件名并保存
        const outputFileName = 'ReplaceTextByRegex_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 文档中的文本。</h1>
      <button onClick={ReplaceWithText}>
        Generate
      </button>
    </div>
    );
  };

export default App;

通过正则表达式匹配并替换文本后,所有符合模式的字符串将被统一替换

通过正则表达式匹配并替换文本后的效果


常见问题

替换操作未生效(文本未被替换)

原因:caseSensitive 或 wholeWord 参数设置不正确导致匹配失败,实际文本与匹配条件不符。

解决:检查大小写是否匹配,或将 caseSensitive 设为 false 以忽略大小写,将 wholeWord 设为 false 以匹配部分单词:

doc.Replace({ matchString: 'word', newValue: 'ReplacedText', caseSensitive: false, wholeWord: false });

正则表达式匹配不到目标内容

原因:正则表达式模式中特殊字符(如 #、\)未正确转义,导致匹配失败。

解决:确保正则表达式中的特殊字符已正确转义。例如匹配 #tag 格式的文本时,# 和 \b 需要按字符串转义规则处理:

let regex = new wasmModule.Regex('#\\S+', wasmModule.RegexOptions.None);

获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。

将文档中的特定文本替换为域代码或另一个文档的内容,是文档自动化中非常实用的需求——例如将模板中的「当前日期」占位符替换为日期域(DATE Field),使其在每次打开文档时自动更新为系统日期;或将合同模板中的「条款内容」占位符替换为另一个 Word 文档中的详细条款内容,实现文档的模块化组装。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接完成此类替换操作,通过虚拟文件系统(VFS)管理字体及文档文件,无需后端服务支持。

本文介绍两个核心功能点:

有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript。以下示例默认已安装 Spire.Doc 并完成 WebAssembly 模块初始化。


文本替换为域

将文本替换为域的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和目标 Word 文档载入 WASM 虚拟文件系统;然后调用 FindString 查找目标文本,获取其所在段落及索引位置,创建 Field 对象并指定域类型(如 FieldDate),依次在段落中插入 Field、FieldMark(FieldSeparator)、FieldMark(FieldEnd) 以构建完整的域结构,最后移除原文本;最后保存文档,从 VFS 读取生成的文件,封装为 Blob 后触发浏览器下载。

import React, { useState } from 'react';

function App() {
  const ReplaceTextWithField = async () => {
    // 获取 Spire.Doc WASM 模块
    const docModule = window.wasmModule?.spiredoc;

    // 检查模块是否就绪
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // 将 Word 文档载入 VFS
    let inputFileName = 'ReplaceTextWithField.docx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

    // 加载文档
    const doc = new docModule.Document();
    doc.LoadFromFile(inputFileName);

    // 查找目标文本
    let selection = doc.FindString({
      stringValue: 'summary',
      caseSensitive: false,
      wholeWord: true,
    });

    // 获取文本范围及其所在段落
    let textRange = selection.GetAsOneRange();
    let ownParagraph = textRange.OwnerParagraph;
    let rangeIndex = ownParagraph.ChildObjects.IndexOf(textRange);

    // 创建日期域(DATE Field)
    let eqField = new docModule.Field(doc);
    eqField.Type = docModule.FieldType.FieldDate;
    eqField.Code = 'DATE \\@ "yyyyMMdd "';

    // 依次插入 Field、FieldSeparator、FieldEnd
    ownParagraph.ChildObjects.Insert(rangeIndex, eqField);

    let mark = new docModule.FieldMark(doc, docModule.FieldMarkType.FieldSeparator);
    ownParagraph.ChildObjects.Insert(rangeIndex + 1, mark);

    let end = new docModule.FieldMark(doc, docModule.FieldMarkType.FieldEnd);
    ownParagraph.ChildObjects.Insert(rangeIndex + 2, end);

    // 移除原文本
    ownParagraph.ChildObjects.Remove(textRange);

    // 定义输出文件名并保存
    const outputFileName = 'ReplaceTextWithField_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 文档中将文本替换为域</h1>
      <button onClick={ReplaceTextWithField}>
        生成
      </button>
    </div>
  );
}

export default App;

文档中的目标文本被替换为日期域后,打开文档时会自动显示当前系统日期

文档中的目标文本被替换为日期域后,打开文档时会自动显示当前系统日期


文本替换为另一个文档

将文本替换为另一个文档的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和两个 Word 文档(主文档和待替换内容文档)载入 WASM 虚拟文件系统;然后分别实例化两个 Document 对象加载两个文档,在主文档上调用 Replace 方法并传入 matchDoc 参数(指向另一个文档对象),将匹配的文本替换为该文档的全部内容;最后保存文档,从 VFS 读取生成的文件,封装为 Blob 后触发浏览器下载。

import React, { useState } from 'react';

function App() {
  const ReplaceWithDocument = async () => {
    // 获取 Spire.Doc WASM 模块
    const docModule = window.wasmModule?.spiredoc;

    // 检查模块是否就绪
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // 将两个 Word 文档载入 VFS
    let inputFileName1 = 'Text2.docx';
    await window.spire.FetchFileToVFS(inputFileName1, '', `${process.env.PUBLIC_URL}/data/`);

    let inputFileName2 = 'Text1.docx';
    await window.spire.FetchFileToVFS(inputFileName2, '', `${process.env.PUBLIC_URL}/data/`);

    // 加载主文档
    let doc = new docModule.Document();
    doc.LoadFromFile(inputFileName1);

    // 加载另一个文档(替换内容)
    let replaceDoc = new docModule.Document();
    replaceDoc.LoadFromFile(inputFileName2);

    // 用另一个文档的内容替换指定文本
    doc.Replace({
      matchString: 'Document1',
      matchDoc: replaceDoc,
      caseSensitive: false,
      wholeWord: true,
    });

    // 定义输出文件名并保存
    const outputFileName = 'ReplaceWithDocument_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 文档中将文本替换为另一个文档</h1>
      <button onClick={ReplaceWithDocument}>
        生成
      </button>
    </div>
  );
}

export default App;

主文档中的占位文本被另一个文档的全部内容替换后的效果

主文档中的占位文本被另一个文档的全部内容替换后的效果


常见问题

插入域后文档中不显示域值

原因:Field、FieldMark(FieldSeparator)、FieldMark(FieldEnd) 三者没有按照正确的顺序插入,破坏了域结构的完整性,导致 Word 无法正确识别和计算域值。

解决:确保按顺序依次插入 Field、FieldSeparator、FieldEnd,且插入索引连续递增:

ownParagraph.ChildObjects.Insert(rangeIndex, eqField);
ownParagraph.ChildObjects.Insert(rangeIndex + 1, mark);
ownParagraph.ChildObjects.Insert(rangeIndex + 2, end);

另一个文档内容未正确替换

原因:Replace 方法中 matchDoc 参数传入的文档对象未正确加载到 VFS 中,或文件路径/名称与 FetchFileToVFS 载入时不一致,导致 LoadFromFile 无法找到目标文件。

解决:确认两个文档均已通过 FetchFileToVFS 载入 VFS,并在调用 Replace 之前分别创建 Document 对象并成功加载:

let doc = new docModule.Document();
doc.LoadFromFile('Text2.docx');

let replaceDoc = new docModule.Document();
replaceDoc.LoadFromFile('Text1.docx');

doc.Replace({
  matchString: 'Document1',
  matchDoc: replaceDoc,
  caseSensitive: false,
  wholeWord: true,
});

获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。

使用 Spire.XLS 在 Java 中将 JSON 数据转换为 Excel

JSON 广泛应用于 REST API、Web 服务和企业应用中的数据交换。然而,业务人员更习惯使用 Excel 进行报表展示、数据筛选和分析。因此,开发者在导出 API 响应、生成报表或向非技术人员共享结构化数据时,经常需要在 Java 中将 JSON 转换为 Excel。

Java 生态中有多种 JSON 处理库,但要将数据转换为结构良好的 Excel 文件,还需要处理列头、单元格类型、行遍历和输出格式等问题——如果没有合适的工具,这些工作会非常繁琐。Spire.XLS for Java 提供了简洁的 API,无需依赖 Microsoft Office 即可创建 Excel 工作簿,大幅简化了这一过程。

本文将介绍如何使用 Spire.XLS for Java 和 Jackson 在 Java 中将 JSON 转换为 Excel,涵盖 JSON 数组转换、嵌套 JSON 处理、JSON 文件读取、XLSX 和 XLS 导出、自适应列宽、格式化以及大数据集处理的最佳实践。

快速导航

  1. 为什么需要在 Java 中将 JSON 转换为 Excel
  2. 安装 Spire.XLS for Java
  3. 准备 JSON 数据
  4. 在 Java 中逐步将 JSON 转换为 Excel
  5. 完整的 Java 代码:将 JSON 转换为 Excel
  6. 在 Java 中将 JSON 导出为 XLSX
  7. 在 Java 中将嵌套 JSON 转换为 Excel
  8. 将 JSON 文件转换为 Excel
  9. 在 Excel 中自适应行高和列宽
  10. 为导出的 Excel 文件应用格式
  11. JSON 转 Excel 的常见挑战
  12. 为什么选择 Spire.XLS for Java
  13. 总结
  14. 常见问题

1. 为什么需要在 Java 中将 JSON 转换为 Excel

JSON 因其轻量且易于机器解析的特点,成为 REST API、Web 服务和企业应用中数据交换的主流格式。然而,业务人员通常需要 Excel 文件来进行报表展示、数据筛选、可视化和进一步分析。

在 Java 中将 JSON 转换为 Excel,能够有效打通后端系统与业务流程之间的壁垒。典型应用场景包括:

导出 API 数据

许多 REST API 返回 JSON 格式的响应。将这些响应转换为 Excel,用户无需手动处理原始 JSON,即可直接查看、筛选和分析数据。

生成报表

Java 应用可以将来自 API、数据库或其他数据源的 JSON 数据转换为结构化的 Excel 报表,包含表头、格式化和规范的表格布局。

共享结构化数据

Excel 文件便于分发,且支持图表、公式和数据透视表等分析工具。将 JSON 数据导出为 Excel,非技术用户也能直接使用这些功能。


2. 安装 Spire.XLS for Java

在开始转换之前,需要在项目中配置以下依赖。

Maven 依赖

Spire.XLS for Java 已通过 e-iceblue Maven 仓库发布。在 pom.xml 中添加仓库和依赖:

<repositories>
    <repository>
        <id>com.e-iceblue</id>
        <name>e-iceblue</name>
        <url>https://repo.e-iceblue.cn/repository/maven-public/</url>
    </repository>
</repositories>

<dependency>
    <groupId>e-iceblue</groupId>
    <artifactId>spire.xls</artifactId>
    <version>16.6.5</version>
</dependency>

也可以下载 Spire.XLS for Java,手动将 JAR 文件添加到项目中。

添加 JSON 处理库

Java 没有内置的 JSON 支持。本指南使用 Jackson,这是 Java 生态中最广泛采用的 JSON 处理库:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.17.2</version>
</dependency>

导入必要的类

在 Java 源文件中添加以下导入语句:

import com.spire.xls.*;
import com.spire.xls.core.spreadsheet.collections.AutoFitType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;

3. 准备 JSON 数据

为了演示转换过程,我们使用一个简单的 JSON 数组,其中每个对象代表一行数据,每个属性代表一列。这是 REST API 响应和数据导出中最常见的 JSON 结构。

示例:简单 JSON 数组

[
  {
    "ID": 1,
    "Name": "陈明辉",
    "Department": "销售部",
    "Salary": 75000,
    "HireDate": "2022-03-15"
  },
  {
    "ID": 2,
    "Name": "林雨婷",
    "Department": "市场部",
    "Salary": 68000,
    "HireDate": "2021-07-01"
  },
  {
    "ID": 3,
    "Name": "赵文博",
    "Department": "技术部",
    "Salary": 92000,
    "HireDate": "2023-01-10"
  }
]

JSON 与 Excel 之间的映射关系非常直观:

  • 每个 JSON 对象对应电子表格中的一行
  • 每个属性名对应一个列头
  • 每个属性值对应相应行和列中的单元格值

理解这一映射关系,有助于理解后续章节中的代码示例。


4. 在 Java 中逐步将 JSON 转换为 Excel

转换过程分为五个步骤:创建工作簿、获取工作表、解析 JSON 数据、写入列头和填充单元格值。本节逐一讲解每个步骤,最后给出完整代码。

第 1 步:创建工作簿

Workbook 类代表一个 Excel 文件。实例化该类即可创建一个新的空工作簿:

Workbook workbook = new Workbook();

第 2 步:获取工作表

一个工作簿包含一个或多个工作表。获取默认创建的第一个工作表,并可选择为其重命名:

Worksheet sheet = workbook.getWorksheets().get(0);
sheet.setName("员工数据");

第 3 步:读取 JSON 数据

使用 Jackson 的 ObjectMapper 将 JSON 字符串解析为 JsonNode 树。如果根元素是 JSON 数组,将其转换为 ArrayNode 以便遍历:

ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.readTree(jsonString);

if (!rootNode.isArray()) {
    throw new IllegalArgumentException("Expected a JSON array at the root level");
}
ArrayNode jsonArray = (ArrayNode) rootNode;

第 4 步:将 JSON 键名写入列头

从第一个 JSON 对象中提取字段名,写入工作表的第一行。注意 Spire.XLS 的行和列索引从 1 开始:

JsonNode firstObject = jsonArray.get(0);
int col = 1;
for (Iterator<Map.Entry<String, JsonNode>> it = firstObject.fields(); it.hasNext(); ) {
    Map.Entry<String, JsonNode> entry = it.next();
    sheet.get(1, col).setValue(entry.getKey());
    col++;
}

第 5 步:将 JSON 值写入 Excel 单元格

遍历数组中的每个 JSON 对象,将其值写入对应的行。由于第 1 行已是列头,数据从第 2 行开始写入:

for (int i = 0; i < jsonArray.size(); i++) {
    JsonNode record = jsonArray.get(i);
    int dataRow = i + 2;
    int dataCol = 1;
    for (Iterator<Map.Entry<String, JsonNode>> it = record.fields(); it.hasNext(); ) {
        Map.Entry<String, JsonNode> entry = it.next();
        JsonNode value = entry.getValue();
        if (value.isNumber()) {
            sheet.get(dataRow, dataCol).setNumberValue(value.doubleValue());
        } else if (value.isBoolean()) {
            sheet.get(dataRow, dataCol).setBooleanValue(value.booleanValue());
        } else {
            sheet.get(dataRow, dataCol).setValue(value.asText());
        }
        dataCol++;
    }
}

这种方式保留了数据类型——数值和布尔值以对应的类型写入单元格,而非作为字符串存储,从而确保生成的 Excel 文件中数值排序、筛选和公式计算能够正常工作。


5. 完整的 Java 代码:将 JSON 转换为 Excel

以下是完整的可运行程序,读取 JSON 字符串并将其转换为 Excel 文件,展示了在 Java 中将 JSON 转换为 Excel 的完整流程:

import com.spire.xls.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import java.io.File;
import java.util.Iterator;
import java.util.Map;

public class JsonToExcelConverter {

    public static void main(String[] args) {

        // 示例 JSON 数据——员工记录数组
        String jsonString = "["
            + "{\"ID\":1,\"Name\":\"陈明辉\",\"Department\":\"销售部\",\"Salary\":75000,\"HireDate\":\"2022-03-15\"},"
            + "{\"ID\":2,\"Name\":\"林雨婷\",\"Department\":\"市场部\",\"Salary\":68000,\"HireDate\":\"2021-07-01\"},"
            + "{\"ID\":3,\"Name\":\"赵文博\",\"Department\":\"技术部\",\"Salary\":92000,\"HireDate\":\"2023-01-10\"}"
            + "]";

        try {
            // 将 JSON 字符串解析为 JsonNode 树结构
            ObjectMapper mapper = new ObjectMapper();
            JsonNode rootNode = mapper.readTree(jsonString);

            if (!rootNode.isArray()) {
                throw new IllegalArgumentException("Expected a JSON array at the root level");
            }
            ArrayNode jsonArray = (ArrayNode) rootNode;

            // 创建新工作簿并获取第一个工作表
            Workbook workbook = new Workbook();
            Worksheet sheet = workbook.getWorksheets().get(0);
            sheet.setName("员工数据");

            // 从第一个 JSON 对象的键名提取列头
            JsonNode firstObject = jsonArray.get(0);
            int col = 1;
            for (Iterator<Map.Entry<String, JsonNode>> it = firstObject.fields(); it.hasNext(); ) {
                Map.Entry<String, JsonNode> entry = it.next();
                sheet.get(1, col).setValue(entry.getKey());
                col++;
            }

            // 将 JSON 值逐行写入 Excel
            for (int i = 0; i < jsonArray.size(); i++) {
                JsonNode record = jsonArray.get(i);
                int dataRow = i + 2;
                int dataCol = 1;

                for (Iterator<Map.Entry<String, JsonNode>> it = record.fields(); it.hasNext(); ) {
                    Map.Entry<String, JsonNode> entry = it.next();
                    JsonNode value = entry.getValue();

                    // 保留数据类型:数值和布尔值以对应类型写入单元格
                    if (value.isNumber()) {
                        sheet.get(dataRow, dataCol).setNumberValue(value.doubleValue());
                    } else if (value.isBoolean()) {
                        sheet.get(dataRow, dataCol).setBooleanValue(value.booleanValue());
                    } else {
                        sheet.get(dataRow, dataCol).setValue(value.asText());
                    }
                    dataCol++;
                }
            }

            // 自适应列宽,提升可读性
            sheet.getAllocatedRange().autoFitColumns();

            // 将工作簿保存为 XLSX 文件
            workbook.saveToFile("员工数据.xlsx", ExcelVersion.Version2016);
            System.out.println("JSON 已成功转换为 Excel 文件。");

            // 释放资源
            workbook.dispose();

        } catch (Exception e) {
            System.err.println("JSON 转 Excel 过程中发生错误:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

运行程序后,JSON 数据将被转换为 Excel 工作表。生成的 员工数据.xlsx 文件包含员工记录,数据类型得以保留,列宽已自动调整:

在 Java 中将 JSON 数据转换为 Excel 电子表格

Spire.XLS 核心类和方法

  • Workbook — 代表一个 Excel 文件,负责创建工作簿、管理工作表和保存文件。
  • Worksheet — 代表工作簿中的单个工作表,提供对单元格、行和列的访问。
  • get(int row, int column) — 返回指定单元格的 CellRange 对象。行和列索引从 1 开始。
  • setValue(String) — 将单元格值设置为字符串,用于文本和表头。
  • setNumberValue(double) — 将单元格值设置为数值,保留数值类型以支持计算。
  • setBooleanValue(boolean) — 将单元格值设置为布尔值(TRUE/FALSE)。
  • saveToFile(String, ExcelVersion) — 将工作簿按指定 Excel 格式保存到文件。
  • dispose() — 释放工作簿占用的非托管资源。

如果还需要将 Excel 文件转换回 JSON 格式,请参阅我们关于如何在 Java 中将 Excel 转换为 JSON的指南。


6. 在 Java 中将 JSON 导出为 XLSX

Spire.XLS for Java 同时支持现代的 XLSX 格式(Excel 2007 及以上版本)和传统的 XLS 格式(Excel 97–2003)。通过向 saveToFile() 传递不同的 ExcelVersion 枚举值,即可控制输出格式。

保存为 XLSX

// 导出为现代 Excel 格式(.xlsx)
workbook.saveToFile("员工数据.xlsx", ExcelVersion.Version2016);

保存为 XLS

// 导出为传统 Excel 格式(.xls)
workbook.saveToFile("员工数据.xls", ExcelVersion.Version97to2003);
格式 说明 适用场景
XLSX 现代 Excel 格式(Excel 2007+) 默认选择;文件更小,功能更完整
XLS 传统 Excel 格式(Excel 97–2003) 需要兼容旧版系统时使用

同一个工作簿对象可以保存为上述任意格式,只需修改文件扩展名和版本参数即可,无需其他代码改动。这在应用需要同时支持新旧环境时尤为实用。

如需了解格式迁移或旧版升级的场景,还可以参考如何在 Java 中转换 XLS 和 XLSX 格式。


7. 在 Java 中将嵌套 JSON 转换为 Excel

实际项目中的 JSON 数据通常包含嵌套的对象和数组。要将嵌套 JSON 写入 Excel,需要将层级结构扁平化为表格格式,每个嵌套字段作为独立的列。

以下 JSON 包含带有嵌套联系方式的员工记录:

[
  {
    "ID": 1,
    "Name": "陈明辉",
    "Department": "销售部",
    "Contact": {
      "Email": "chenminghui@ company.com",
      "Phone": "555-0101"
    }
  },
  {
    "ID": 2,
    "Name": "林雨婷",
    "Department": "市场部",
    "Contact": {
      "Email": "linyuting@ company.com",
      "Phone": "555-0102"
    }
  }
]

目标是将 Contact 对象扁平化,使 Email 和 Phone 成为独立的列:

ID Name Department Contact.Email Contact.Phone
1 陈明辉 销售部 该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。 555-0101
2 林雨婷 市场部 该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。 555-0102

以下代码使用递归扁平化方法,可处理任意深度的嵌套:

import com.spire.xls.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

public class NestedJsonToExcel {

    public static void main(String[] args) {

        String jsonString = "["
            + "{\"ID\":1,\"Name\":\"陈明辉\",\"Department\":\"销售部\","
            + "\"Contact\":{\"Email\":\"chenminghui@ company.com\",\"Phone\":\"555-0101\"}},"
            + "{\"ID\":2,\"Name\":\"林雨婷\",\"Department\":\"市场部\","
            + "\"Contact\":{\"Email\":\"linyuting@ company.com\",\"Phone\":\"555-0102\"}}"
            + "]";

        try {
            ObjectMapper mapper = new ObjectMapper();
            ArrayNode jsonArray = (ArrayNode) mapper.readTree(jsonString);

            Workbook workbook = new Workbook();
            Worksheet sheet = workbook.getWorksheets().get(0);
            sheet.setName("员工信息");

            // 扁平化第一个对象,提取所有列头(包括嵌套键)
            LinkedHashMap<String, String> firstFlat = flattenJson(jsonArray.get(0), "");
            int col = 1;
            for (String key : firstFlat.keySet()) {
                sheet.get(1, col).setValue(key);
                col++;
            }

            // 写入数据行
            for (int i = 0; i < jsonArray.size(); i++) {
                LinkedHashMap<String, String> flat = flattenJson(jsonArray.get(i), "");
                int dataRow = i + 2;
                int dataCol = 1;
                for (String key : firstFlat.keySet()) {
                    String value = flat.getOrDefault(key, "");
                    sheet.get(dataRow, dataCol).setValue(value);
                    dataCol++;
                }
            }

            sheet.getAllocatedRange().autoFitColumns();
            workbook.saveToFile("NestedEmployees.xlsx", ExcelVersion.Version2016);
            System.out.println("嵌套 JSON 已成功转换为 Excel 文件。");
            workbook.dispose();

        } catch (Exception e) {
            System.err.println("错误:" + e.getMessage());
        }
    }

    /**
     * 递归扁平化 JSON 对象为键值对。
     * 嵌套键以点号连接(例如 "Contact.Email")。
     */
    private static LinkedHashMap<String, String> flattenJson(JsonNode node, String prefix) {
        LinkedHashMap<String, String> flat = new LinkedHashMap<>();
        if (node.isObject()) {
            for (Iterator<Map.Entry<String, JsonNode>> it = node.fields(); it.hasNext(); ) {
                Map.Entry<String, JsonNode> entry = it.next();
                String newPrefix = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
                flat.putAll(flattenJson(entry.getValue(), newPrefix));
            }
        } else {
            flat.put(prefix, node.asText());
        }
        return flat;
    }
}

flattenJson 方法递归遍历每个 JSON 对象。遇到嵌套对象时,以点号作为分隔符拼接父键名(如 Contact.Email);到达叶子节点时,将完整的点分隔键及其值存入 Map。这确保了任意嵌套深度的字段都能在 Excel 中表示为独立的列。

在 Java 中将嵌套 JSON 转换为扁平 Excel 表格


8. 将 JSON 文件转换为 Excel

在实际应用中,JSON 数据通常来自磁盘文件,而非内联字符串。转换步骤保持不变,只是 JSON 的数据源不同。Jackson 的 ObjectMapper 可以直接从 File 对象读取:

import com.spire.xls.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import java.io.File;
import java.util.Iterator;
import java.util.Map;

public class JsonFileToExcel {

    public static void main(String[] args) {

        try {
            // 第 1 步:读取并解析 JSON 文件
            ObjectMapper mapper = new ObjectMapper();
            JsonNode rootNode = mapper.readTree(new File("employees.json"));

            if (!rootNode.isArray()) {
                throw new IllegalArgumentException("Expected a JSON array at the root level");
            }
            ArrayNode jsonArray = (ArrayNode) rootNode;

            // 第 2 步:创建工作簿
            Workbook workbook = new Workbook();
            Worksheet sheet = workbook.getWorksheets().get(0);
            sheet.setName("员工信息");

            // 第 3 步:从第一个对象提取列头
            JsonNode firstObject = jsonArray.get(0);
            int col = 1;
            for (Iterator<Map.Entry<String, JsonNode>> it = firstObject.fields(); it.hasNext(); ) {
                Map.Entry<String, JsonNode> entry = it.next();
                sheet.get(1, col).setValue(entry.getKey());
                col++;
            }

            // 第 4 步:写入数据行
            for (int i = 0; i < jsonArray.size(); i++) {
                JsonNode record = jsonArray.get(i);
                int dataRow = i + 2;
                int dataCol = 1;
                for (Iterator<Map.Entry<String, JsonNode>> it = record.fields(); it.hasNext(); ) {
                    Map.Entry<String, JsonNode> entry = it.next();
                    JsonNode value = entry.getValue();
                    if (value.isNumber()) {
                        sheet.get(dataRow, dataCol).setNumberValue(value.doubleValue());
                    } else if (value.isBoolean()) {
                        sheet.get(dataRow, dataCol).setBooleanValue(value.booleanValue());
                    } else {
                        sheet.get(dataRow, dataCol).setValue(value.asText());
                    }
                    dataCol++;
                }
            }

            // 第 5 步:导出为 Excel
            sheet.getAllocatedRange().autoFitColumns();
            workbook.saveToFile("EmployeesFromJson.xlsx", ExcelVersion.Version2016);
            System.out.println("JSON 文件已成功转换为 Excel。");
            workbook.dispose();

        } catch (Exception e) {
            System.err.println("读取 JSON 文件时出错:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

这种方式能够高效处理大型 JSON 文件,因为 Jackson 以流式树模型处理文件。对于超大 JSON 文件(数百 MB),建议使用 Jackson 的 JsonParser 流式模式逐条读取记录,避免将整个文件一次性加载到内存中。


9. 在 Excel 中自适应行高和列宽

将 JSON 数据写入 Excel 单元格后,默认列宽可能不足以显示所有内容,文本值(如邮箱地址、URL 或较长的描述)会被截断。Spire.XLS 提供了自适应方法,可根据内容自动调整列宽和行高:

// 自适应已用区域的所有列宽和行高
sheet.getAllocatedRange().autoFitColumns();
sheet.getAllocatedRange().autoFitRows();

在写入所有数据之后、保存工作簿之前添加以上代码。getAllocatedRange() 方法返回包含数据的单元格区域,因此只有已填充的单元格会受到影响。

如需更精细的控制,可以对单独列执行自适应:

// 自适应特定列(例如第 3 列)
sheet.getAllocatedRange().getColumns()[2].autoFitColumns();

自适应处理能让电子表格更加专业和易读,尤其在 JSON 数据包含长度不一的文本字段时效果明显。下图对比了原始导出与应用自适应后的效果差异。


10. 为导出的 Excel 文件应用格式

原始数据导出往往需要进一步格式化,才能满足企业报表的要求。Spire.XLS for Java 提供了丰富的单元格格式化 API,支持设置表头样式、数字格式和日期格式——全部通过代码完成。

格式化表头行

为第一行应用加粗字体和背景色,使表头与数据区分开来:

import com.spire.xls.core.spreadsheet.styles.CellStyle;
import java.awt.Color;

// 为表头行应用格式
CellRange headerRange = sheet.getAllocatedRange().getRows()[0];
headerRange.getStyle().setFont(new ExcelFont(true));
headerRange.getStyle().setColor(Color.decode("#4472C4"));
headerRange.getStyle().getFont().setColor(Color.WHITE);
headerRange.setStyle(headerRange.getStyle());

格式化数字

为数值列应用货币或百分比格式:

// 将 Salary 列(第 4 列)格式化为货币格式
CellRange salaryColumn = sheet.getAllocatedRange().getColumns()[3];
salaryColumn.setNumberFormat("$#,##0.00");

格式化日期

如果 JSON 中包含日期字符串,可以对相应列设置统一的日期显示格式:

// 将 HireDate 列(第 5 列)格式化为日期格式
CellRange dateColumn = sheet.getAllocatedRange().getColumns()[4];
dateColumn.setNumberFormat("yyyy-mm-dd");

上述格式化方法可以组合使用,生成专业的 Excel 报表。如需了解更全面的 Excel 格式化功能,请参考如何使用 Spire.XLS 在 Java 中创建和格式化 Excel 文件。


11. JSON 转 Excel 的常见挑战

实际项目中的 JSON 数据往往不如教程示例那样理想。以下是开发者在转换过程中最常遇到的问题及对应的解决方案。

对象间字段不一致

同一数组中的不同 JSON 对象可能包含不同的字段。某条记录可能包含 Phone 字段,而另一条则完全没有。如果代码假设所有对象具有相同的键,缺失的字段会导致 Excel 输出中的列错位。

解决方案: 先收集所有对象中的全部唯一键,再按统一的键列表写入每条记录的值:

// 收集所有 JSON 对象中的唯一键
LinkedHashSet<String> allKeys = new LinkedHashSet<>();
for (JsonNode record : jsonArray) {
    record.fieldNames().forEachRemaining(allKeys::add);
}

// 按完整的键集合写入列头
int col = 1;
for (String key : allKeys) {
    sheet.get(1, col).setValue(key);
    col++;
}

// 写入值,缺失字段以空字符串填充
for (int i = 0; i < jsonArray.size(); i++) {
    JsonNode record = jsonArray.get(i);
    int dataRow = i + 2;
    int dataCol = 1;
    for (String key : allKeys) {
        JsonNode value = record.get(key);
        String cellValue = (value != null && !value.isNull()) ? value.asText() : "";
        sheet.get(dataRow, dataCol).setValue(cellValue);
        dataCol++;
    }
}

嵌套对象

JSON 对象可以包含任意深度的嵌套。如果直接将嵌套对象写入单元格,输出结果将是不可读的 [object Object] 或序列化后的 JSON 字符串。

解决方案: 使用第 7 节中演示的递归扁平化方法。flattenJson 方法遍历整个对象树,生成扁平的键值对,嵌套键以点号分隔。

大型 JSON 文件

将超大 JSON 文件(数百 MB 或更大)解析为单一的内存树结构可能导致 Java 的 OutOfMemoryError。此外,逐单元格写入数万行数据也会很慢。

解决方案: 使用 Jackson 的流式 API(JsonParser)逐条读取 JSON 记录,每读取一条立即写入 Excel。这样无论文件多大,内存占用都保持恒定:

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;

JsonFactory factory = new JsonFactory();
try (JsonParser parser = factory.createParser(new File("large_data.json"))) {
    int dataRow = 2;
    while (parser.nextToken() != JsonToken.END_ARRAY) {
        // 逐条解析对象
        JsonNode record = mapper.readTree(parser);
        // 写入 Excel...
        dataRow++;
    }
}

数据类型转换

JSON 支持字符串、数值、布尔值、null、数组和对象。Excel 单元格支持文本、数值、布尔值、日期和错误值。类型不匹配——例如将数值存储为字符串——会导致 Excel 的排序和公式功能无法正常工作。

解决方案: 在写入单元格前检查每个 JSON 值的类型。数值使用 setNumberValue(),布尔值使用 setBooleanValue(),文本使用 setValue()。对于 null 值,写入空字符串或占位符。对于日期字符串,将其解析为 Date 对象后使用 setDateTimeValue() 写入为 Excel 日期单元格:

if (value == null || value.isNull()) {
    sheet.get(dataRow, dataCol).setValue("");
} else if (value.isNumber()) {
    sheet.get(dataRow, dataCol).setNumberValue(value.doubleValue());
} else if (value.isBoolean()) {
    sheet.get(dataRow, dataCol).setBooleanValue(value.booleanValue());
} else {
    sheet.get(dataRow, dataCol).setValue(value.asText());
}

12. 为什么选择 Spire.XLS for Java 进行 JSON 转 Excel

以下特性使 Spire.XLS for Java 在企业 Java 应用的 JSON 转 Excel 场景中表现出色。

无需安装 Microsoft Excel

Spire.XLS for Java 是一个独立的库,不依赖 Microsoft Office 或任何外部软件。它可以在任何安装了 Java 运行环境的系统上运行,包括 Linux 服务器、Docker 容器和未安装 Office 的云平台。

同时支持 XLS 和 XLSX

该库同时支持传统的 XLS 格式(Excel 97–2003)和现代的 XLSX 格式(Excel 2007+)。只需修改一个参数即可导出为任意格式,轻松适配不同的下游环境。

丰富的格式化功能

除了基本的单元格值写入,Spire.XLS 还提供全面的格式化能力——单元格样式、数字格式、字体、颜色、边框、条件格式、图表和数据透视表。可以直接从 JSON 数据生成专业级的 Excel 文件,无需在 Excel 中进行后处理。

简洁的 API

API 遵循直观的对象模型:Workbook 包含 Worksheets,每个 Worksheet 包含 CellRanges,每个 CellRange 支持值设置、样式和格式化。熟悉 Excel 对象模型的开发者可以快速上手。

适用于企业级应用

Spire.XLS for Java 专为服务端和企业场景设计。它能高效处理大文件,支持多线程访问模式,并与 Spring Boot、Jakarta EE 等企业级 Java 框架无缝集成。

可以申请 30 天免费许可证来在项目中评估所有功能。


13. 总结

本文详细介绍了如何使用 Spire.XLS for Java 和 Jackson 在 Java 中将 JSON 转换为 Excel。通过解析 JSON 数据、将值写入 Excel 工作表,并将工作簿导出为 XLSX 或 XLS 文件,开发者可以高效地将结构化 JSON 数据转换为易读的电子表格。

Spire.XLS for Java 提供了一种简洁灵活的方式来从 JSON 数据生成 Excel 文件,无需依赖 Microsoft Office 或其他外部依赖。同时支持格式化、自适应列宽以及处理复杂数据结构等高级功能,满足专业 Excel 报表的生成需求。


14. 常见问题

如何在 Java 中将 JSON 转换为 Excel?

使用 Jackson 的 ObjectMapper 解析 JSON 数据,通过 Spire.XLS for Java 创建 Workbook 和 Worksheet,将 JSON 键名写入第一行作为列头,然后遍历 JSON 数组填充每一行数据。最后使用 saveToFile() 配合指定的 ExcelVersion 保存工作簿。完整代码示例见第 5 节。

在没有安装 Microsoft Excel 的情况下,能在 Java 中将 JSON 转换为 XLSX 吗?

可以。Spire.XLS for Java 是一个独立的库,不需要 Microsoft Office 或其他软件。它能完全在 Java 中创建、读取和写入 XLSX 文件,适用于运行在 Linux、Docker 或云平台上的服务端应用。

转换嵌套 JSON 对象到 Excel 时如何处理?

使用递归扁平化函数遍历 JSON 对象树,生成扁平的键值对。嵌套键以点号分隔(如 Contact.Email),扁平化后的键作为 Excel 中的列头。完整实现见第 7 节。

Spire.XLS 中 setValue() 和 setNumberValue() 有什么区别?

setValue(String) 将字符串值写入单元格,而 setNumberValue(double) 将数值写入单元格,Excel 会将其作为数字处理。对数值类型的 JSON 字段使用 setNumberValue() 可以确保排序、筛选和公式计算正常工作。类似地,setBooleanValue(boolean) 写入的是类型化的布尔值。

如何在不内存溢出的情况下将大型 JSON 文件转换为 Excel?

对于大型 JSON 文件,使用 Jackson 的流式 API(JsonParser)逐条读取和处理 JSON 记录,而非将整个文件加载到内存中。每解析一条记录就立即写入 Excel 工作表,这样无论文件多大,内存占用都保持恒定。

Spire.XLS for Java 是免费的吗?

Spire.XLS for Java 是商业库。同时提供 Free Spire.XLS for Java 免费版本,但在工作表数量和功能上有限制。也可以申请 30 天免费许可证来评估完整功能后再决定是否购买。

在实际文档排版中,文本框是承载独立内容的理想容器——它可以自由定位、不受页面流限制,非常适合用于侧边栏、引述框、产品示意图等场景。而往文本框中插入图片或表格,则是实现丰富布局的常见需求。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接处理 Word 文档,通过虚拟文件系统(VFS)管理字体和文件资源,无需后端服务支持。

本文介绍两个核心功能点:

有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript 的方法。以下示例默认已安装 Spire.Doc 并完成 WebAssembly 模块初始化。


向文本框插入图片

将图片填入文本框是制作产品标签、名片或宣传物料时的常用技巧。Spire.Doc 通过 AppendTextBox 方法创建文本框,然后利用 FillEfects.SetPicture 用图片填充文本框背景。核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和图片文件载入 WASM 虚拟文件系统;然后创建文档,添加文本框并设置位置属性,通过填充效果将图片应用到文本框中;最后保存文档并从 VFS 读取生成的文件,封装为 Blob 后触发浏览器下载。

function App() {
  const InsertImageIntoTextBox = async () => {
    // 获取 Spire.Doc WASM 模块
    const docModule = window.wasmModule?.spiredoc;

    // 检查模块是否就绪
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // 将字体和图片文件载入 VFS
    const imageFileName = 'Spire.Doc.png';
    await window.spire.FetchFileToVFS(imageFileName, '', `${process.env.PUBLIC_URL}/data/`);

    // 创建文档
    const doc = new docModule.Document();

    // 添加一个节
    let section = doc.AddSection();

    // 添加一个段落
    let paragraph = section.AddParagraph();

    // 在段落中追加一个 220x220 的文本框
    let tb = paragraph.AppendTextBox(220, 220);

    // 设置文本框位置
    tb.Format.HorizontalOrigin = docModule.HorizontalOrigin.Page;
    tb.Format.HorizontalPosition = 50;
    tb.Format.VerticalOrigin = docModule.VerticalOrigin.Page;
    tb.Format.VerticalPosition = 50;

    // 将文本框的填充类型设为图片
    tb.Format.FillEfects.Type = docModule.BackgroundType.Picture;

    // 用图片填充文本框
    tb.Format.FillEfects.SetPicture(imageFileName);

    // 定义输出文件名并保存
    const outputFileName = "InsertImageIntoTextBox_output.docx";
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    // 释放资源
    doc.Close();
    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={InsertImageIntoTextBox}>
        Generate
      </button>
    </div>
  );
}

export default App;

用图片填充文本框后,图片将按文本框尺寸自动适配显示。

向文本框插入图片后的效果


向文本框插入表格

在侧边栏或引用区域中嵌入结构化表格数据,是技术文档和报表中常见的需求。Spire.Doc 支持在文本框内创建表格,并通过 textbox.Body.AddTable 方法直接将表格添加到文本框主体中。相比在主文档中创建表格,文本框内的表格可以独立定位,不受页面布局影响。核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件载入 WASM 虚拟文件系统;然后创建文档和文本框,通过 textbox.Body.AddTable 添加表格并指定行列数,逐行填充数据并应用样式;最后保存文档并从 VFS 读取生成的文件,封装为 Blob 后触发浏览器下载。

function App() {
  const InsertTableIntoTextBox = async () => {
    // 获取 Spire.Doc WASM 模块
    const docModule = window.wasmModule?.spiredoc;

    // 检查模块是否就绪
    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 paragraph = section.AddParagraph();

    // 添加一个 300x100 的文本框
    let textbox = paragraph.AppendTextBox(300, 150);

    // 设置文本框位置
    textbox.Format.HorizontalOrigin = docModule.HorizontalOrigin.Page;
    textbox.Format.HorizontalPosition = 140;
    textbox.Format.VerticalOrigin = docModule.VerticalOrigin.Page;
    textbox.Format.VerticalPosition = 50;

    // 在文本框中添加标题文本
    let textboxParagraph = textbox.Body.AddParagraph();
    let textboxRange = textboxParagraph.AppendText("表格 1");
    textboxRange.CharacterFormat.FontName = "微软雅黑";

    // 在文本框中插入表格
    let table = textbox.Body.AddTable({ showBorder: true });

    // 指定表格的行列数
    table.ResetCells(4, 4);

    let data = [
      ["姓名", "年龄", "性别", "编号"],
      ["张三", "28", "男", "0023"],
      ["李四", "30", "男", "0024"],
      ["王五", "26", "女", "0025"]
    ];
    // 向表格填充数据
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 4; j++) {
        let tableRange = table.Rows.get_Item(i).Cells.get_Item(j).AddParagraph().AppendText(data[i][j]);
        tableRange.CharacterFormat.FontName = "微软雅黑";
      }
    }

    // 应用表格样式
    table.ApplyStyle({ builtinTableStyle: docModule.DefaultTableStyle.TableColorful2 });

    // 定义输出文件名并保存
    const outputFileName = "InsertTableIntoTextBox_output.docx";
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    // 释放资源
    doc.Close();
    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={InsertTableIntoTextBox}>
        Generate
      </button>
    </div>
  );
}

export default App;

在文本框中创建表格后,表格将包含在文本框内部,可随文本框自由定位。

向文本框插入表格后的效果


常见问题

文本框中的图片显示不完整或被裁剪

原因:文本框尺寸与图片宽高比不匹配,创建文本框时指定的尺寸无法完整容纳图片内容。

解决:调整文本框尺寸以匹配图片比例,或选择适合文本框尺寸的图片:

let tb = paragraph.AppendTextBox(400, 300);

文本框中的表格没有显示边框

原因:创建表格时 showBorder 参数未设置或设为 false,导致表格无边框。

解决:创建表格时确认 showBorder 参数为 true:

let table = textbox.Body.AddTable({ showBorder: true });

保存后文本框位置与预期不符

原因:HorizontalOrigin 或 VerticalOrigin 设置不正确,导致定位基准点与预期不同。

解决:根据需求选择合适的原点类型,并结合 HorizontalPosition / VerticalPosition 微调位置:

tb.Format.HorizontalOrigin = docModule.HorizontalOrigin.Page;
tb.Format.HorizontalPosition = 50;
tb.Format.VerticalOrigin = docModule.VerticalOrigin.Page;
tb.Format.VerticalPosition = 50;

获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。

在实际文档流转中,水印是标识文档状态(如"机密""草稿")或保护版权的常用手段。无论是给内部文档打上"保密"字样,还是将公司 Logo 以水印形式嵌入合同,亦或是清理他人文档中的现有水印,都是高频操作。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接处理 Word 文档,通过虚拟文件系统(VFS)管理字体和文件资源,无需后端服务支持。

本文介绍三个核心功能点:

有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript 的方法。以下示例默认已安装 Spire.Doc 并完成 WebAssembly 模块初始化。


添加文字水印

为文档添加文字水印是最常见的需求之一。例如,在内部文档上标注"机密",或在审阅版本中标明"草稿"。Spire.Doc 通过 TextWatermark 对象,可设置水印文字、字号、颜色和布局方向(水平/对角线)。核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和目标 Word 文件载入 WASM 虚拟文件系统;然后实例化 Document 加载文件,创建 TextWatermark 对象并设置属性,将其赋值给文档的 Watermark 属性;最后保存文档并从 VFS 读取生成的文件,封装为 Blob 后触发浏览器下载。

function App() {
  const AddTextWatermark = async () => {
    // 获取 Spire.Doc WASM 模块
    const wasmModule = window.wasmModule?.spiredoc;

    // 检查模块是否就绪
    if (!wasmModule) {
      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 = 'Template.docx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

    // 加载文档
    const doc = new wasmModule.Document();
    doc.LoadFromFile(inputFileName);

    // 创建文字水印对象
    let txtWatermark = new wasmModule.TextWatermark();
    txtWatermark.Text = "机密";
    txtWatermark.FontSize = 95;
    txtWatermark.Color = wasmModule.Color.get_Red();
    txtWatermark.Layout = wasmModule.WatermarkLayout.Diagonal;

    // 将水印应用到文档
    doc.Watermark = txtWatermark;

    // 定义输出文件名并保存
    const outputFileName = "TextWatermark_output.docx";
    doc.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Docx2013 });

    // 释放资源
    doc.Close();
    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={AddTextWatermark}>
        Generate
      </button>
    </div>
  );
}

export default App;

添加文字水印后,文档背景中会显示指定的文字内容。

添加文字水印后的文档效果


添加图片水印

除了文字水印,许多企业也偏好使用图片水印——将公司 Logo 或品牌图形以半透明方式铺满文档背景,既提升文档专业度,又防止内容被随意复制传播。Spire.Doc 通过 PictureWatermark 对象实现图片水印的添加,支持设置缩放比例和冲刷效果(IsWashout)。相比文字水印,图片水印需要额外将图片文件载入 VFS。

function App() {
  const AddImageWatermark = async () => {
    // 获取 Spire.Doc WASM 模块
    const wasmModule = window.wasmModule?.spiredoc;

    // 检查模块是否就绪
    if (!wasmModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    //图片和 Word 文件载入 VFS
    const inputFileName = 'Template.docx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);
    const imageFileName = 'Logo.png';
    await window.spire.FetchFileToVFS(imageFileName, '', `${process.env.PUBLIC_URL}/data/`);

    // 加载文档
    const doc = new wasmModule.Document();
    doc.LoadFromFile(inputFileName);

    // 创建图片水印对象
    let picture = new wasmModule.PictureWatermark();
    picture.SetPicture(imageFileName);
    picture.Scaling = 250;
    picture.IsWashout = false;

    // 将水印应用到文档
    doc.Watermark = picture;

    // 定义输出文件名并保存
    const outputFileName = "ImageWatermark_output.docx";
    doc.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Docx2013 });

    // 释放资源
    doc.Close();
    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={AddImageWatermark}>
        Generate
      </button>
    </div>
  );
}

export default App;

添加图片水印后,文档背景会显示指定的图片内容。

添加图片水印后的文档效果


移除水印

接手他人文档时,常需先清除原有水印再应用企业模板。无论是文字水印还是图片水印,Spire.Doc 均通过将文档的 Watermark 属性设为 null 一键移除,无需区分水印类型。核心流程:载入文档 → 将 Watermark 置空 → 保存输出。

function App() {
  const RemoveWatermark = async () => {
    // 获取 Spire.Doc WASM 模块
    const wasmModule = window.wasmModule?.spiredoc;

    // 检查模块是否就绪
    if (!wasmModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // 将Word 文件载入 VFS
    const inputFileName = 'Watermarked.docx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

    // 加载文档
    const doc = new wasmModule.Document();
    doc.LoadFromFile(inputFileName);

    // 将水印设为 null,移除文字或图片水印
    doc.Watermark = null;

    // 定义输出文件名并保存
    const outputFileName = "RemoveWatermark_output.docx";
    doc.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Docx2013 });

    // 释放资源
    doc.Close();
    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={RemoveWatermark}>
        Generate
      </button>
    </div>
  );
}

export default App;

水印移除后,文档恢复为无干扰的纯内容状态。

水印移除后的文档效果


常见问题

添加文字水印后文档中未显示

原因:水印对象已创建但未赋值给 document.Watermark 属性,或水印颜色与文档背景色相同导致不可见。

解决:确保创建后执行赋值操作,并选择与背景有对比的颜色:

document.Watermark = txtWatermark;

图片水印显示为全黑或全白

原因:图片文件未正确载入 VFS,或 SetPicture 传入的文件名与 VFS 中的路径不一致。

解决:确认图片已通过 FetchFileToVFS 载入,且文件名完全匹配:

await window.spire.FetchFileToVFS('Logo.png', '', `${process.env.PUBLIC_URL}/data/`);
picture.SetPicture('Logo.png');

移除水印后文档变空白

原因:错误地创建了空文档而非加载原始文档,导致水印移除后保存的是空文档。

解决:确认使用 LoadFromFile 加载了正确的输入文档:

document.LoadFromFile(inputFileName);

获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。

将 Excel 文件按工作表、按行或按列拆分为多个独立文件,是数据分发与管理的常见需求。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成拆分操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

本文介绍三个核心功能点:

有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。


按工作表拆分 Excel 为独立文件

按工作表拆分是将多工作表工作簿中的每个工作表导出为独立的 Excel 文件。当一个工作簿中包含多个工作表,且每个表代表不同的数据,如不同部门或月份时,我们可以将每个工作表拆分并保存为独立的 Excel 文件。Spire.XLS 通过遍历源文件中的所有工作表,创建新的工作簿,并拷贝当前工作表来完成这个任务。下方为具体的操作步骤:

  1. 创建 Workbook 对象并使用 LoadFromFile() 加载 Excel 源文档。
  2. 遍历源文档中的所有工作表。
  3. 创建新的 Workbook 对象。
  4. 通过 CopyFrom 方法将源工作表复制到新工作簿的默认工作表中。
  5. 通过 sheet.Name 获取工作表的名称,作为输出文件名。
  6. 通过 SaveToFile() 方法保存新的工作簿为 Excel 文件。

下面是一个完整的代码示例,展示了将工作表拆分为多个独立的 Excel 文件:

function App() {
  const splitByWorksheet = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将字体和 Excel 文件载入 VFS
    await window.spire.FetchFileToVFS('arial.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'Sample.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 加载工作簿
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });

    // 遍历每个工作表,将其导出为独立文件
    for (let i = 0; i < workbook.Worksheets.Count; i++) {
      let sheet = workbook.Worksheets.get(i);

      // 创建新工作簿并复制当前工作表
      let newWorkbook = new xlsModule.Workbook();
      let newSheet = newWorkbook.Worksheets.get(0);
      newSheet.CopyFrom(sheet);

      // 使用工作表名称作为输出文件名
      const outputFileName = `${sheet.Name}.xlsx`;
      newWorkbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
      newWorkbook.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);
    }

    // 释放资源
    workbook.Dispose();
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Split Excel By Worksheet</h1>
      <button onClick={splitByWorksheet}>
        Generate
      </button>
    </div>
  );
}

export default App;

按工作表拆分后,每个独立文件包含原工作簿中的一个工作表

按工作表拆分后,每个独立文件包含原工作簿中的一个工作表


按行拆分 Excel 文件

按行拆分适用于将大型表格按固定行数分割为多个小文件,便于分页查看或分发。当一个工作表中包含大量数据行,需要拆分为多个文件时,Spire.XLS 通过逐行复制源行到新工作簿来完成这个任务。下方为具体的操作步骤:

  1. 创建 Workbook 对象并使用 LoadFromFile() 加载 Excel 源文档,获取第一个工作表。
  2. 创建新的 Workbook 对象。
  3. 通过循环逐行调用 Copy 方法,将源工作表的指定行复制到新工作表。
  4. 复制源工作表的列宽到新工作表。
  5. 通过 SaveToFile() 方法保存新的工作簿为 Excel 文件。
  6. 重复上述步骤创建更多拆分文件,并在必要时单独复制表头行。

下面是一个完整的代码示例,展示了按行将工作表拆分为多个独立的 Excel 文件:

function App() {
  const splitByRow = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将字体和 Excel 文件载入 VFS
    await window.spire.FetchFileToVFS('arial.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'Sample.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);

    // 创建新的工作簿(默认自带一个工作表)
    let newWorkbook1 = new xlsModule.Workbook();
    let newSheet1 = newWorkbook1.Worksheets.get(0);

    // 将第 1-5 行复制到目标文件
    let destRow = 1;
    for (let i = 0; i < 5; i++) {
      sheet.Copy({
        sourceRange: sheet.Rows[i],
        worksheet: newSheet1,
        destRow: destRow,
        destColumn: 1,
        copyStyle: true
      });
      destRow++;
    }

    // 复制列宽
    for (let c = 0; c < sheet.Columns.length; c++) {
      newSheet1.SetColumnWidth(c + 1, sheet.GetColumnWidth(c + 1));
    }

    // 保存第一个拆分文件
    const outputFileName1 = "Rows1-5.xlsx";
    newWorkbook1.SaveToFile({ fileName: outputFileName1, version: xlsModule.ExcelVersion.Version2010 });
    newWorkbook1.Dispose();

    // 从 VFS 读取文件数据
    const fileData1 = window.dotnetRuntime.Module.FS.readFile(outputFileName1);

    // 创建第二个新工作簿
    let newWorkbook2 = new xlsModule.Workbook();
    let newSheet2 = newWorkbook2.Worksheets.get(0);

    destRow = 1;

    // 复制表头
    sheet.Copy({
      sourceRange: sheet.Rows[0],
      worksheet: newSheet2,
      destRow: destRow,
      destColumn: 1,
      copyStyle: true
    });
    destRow++;

    // 将第 6-10 行复制到第二个目标文件
    for (let i = 5; i < 10; i++) {
      sheet.Copy({
        sourceRange: sheet.Rows[i],
        worksheet: newSheet2,
        destRow: destRow,
        destColumn: 1,
        copyStyle: true
      });
      destRow++;
    }

    // 复制列宽
    for (let c = 0; c < sheet.Columns.length; c++) {
      newSheet2.SetColumnWidth(c + 1, sheet.GetColumnWidth(c + 1));
    }

    // 保存第二个拆分文件
    const outputFileName2 = "Rows6-10.xlsx";
    newWorkbook2.SaveToFile({ fileName: outputFileName2, version: xlsModule.ExcelVersion.Version2010 });
    newWorkbook2.Dispose();

    // 从 VFS 读取文件数据
    const fileData2 = window.dotnetRuntime.Module.FS.readFile(outputFileName2);

    // 将拆分后的文件打包为 ZIP 下载
    const zip = new JSZip();
    zip.file(outputFileName1, fileData1);
    zip.file(outputFileName2, fileData2);
    const zipBlob = await zip.generateAsync({ type: 'blob' });
    const zipUrl = URL.createObjectURL(zipBlob);
    const a = document.createElement('a');
    a.href = zipUrl;
    a.download = "SplitByRows.zip";
    a.click();
    URL.revokeObjectURL(zipUrl);

    // 释放资源
    workbook.Dispose();
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Split Excel By Row</h1>
      <button onClick={splitByRow}>
        Generate
      </button>
    </div>
  );
}

export default App;

按行拆分后,每个文件包含表头行和指定数量的数据行

按行拆分后,每个文件包含表头行和指定数量的数据行


按列拆分 Excel 文件

按列拆分适用于将宽表格按列分组拆分为多个文件,使数据结构更清晰。当一个工作表包含大量列,需要将不同列组拆分为独立文件时,Spire.XLS 通过逐列复制源列到新工作簿来完成这个任务。下方为具体的操作步骤:

  1. 创建 Workbook 对象并使用 LoadFromFile() 加载 Excel 源文档,获取第一个工作表。
  2. 创建新的 Workbook 对象。
  3. 通过循环逐列调用 Copy 方法,将源工作表的指定列复制到新工作表。
  4. 复制源工作表的列宽到新工作表。
  5. 通过 SaveToFile() 方法保存新的工作簿为 Excel 文件。
  6. 重复上述步骤创建更多拆分文件。

下面是一个完整的代码示例,展示了按列将工作表拆分为多个独立的 Excel 文件:

function App() {
  const splitByColumn = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将字体和 Excel 文件载入 VFS
    await window.spire.FetchFileToVFS('arial.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'Sample.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);

    // 创建新的工作簿,将第 1-2 列(AB 列)复制到新文件
    let newWorkbook1 = new xlsModule.Workbook();
    let newSheet1 = newWorkbook1.Worksheets.get(0);

    for (let i = 1; i <= 2; i++) {
      sheet.Copy({
        sourceRange: sheet.Columns[i - 1],
        worksheet: newSheet1,
        destRow: 1,
        destColumn: i,
        copyStyle: true
      });
    }

    // 复制列宽
    for (let i = 1; i <= 2; i++) {
      newSheet1.SetColumnWidth(i, sheet.GetColumnWidth(i));
    }

    // 保存第一个拆分文件
    const outputFileName1 = "ColumnsAB.xlsx";
    newWorkbook1.SaveToFile({ fileName: outputFileName1, version: xlsModule.ExcelVersion.Version2010 });
    newWorkbook1.Dispose();

    // 从 VFS 读取文件数据
    const fileData1 = window.dotnetRuntime.Module.FS.readFile(outputFileName1);

    // 创建第二个新工作簿,将第 3-4 列(CD 列)复制到新文件
    let newWorkbook2 = new xlsModule.Workbook();
    let newSheet2 = newWorkbook2.Worksheets.get(0);

    for (let i = 3; i <= 4; i++) {
      sheet.Copy({
        sourceRange: sheet.Columns[i - 1],
        worksheet: newSheet2,
        destRow: 1,
        destColumn: i - 2,
        copyStyle: true
      });
    }

    // 复制列宽
    for (let i = 3; i <= 4; i++) {
      newSheet2.SetColumnWidth(i - 2, sheet.GetColumnWidth(i));
    }

    // 保存第二个拆分文件
    const outputFileName2 = "ColumnsCD.xlsx";
    newWorkbook2.SaveToFile({ fileName: outputFileName2, version: xlsModule.ExcelVersion.Version2010 });
    newWorkbook2.Dispose();

    // 从 VFS 读取文件数据
    const fileData2 = window.dotnetRuntime.Module.FS.readFile(outputFileName2);

    // 将拆分后的文件打包为 ZIP 下载
    const zip = new JSZip();
    zip.file(outputFileName1, fileData1);
    zip.file(outputFileName2, fileData2);
    const zipBlob = await zip.generateAsync({ type: 'blob' });
    const zipUrl = URL.createObjectURL(zipBlob);
    const a = document.createElement('a');
    a.href = zipUrl;
    a.download = "SplitByColumns.zip";
    a.click();
    URL.revokeObjectURL(zipUrl);

    // 释放资源
    workbook.Dispose();
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Split Excel By Column</h1>
      <button onClick={splitByColumn}>
        Generate
      </button>
    </div>
  );
}

export default App;

按列拆分后,每个文件包含原工作表中的部分列数据

按列拆分后,每个文件包含原工作表中的部分列数据


常见问题

拆分后工作表名称显示为默认名(Sheet1)而非原名

原因:CopyFrom 方法仅复制工作表内容,不会保留原工作表名称。新工作簿的默认工作表保持默认名称不变。

解决:复制后通过 newSheet.Name = sheet.Name 手动设置工作表名称:

let newSheet = newWorkbook.Worksheets.get(0);
newSheet.CopyFrom(sheet);
newSheet.Name = sheet.Name;

VFS 文件加载失败或路径错误

原因:文件路径或 VFS 文件名不正确,或必需的字体文件未载入 VFS,导致工作簿加载失败。

解决:确认 FetchFileToVFS 的参数路径正确。字体文件路径应为 /Library/Fonts/,并确保字体文件名精确匹配(如 arial.ttf):

await window.spire.FetchFileToVFS('arial.ttf', '/Library/Fonts/', fontSourcePath);

获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。