React TypeScript中JSZip写入docx/rtf到压缩包失败如何解决
核心问题排查
你的代码存在4个直接导致功能失效的错误:
- 类型判断完全错误:
createWordClick返回的是docx库内置的Document类实例,既不是File类型也不是Blob类型,你手动将实例传入Blob构造函数、或者直接把实例传给JSZip的file方法,写入的都是无效的对象序列化数据,根本不是可正常打开的Word文件。 - 异步时序逻辑错误:
Packer.toBlob()是异步Promise操作,你没有等待所有文档的Blob生成、写入zip的流程执行完毕,就提前触发了zip打包逻辑,此时JSZip实例内还没有存入任何有效Word文件,最终生成的压缩包自然找不到目标文件。 - 冗余逻辑与格式不匹配:你重复调用了两次
zip.generateAsync()属于无意义操作;另外docx库的Packer只能生成标准OOXML格式的.docx文件,根本不支持生成.rtf格式,硬写.rtf后缀会导致解压后文件直接报格式损坏。 - 全局变量污染:你使用全局数组
bigDockArrTable存储文档内容,多次触发导出时会出现不同文档内容拼接错乱的问题。
修复实现
首先调整createWordClick函数,移除全局变量,将文档内容数组改为函数内部变量:
import { saveAs } from "file-saver"; import JSZip from "jszip"; import imageSize from 'image-size'; import { Document, Paragraph, Table, TableCell, TableRow, Packer, WidthType, BorderStyle, AlignmentType, HeadingLevel, convertInchesToTwip, VerticalAlign, TextRun, ImageRun, } from "docx"; interface ForZip { array: any[]; wordHeaders: string[]; dataKeys: string[]; image?: any; headerName?:string } const createWordClick = (arrForWord: ForZip[]) => { // 文档内容数组改为函数内部定义,每次调用生成独立实例,避免全局污染 const bigDockArrTable: (Paragraph | Table)[] = []; arrForWord.forEach((forward) => { if (forward.array.length !== 0 && forward.headerName !== undefined && forward.image !== undefined && forward.image !== null) { const originalSize = imageSize(Buffer.from(forward.image, "base64")); const ratioWidthHeight = Number(originalSize.height) / Number(originalSize.width) if (300*ratioWidthHeight < 700){ const imgParagraph = new Paragraph({ children: [ new TextRun({ text: `рис. ${forward.headerName}`, bold: false, allCaps: true, }), new ImageRun({ data: Buffer.from(forward.image, "base64"), transformation: { width: 600, height: 600*ratioWidthHeight, } }) ], alignment: AlignmentType.CENTER, }); bigDockArrTable.push(imgParagraph); } // 构建表头 const myrows = [ new TableRow({ tableHeader: true, children: forward.wordHeaders.map((e) => { return new TableCell({ borders: { top: { style: BorderStyle.SINGLE, size: 10, color: "000000" }, bottom: { style: BorderStyle.SINGLE, size: 10, color: "000000" }, left: { style: BorderStyle.SINGLE, size: 10, color: "000000" }, right: { style: BorderStyle.SINGLE, size: 10, color: "000000" }, }, width: { size: 2505, type: WidthType.DXA }, verticalAlign: VerticalAlign.CENTER, children: [ new Paragraph({ text: e, style: "top", heading: HeadingLevel.HEADING_1, alignment: AlignmentType.CENTER, }), ], }); }), }), ]; // 构建表格行 const tableRows = forward.array.map((i) => { return new TableRow({ children: forward.dataKeys.map((element) => { return new TableCell({ children: [ new Paragraph({ text: i[element].toString() }), ], }); }), }); }); const emptyParagraph = new Paragraph({ children: [ new TextRun({ text: " ", bold: true, allCaps: true }), ], alignment: AlignmentType.CENTER, }); myrows.push(...tableRows); const table = new Table({ rows: myrows, indent: { size: 900, type: WidthType.DXA }, }); bigDockArrTable.push(emptyParagraph); try { const titleParagraph = new Paragraph({ children: [ new TextRun({ text: forward.headerName, bold: true, allCaps: true }), ], alignment: AlignmentType.CENTER, }); bigDockArrTable.push(titleParagraph); } catch { bigDockArrTable.push(emptyParagraph); } bigDockArrTable.push(table); } }); return new Document({ styles: { default: { listParagraph: { paragraph: { indent: { left: convertInchesToTwip(0.5) } }, }, }, paragraphStyles: [ { id: "top", name: "Top", basedOn: "Normal", next: "Normal", run: { color: "000000", bold: true, size: 28 }, }, ], }, sections: [ { properties: {}, children: [...bigDockArrTable] }, ], }); };
调整导出压缩包的逻辑,等待所有异步生成文档的操作完成后再打包:
export const export2zip = async (arrforzip: ForZip[]) => { const zip = new JSZip(); const wordFolder = zip.folder("word"); if (!wordFolder) throw new Error("创建压缩包目录失败"); // 收集所有文档生成、写入zip的异步任务 const writeTasks = arrforzip.map(async (forword) => { // 跳过没有headerName的无效项 if (!forword.headerName) return; const doc = createWordClick([forword]); // 等待docx库生成Blob const docBlob = await Packer.toBlob(doc); // 写入zip,传Blob时不需要设置binary:true,JSZip会自动识别类型 // 注意:docx库只生成docx格式,不要用rtf后缀,否则文件损坏 wordFolder.file(`${forword.headerName}.docx`, docBlob); }); // 等待所有文件全部写入zip后,再生成压缩包 await Promise.all(writeTasks); const zipBlob = await zip.generateAsync({ type: "blob", compression: "DEFLATE", mimeType: "application/zip" }); saveAs(zipBlob, `examples.zip`); };
注意事项
- 如果你确实需要导出
.rtf格式文件,不要用docx库,该库不支持RTF格式生成,需要更换支持RTF的文档生成库。 - 上述实现调整了
createWordClick的传参逻辑,每个条目生成独立的Word文件存入zip,符合批量导出的需求。 - 压缩包的MIME类型统一用
application/zip即可,不需要使用Word文档的MIME类型。
内容的提问来源于stack exchange,提问作者Cyril Strone
相关产品推荐
相关产品推荐

