三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Node.js动态生成Word文档:docxtemplater、officegen与adm-zip实战指南

Node.js动态生成Word文档:docxtemplater、officegen与adm-zip实战指南

1. 项目概述:为什么要在Node.js里折腾Word文档?

如果你做过Web后台开发,尤其是涉及报表导出、合同生成、证书打印这类业务,大概率会碰到一个需求:在服务器端动态生成Word文档。最早的时候,我们可能会用PHP或者Java,但现在,Node.js凭借其异步高并发的特性,在处理这类I/O密集型的文档生成任务时,优势越来越明显。想象一下,一个在线教育平台要批量生成几千份学员结业证书,或者一个OA系统需要根据表单数据动态填充合同条款,如果手动操作,那简直是灾难。而用代码自动化,不仅速度快,还能保证格式统一、零差错。

我最初接触这个需求时,也走了不少弯路。网上方案很多,有的让你用html-to-docx,把HTML转成Word,但样式控制是个玄学;有的推荐用docx库直接操作底层的XML结构,学习成本又太高。经过多个项目的实战,我逐渐把目光聚焦在了三个核心库上:docxtemplaterofficegenadm-zip。它们各有各的“脾气”,也各有各的擅长领域。docxtemplater就像一个智能的邮件合并工具,擅长基于模板做变量替换;officegen则更像一个编程式的文档构建器,让你用代码“画”出文档的每一部分;而adm-zip,则是处理那些打包在.docx文件背后的压缩文件所必需的“瑞士军刀”。

这篇文章,我就以一个过来人的身份,把这几年在Node.js里操作Word文档的经验、踩过的坑,以及如何根据场景选择合适的工具,系统地梳理一遍。无论你是需要快速生成一份带数据的报告,还是要构建一个复杂的、包含图表和动态内容的文档,相信都能在这里找到可落地的方案。

2. 核心工具选型与场景匹配

面对不同的Word文档处理需求,选对工具是成功的一半。盲目上手一个库,后期可能会在兼容性、性能或功能上遇到难以逾越的障碍。下面我结合具体场景,帮你分析这三个核心库该怎么选。

2.1 docxtemplater:模板驱动的变量替换之王

核心定位:当你有一个设计好的Word模板(.docx文件),只需要往里面填充动态数据(如姓名、日期、表格行)时,docxtemplater是你的首选。它的工作原理非常直观:你事先在Word里用特定的语法(如{name}{company})标记好占位符,然后在Node.js中加载模板,传入一个JSON数据对象,它就能精准地替换所有标记,生成新的文档。

