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

使用T3、ExcelJS与tRPC实现Excel下载失败求助(路由端问题)

解决Next.js + tRPC + ExcelJS Excel文件下载问题

问题分析

你的代码存在几个关键问题:

  1. tRPC路由未设置正确响应头:浏览器无法识别返回的二进制流为可下载Excel文件
  2. 前端未正确处理二进制响应:tRPC客户端默认解析JSON,会导致二进制文件解析失败
  3. 前端函数调用错误:按钮点击事件绑定的handleClickOpen未定义,实际应为onDownload

修正后的代码实现

1. tRPC路由代码(含响应头设置与错误处理)

import { z } from "zod";
import { router, publicProcedure } from "../trpc";
import ExcelJS from "exceljs";
import path from "path";
import fs from "fs";

export const guestRouter = router({
  xlsx: publicProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      try {
        // 使用绝对路径避免文件找不到问题
        const FILE_PATH = path.resolve(process.cwd(), "src/utils/01.xlsx");

        // 检查文件是否存在
        if (!fs.existsSync(FILE_PATH)) {
          ctx.res.statusCode = 404;
          ctx.res.end("目标Excel文件不存在");
          return;
        }

        const wb = new ExcelJS.Workbook();
        await wb.xlsx.readFile(FILE_PATH);

        const ws = wb.getWorksheet(1);
        if (!ws) {
          ctx.res.statusCode = 404;
          ctx.res.end("工作表不存在");
          return;
        }

        // 修改指定单元格内容
        ws.getCell("H4").value = input.id;

        // 设置响应头,告诉浏览器这是可下载的Excel文件
        ctx.res.setHeader(
          "Content-Type",
          "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        );
        ctx.res.setHeader(
          "Content-Disposition",
          `attachment; filename="modified-${input.id}.xlsx"`
        );

        // 将修改后的工作簿写入响应流
        await wb.xlsx.write(ctx.res);
        ctx.res.end(); // 确保响应正常结束
      } catch (error) {
        console.error("Excel文件处理失败:", error);
        ctx.res.statusCode = 500;
        ctx.res.end("生成Excel文件失败");
      }
    }),
});

2. 前端代码(正确处理二进制响应)

import React from "react";
import { trpc } from "../utils/trpc";

function Print() {
  const trpcClient = trpc.useContext();

  const onDownload = React.useCallback(async () => {
    try {
      // 获取原始响应对象,避免tRPC自动解析JSON
      const response = await trpcClient.guest.xlsx.fetch({ id: "test" });
      const blob = await response.blob();

      // 创建下载链接并触发下载
      const url = window.URL.createObjectURL(blob);
      const downloadLink = document.createElement("a");
      downloadLink.href = url;
      downloadLink.download = "modified-excel.xlsx";
      document.body.appendChild(downloadLink);
      downloadLink.click();

      // 清理资源
      window.URL.revokeObjectURL(url);
      document.body.removeChild(downloadLink);
    } catch (error) {
      console.error("下载失败:", error);
      alert("Excel文件下载失败,请稍后重试");
    }
  }, [trpcClient]);

  return (
    <>
      <button onClick={onDownload}>下载Excel</button>
    </>
  );
}

export default Print;

关于ctx.res.pipe()的使用场景

如果不需要修改Excel文件,只是直接返回已存在的文件,使用pipe()会更高效(流式传输,无需加载整个文件到内存),实现方式如下:

// tRPC路由中直接返回现有文件的代码
.mutation(async ({ ctx }) => {
  try {
    const FILE_PATH = path.resolve(process.cwd(), "src/utils/01.xlsx");
    
    if (!fs.existsSync(FILE_PATH)) {
      ctx.res.statusCode = 404;
      ctx.res.end("文件不存在");
      return;
    }

    ctx.res.setHeader(
      "Content-Type",
      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    );
    ctx.res.setHeader(
      "Content-Disposition",
      'attachment; filename="original-excel.xlsx"'
    );

    // 创建文件读取流并管道到响应
    const readStream = fs.createReadStream(FILE_PATH);
    readStream.pipe(ctx.res);

    // 处理流读取错误
    readStream.on("error", (err) => {
      console.error("文件读取失败:", err);
      ctx.res.statusCode = 500;
      ctx.res.end("文件读取失败");
    });
  } catch (error) {
    console.error("下载处理失败:", error);
    ctx.res.statusCode = 500;
    ctx.res.end("下载处理失败");
  }
}),

但如果需要修改文件内容,必须使用ExcelJS加载文件到内存修改后再写入响应,无法直接使用pipe()。


内容的提问来源于stack exchange,提问作者Balius

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 05:10:20