You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 00:39:18