使用T3、ExcelJS与tRPC实现Excel下载失败求助(路由端问题)
解决Next.js + tRPC + ExcelJS Excel文件下载问题
问题分析
你的代码存在几个关键问题:
- tRPC路由未设置正确响应头:浏览器无法识别返回的二进制流为可下载Excel文件
- 前端未正确处理二进制响应:tRPC客户端默认解析JSON,会导致二进制文件解析失败
- 前端函数调用错误:按钮点击事件绑定的
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
相关产品推荐
相关产品推荐