最适合的场景

  • 批量生成格式固定的文档:如劳动合同、录取通知书、获奖证书、发票等。法务或行政人员设计好标准模板,开发人员只需绑定数据源。
  • 包含复杂条件判断和循环的文档:比如一份销售报告,需要根据业绩数据动态生成不同数量的表格行,或者根据客户等级显示不同的条款段落。docxtemplater支持类似{#users}{name}{/users}的循环语法和{^hasDiscount}折扣信息{/hasDiscount}的条件判断语法,直接在模板里声明逻辑。
  • 对原始模板格式有严格保留要求的场景:因为它只替换文本,不改变原有的样式、排版、页眉页脚、图片位置等,所以能最大程度保持设计原貌。

我踩过的坑与心得

注意:docxtemplater处理的是.docx文件,这是一个ZIP压缩包。如果你直接用fs.readFile读取并传入二进制Buffer,它内部会调用adm-zipjszip来解包。但有时,如果模板文件是在Mac的Pages或某些在线编辑器中保存的,可能会包含一些额外的元数据文件,导致解析失败。最稳妥的方式是,模板一定要用Microsoft Word或WPS Office桌面版保存为标准.docx格式。

一个简单的性能技巧:如果你需要生成成千上万份文档,不要为每一份都去重新读取和解析模板文件。正确的做法是,在服务启动时,将模板文件加载并编译成一个docxtemplater的实例对象,保存在内存中。当请求到来时,直接克隆这个实例并注入新的数据,这样可以节省大量的I/O和解析时间。

// 服务启动时,预加载并编译模板 const fs = require('fs'); const Docxtemplater = require('docxtemplater'); const PizZip = require('pizzip'); // docxtemplater@3 之后推荐使用pizzip const templateContent = fs.readFileSync('path/to/template.docx', 'binary'); const zip = new PizZip(templateContent); const precompiledDoc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true }); // 在请求处理函数中 function generateDocument(userData) { // 克隆预编译的实例(注意:docxtemplater实例状态可变,需要深拷贝或重新创建) // 更佳实践是,将预编译的`zip`对象缓存,每次new一个新的Docxtemplater实例 const zip = new PizZip(templateContent); // 从缓存的二进制内容创建新zip const doc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true }); doc.setData(userData); try { doc.render(); } catch (error) { // 处理渲染错误 console.error(error); } const buf = doc.getZip().generate({ type: 'nodebuffer' }); return buf; }

2.2 officegen:编程式文档构建的瑞士军刀

核心定位:当你需要从零开始,完全用代码“搭建”一个Word文档时,officegen提供了这种可能性。它不依赖于任何模板,你通过调用API来添加段落、设置样式、插入表格和图片。这给了开发者极大的灵活性,特别适合文档结构完全由业务逻辑动态决定的场景。

最适合的场景

  • 文档结构高度动态化:比如一个数据可视化报告,章节、图表、分析结论的数量和顺序都根据查询结果实时变化。
  • 需要集成图表或复杂格式:虽然docxtemplater也能通过插件插入图片,但officegen在编程式添加内容时,对位置和格式的控制更直观。
  • 生成非标准格式的文档:除了Word(.docx),officegen还支持生成PowerPoint(.pptx)和Excel(.xlsx)文件,如果你有多格式文档生成需求,用一个库统一处理会简化技术栈。

它的“脾气”你要知道officegen的API是底层的,这意味着你需要自己管理很多细节。比如,你要手动计算并设置图片的尺寸(通常以英制单位EMU),要精确地定义段落样式(字体、大小、颜色、对齐)。它的文档和社区支持相对docxtemplater弱一些,有些高级功能可能需要你阅读源码或自己摸索。

实操中的一个关键点:生成文档流。officegen生成的是一个Node.js流(Stream),你需要正确地管道(pipe)到文件流或HTTP响应中,并妥善处理finalize事件,确保所有内容都已写入。

const officegen = require('officegen'); const fs = require('fs'); // 创建一个新的docx文档对象 let docx = officegen('docx'); // 监听错误(非常重要!) docx.on('error', function(err) { console.log(err); }); // 添加一个段落 let pObj = docx.createP(); pObj.addText('Hello World', { font_face: 'Arial', font_size: 48 }); // 创建输出流 let out = fs.createWriteStream('output.docx'); // 将文档流管道到文件 docx.generate(out); // 文档生成完成后的回调 out.on('close', function() { console.log('文档已生成。'); });

2.3 adm-zip:不可或缺的底层文件操作工具

核心定位.docx文件本质上是一个ZIP压缩包,里面包含了document.xmlstyles.xml以及图片等资源。adm-zip是一个纯JavaScript的ZIP压缩/解压缩库。虽然docxtemplaterofficegen内部可能已经集成了类似的ZIP处理功能,但在一些高级或定制化场景下,你仍然需要直接操作这个ZIP包。

你会用到它的场景

  1. 手动注入或替换资源:比如,你想在生成的文档中嵌入特定的字体文件,或者替换模板中的背景图片。你需要用adm-zip打开.docx,找到对应路径(如word/media/image1.png)的文件进行替换。
  2. 批量处理文档中的元数据:有时需要清理或修改文档属性(docProps/core.xml)。
  3. 调试:当文档生成出现问题时,你可以用adm-zip解压生成的文件和原始模板,对比内部的XML文件,精准定位是哪个部分的渲染出了问题。
  4. docxtemplater的旧版本配合docxtemplaterv2版本依赖jszip,而adm-zip是另一个流行的选择,在某些环境下可能性能或兼容性更好。

使用示例:替换文档中的图片

const AdmZip = require('adm-zip'); const fs = require('fs'); // 读取一个已有的.docx文件 let zip = new AdmZip('template_with_image.docx'); // 假设我们要替换word/media目录下的image1.png let newImageBuffer = fs.readFileSync('new_logo.png'); zip.updateFile('word/media/image1.png', newImageBuffer); // 或者添加一个新图片 zip.addFile('word/media/image2.png', newImageBuffer); // 将修改后的zip包写回文件 zip.writeZip('modified.docx'); console.log('图片替换/添加完成。');

选择策略总结

  • 有固定模板,数据驱动-> 首选docxtemplater
  • 无模板,结构动态生成-> 选用officegen
  • 需要深入操作.docx文件内部结构-> 备好adm-zip
  • 复杂项目:很可能需要组合使用。例如,用docxtemplater生成主体内容,但其中某个复杂表格用officegen生成后再以图片或OLE对象形式嵌入(这需要更高级的操作),最后用adm-zip进行最终的资源整合。

3. 深入实战:从模板准备到完整生成流程

理论说再多,不如亲手做一遍。这一部分,我将带你走完一个完整的、基于docxtemplater的合同生成流程,这是最常见也最实用的场景。我会把每个步骤掰开揉碎,包括那些官方文档可能没细说的“坑”。

3.1 第一步:制作一个“健壮”的Word模板

很多人觉得这一步是设计师的事,其实不然。一个结构清晰、标记规范的模板,能省去后端开发无数调试时间。

  1. 使用真正的Microsoft Word或WPS:在网页版或Mac的文本编辑器中制作的模板,编码可能不一致。用桌面版Office软件创建并保存为.docx格式。

  2. 占位符语法docxtemplater默认使用花括号{}。在模板中,直接像普通文本一样输入{companyName}{user.address}。为了可读性,我建议使用“蛇形命名法”或“驼峰命名法”,并与后端数据对象的属性名严格对应。

  3. 处理段落和换行:如果你想在替换的文本中保留换行符,需要在代码中设置linebreaks: true选项,并在模板中,将占位符所在段落的行距设置为“单倍行距”或“固定值”,避免Word自动的段落格式干扰。

  4. 循环区块的标记:这是核心功能。假设你有一个用户列表要在表格中展示。

    • 在Word中,先插入一个一行多列的表格,第一行是表头。
    • 在第二行(数据行)的每个单元格里,写上对应的占位符,比如{name},{age}
    • 然后,选中整个第二行(点击表格左侧的边框外区域可以选中整行)。
    • 接着,打开Word的“插入”菜单 -> “文档部件” -> “域”(Field)。
    • 在域名列表中,选择“MergeField”,然后在“域属性”的“域名”中,输入你的循环开始标记,例如users
    • 确定后,你会发现选中的表格行被一个灰色的«users»框起来了。这就标记了一个循环区块的开始。
    • 在紧接着的下一行(你可以先插入一个新行),用同样的方法插入一个域,域名输入/users,作为循环结束标记。
    • 最终,«users»«/users»之间的表格行(即你最初设计的那一行数据行),在渲染时就会根据数据数组users的长度进行复制。
    • 重要docxtemplaterv3之后,更推荐使用{#users}{/users}这种语法,它更直观且不依赖Word域。你只需在模板的纯文本部分写入这些标签即可。但要注意,这些标签本身必须是独立的段落或表格单元格内容,不要和其他文字混在一起。
  5. 图片占位符:如果你想动态插入图片,占位符需要特殊格式,例如{%image}。然后在数据对象中,image属性需要是一个包含width,height,data(base64或Buffer)等信息的对象。通常配合docxtemplater-image-module等插件使用。

3.2 第二步:搭建Node.js环境与安装依赖

确保你的Node.js版本在12以上(推荐16 LTS或18 LTS)。新建一个项目目录,初始化并安装核心包。

mkdir node-word-generator cd node-word-generator npm init -y npm install docxtemplater pizzip fs-extra # 如果需要在模板中使用循环/条件,可能需要安装对应的模块,但现代版本已内置。 # 如果需要处理图片,安装图片模块 # npm install docxtemplater-image-module-free

这里解释一下:pizzip是处理ZIP压缩包的库,docxtemplater依赖它。fs-extrafs的增强版,提供了像readFilewriteFile的Promise版本和复制目录等便捷方法,让代码更简洁。

3.3 第三步:编写核心生成代码

我们来写一个完整的生成函数。假设我们有一个简单的劳动合同模板contract_template.docx,里面有{employeeName},{startDate},{salary}等占位符。

const Docxtemplater = require('docxtemplater'); const PizZip = require('pizzip'); const fs = require('fs').promises; // 使用Promise API const path = require('path'); async function generateContract(data) { // 1. 读取模板文件 const templatePath = path.resolve(__dirname, 'templates', 'contract_template.docx'); let templateContent; try { templateContent = await fs.readFile(templatePath); } catch (err) { throw new Error(`无法读取模板文件: ${err.message}`); } // 2. 加载模板到docxtemplater const zip = new PizZip(templateContent); const doc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true, // 如果使用图片插件,需要在这里配置 // modules: [new ImageModule({ ... })] }); // 3. 设置要替换的数据 // data 应该是一个对象,如 { employeeName: '张三', startDate: '2023-10-27', salary: 15000 } doc.setData(data); // 4. 渲染文档(执行替换) try { doc.render(); } catch (error) { // 渲染错误通常是因为模板语法错误或数据格式不对 console.error('文档渲染失败:'); console.error('错误信息:', error.message); console.error('错误位置:', error.properties); // error.properties包含了详细的错误上下文,如哪个标签出错 throw new Error(`文档渲染失败: ${error.message}`); } // 5. 获取生成的文档Buffer const outputBuffer = doc.getZip().generate({ type: 'nodebuffer', // compression: 'DEFLATE' // 压缩选项,一般默认即可 }); return outputBuffer; } // 使用示例 (async () => { const contractData = { employeeName: '李四', startDate: '2023年11月1日', salary: '18000', department: '技术研发部', // 假设有循环数据,对应模板中的 {#projects} ... {/projects} projects: [ { name: '项目A', role: '后端开发' }, { name: '项目B', role: '架构师' } ] }; try { const wordBuffer = await generateContract(contractData); // 保存到文件 await fs.writeFile('生成的合同_李四.docx', wordBuffer); console.log('合同生成成功!'); // 或者直接通过HTTP响应发送给前端 // res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); // res.setHeader('Content-Disposition', 'attachment; filename=contract.docx'); // res.send(wordBuffer); } catch (error) { console.error('生成过程出错:', error); } })();

代码关键点解析

  • paragraphLoop: true:这个选项至关重要。当你的数据替换后,如果段落变多或变少(比如循环生成了多行),开启此选项能确保Word的段落编号和引用(如“见上文第X段”)保持正确。对于大多数模板,建议都设为true
  • linebreaks: true:允许在替换的文本中使用\n作为换行符。如果你在数据中写了多行文本,这个选项能使其在Word中正确换行。
  • 错误处理doc.render()要用try...catch包裹。捕获的错误对象有一个非常有用的properties属性,它会告诉你具体是哪个标签(tag)出了问题,以及上下文信息,这是调试模板语法错误的利器。
  • 输出Bufferdoc.getZip().generate({ type: 'nodebuffer' })生成的是一个Node.js的Buffer对象,你可以直接写入文件,或者通过HTTP响应流式发送给浏览器。

3.4 第四步:处理复杂场景——循环、条件与图片

循环列表:如上例所示,在数据对象中传入数组projects,在模板中对应的位置使用{#projects}{/projects}包裹循环体。循环体内可以使用{name},{role}来访问数组每一项的属性。

条件判断:模板语法支持{^hasBonus}{/hasBonus}(如果hasBonus为falsy值,如false, null, undefined,则显示中间内容),以及{#hasBonus}{/hasBonus}(如果hasBonus为truthy值,则显示)。这在显示可选条款时非常有用。

动态图片插入:这需要安装并配置docxtemplater-image-module-free(免费版)或docxtemplater-image-module。步骤稍复杂:

  1. 安装模块。
  2. 在模板中,用{%imageTag}作为占位符。
  3. 在数据对象中,imageTag属性需要是一个对象,例如:
    { imageTag: { data: fs.readFileSync('logo.png'), // Buffer数据 size: [width, height], // 像素尺寸,如 [100, 50] // 或者使用物理尺寸 // width: 600000, // 以EMU为单位,600000 EMU ≈ 2cm // height: 300000, } }
  4. 在初始化docxtemplater时,加载图片模块。
    const ImageModule = require('docxtemplater-image-module-free'); const opts = { ... modules: [new ImageModule({ ... })] }; const doc = new Docxtemplater(zip, opts);
    图片模块的配置项(如centered是否居中)需要根据文档仔细设置。

4. 性能优化与大规模生成策略

当你的系统需要一次性生成数百甚至数千份文档时(例如批量打印快递单、期末成绩单),性能问题就会凸显。直接循环调用上面的generateContract函数会导致内存激增,甚至进程崩溃。

4.1 策略一:模板预加载与实例复用

正如之前提到的,避免每次生成都从磁盘读取和解析模板。我们可以将编译好的PizZip对象或docxtemplater的配置缓存起来。

// 模板管理器(单例模式) class TemplateManager { constructor() { this.templateCache = new Map(); // 缓存模板的Zip对象 } async loadTemplate(templateName) { if (this.templateCache.has(templateName)) { return this.templateCache.get(templateName); } const templatePath = path.resolve(__dirname, 'templates', `${templateName}.docx`); const content = await fs.readFile(templatePath); const zip = new PizZip(content); this.templateCache.set(templateName, zip); return zip; } async generateFromTemplate(templateName, data) { const zip = await this.loadTemplate(templateName); // 注意:Docxtemplater实例是有状态的(存储了渲染后的数据),不能直接复用。 // 每次生成都需要从干净的zip创建一个新实例。 const doc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true, }); doc.setData(data); try { doc.render(); } catch (error) { // ... 错误处理 throw error; } return doc.getZip().generate({ type: 'nodebuffer' }); } }

4.2 策略二:引入队列与流式处理

对于超大批量任务,不要同步处理。使用消息队列(如Bull、RabbitMQ)将生成任务异步化。每个工作进程从队列中领取任务,生成文档后上传到对象存储(如AWS S3、阿里云OSS、腾讯云COS),并将下载链接返回或存入数据库。这样可以水平扩展工作进程,避免阻塞主服务。

基本流程

  1. 用户触发批量生成请求。
  2. 后端API将N个生成任务(每个任务包含数据)推送到队列。
  3. 多个Node.js工作进程监听队列,并行处理任务。
  4. 每个进程生成单个文档后,立即将Buffer流式上传到云存储。
  5. 上传成功后,将文件URL记录到数据库,并标记任务完成。
  6. 前端可以通过轮询或WebSocket获取生成进度和最终的文件打包下载链接。

4.3 策略三:内存管理与Buffer处理

即使单个文档不大,同时处理成千上万个Buffer也会消耗大量内存。要确保及时释放内存。

  • 避免在内存中累积所有Buffer:不要用Promise.all一次性等待所有文档生成完毕。应该使用流或控制并发数。
  • 使用流进行文件操作:如果生成后直接写入磁盘,使用fs.createWriteStream配合文档生成流(如果库支持)。对于docxtemplater,它是先生成完整Buffer,可以这样写:
    const buffer = doc.getZip().generate({ type: 'nodebuffer' }); await fs.writeFile(outputPath, buffer); // 一次性写入 // 对于超大buffer,可以考虑使用stream,但docxtemplater输出的是完整buffer。
  • 手动触发垃圾回收(谨慎使用):在长时间循环中,可以在适当位置调用global.gc()(需要Node.js以--expose-gc参数启动),但这通常是最后的手段,优化代码结构才是根本。

5. 常见问题排查与调试技巧实录

在实际开发中,你肯定会遇到各种奇怪的问题。下面是我总结的“排坑指南”。

5.1 问题:生成的文档用Word打开报错“文件已损坏”

这是最常见的问题,几乎都是因为生成的ZIP包结构或内部XML不符合Office标准。

排查步骤

  1. 检查模板文件:确保模板是有效的.docx文件。用解压软件(如7-Zip)打开它,应该能看到[Content_Types].xml,word/document.xml等标准文件和文件夹。如果打不开或结构异常,说明模板本身就有问题。
  2. 检查数据中的特殊字符:如果你的数据包含XML特殊字符(如<,>,&,",'),docxtemplater默认会进行XML转义(将<变成&lt;),这是正确的。但如果你传入的数据本身已经是转义后的实体(比如从某些富文本编辑器来的&lt;p&gt;),就会导致双重转义,生成错误的XML。这时需要在数据传入前先解码,或者使用docxtemplaterrawXML标签(高级用法,需谨慎)。
  3. 对比“好”与“坏”的文件
    • adm-zip或解压软件,分别解压一个能正常打开的模板文件和你生成的错误文件。
    • 重点对比word/document.xml。用代码编辑器(如VSCode)打开,格式化XML,查找异常的地方,比如未闭合的标签、非法字符等。问题往往出现在你插入的动态数据附近。
  4. 验证ZIP包完整性:用adm-zip读取你生成的Buffer,尝试解压到内存并列出文件,看是否报错。
    const AdmZip = require('adm-zip'); try { const zip = new AdmZip(outputBuffer); const zipEntries = zip.getEntries(); // 获取所有条目 console.log('ZIP包内文件列表:'); zipEntries.forEach(entry => console.log(entry.entryName)); } catch (err) { console.error('生成的Buffer不是一个有效的ZIP文件:', err); }

5.2 问题:循环或条件判断没有生效

可能原因

  1. 模板语法错误:检查{#tags}{/tags}是否完全匹配,中间没有多余空格或换行符干扰(虽然通常允许有)。确保它们是一个完整的文本节点。
  2. 数据格式不对:对于循环,数据必须是数组。doc.setData({ projects: [...] })。如果projectsnullundefined,循环区块会被忽略。对于条件判断,确保你传入的是布尔值或可以被判断为truthy/falsy的值。
  3. Word自动更正干扰:有时Word会自动将你输入的花括号{}转换成其他字符(如中文引号)。在模板中输入占位符后,仔细检查其字体和编码,确保是纯英文符号。一个技巧是,先在记事本里写好占位符,再复制到Word中。

5.3 问题:中文或特殊字体显示异常(乱码或字体失效)

原因与解决

  1. 字体嵌入:如果你的模板使用了“微软雅黑”等非Windows系统默认字体,而生成文档的服务器(通常是Linux)上没有该字体,Word会尝试用默认字体(如宋体)替换,可能导致排版错乱。根本的解决方案是在模板设计阶段,将中文字体嵌入文档
    • 在Word中,打开“文件”->“选项”->“保存”。
    • 勾选“将字体嵌入文件”。可以选择“仅嵌入文档中使用的字符”以减小文件体积。
    • 这样,即使用户电脑没有该字体,文档也能正确显示。
  2. 编码问题:确保你的Node.js脚本文件(.js)和模板文件(.docx)都使用UTF-8编码。在数据对象中,中文字符串是正常的JavaScript字符串即可。

5.4 问题:图片无法显示或尺寸不对

排查方向

  1. 图片模块未正确配置:检查是否安装了正确的图片模块,并在Docxtemplater构造函数中通过modules选项加载。
  2. 图片数据格式:确保传入的data是Buffer或base64字符串。如果是文件路径,需要用fs.readFileSync读取为Buffer。
  3. 尺寸单位size数组的单位是像素,而width/height的单位是英制单位EMU(English Metric Unit)。1厘米 ≈ 360000 EMU。混用单位会导致图片巨大或微小。建议统一使用一种。对于打印文档,使用EMU单位更精确。
  4. 图片路径或类型:确保图片文件存在且格式被支持(如PNG, JPEG)。某些.docx版本对SVG支持可能不佳。

5.5 调试神器:启用详细日志与输出中间文件

docxtemplater可以开启调试模式,输出更详细的信息。

const doc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true, // 开启调试 debug: true, // 输出一些日志 // 或者使用更详细的日志函数 // parser: function(tag) { console.log('解析标签:', tag); return { ... }; } });

最实用的调试方法,是在渲染出错或结果不对时,将出错的中间XML文件输出到磁盘进行对比。

try { doc.render(); } catch (error) { // 输出错误的上下文信息 console.error(error.properties); // 可以将当前出错的zip内容写出来检查 const faultyBuffer = doc.getZip().generate({ type: 'nodebuffer' }); await fs.writeFile('debug_faulty.docx', faultyBuffer); console.log('已保存错误文档供调试: debug_faulty.docx'); throw error; }

然后,用解压软件打开这个debug_faulty.docx,检查word/document.xml,找到错误标签附近的内容,就能一目了然地看到问题所在。

← 返回列表