基于T3栈(Next.js+TRPC)实现前端Blob图片上传后端的方案咨询
解决方案:通过T3栈上传Leaflet截图Blob并保存为PDF
核心思路
放弃Base64/ArrayBuffer的序列化传输,改用FormData直接传输Blob——这是二进制文件上传的标准方案,能避免体积膨胀和序列化异常问题。以下是前端+后端的完整实现步骤:
前端实现(Next.js Client Component + TRPC)
1. 截图并构建FormData
利用leaflet-simple-map-screenshoter获取Blob后,将其存入FormData:
import { useMutation } from '@tanstack/react-query'; import { api } from '@/utils/api'; import { Map } from 'leaflet'; import { useRef } from 'react'; import 'leaflet-simple-map-screenshoter'; const MapUploadButton = () => { const uploadMutation = api.map.saveScreenshotAsPdf.useMutation(); const mapRef = useRef<Map>(null); const handleUpload = async () => { if (!mapRef.current) return; // 获取地图截图Blob const screenshotBlob = await mapRef.current.screenshot({ format: 'png', quality: 0.8, }); // 构建FormData const formData = new FormData(); formData.append('mapScreenshot', screenshotBlob, 'map-shot.png'); // 发送请求 try { await uploadMutation.mutateAsync(formData); alert('PDF保存成功'); } catch (err) { console.error('上传失败:', err); } }; return ( <button onClick={handleUpload} disabled={uploadMutation.isLoading}> {uploadMutation.isLoading ? '上传中...' : '保存地图为PDF'} </button> ); }; export default MapUploadButton;
2. 配置TRPC客户端支持FormData
TRPC默认会序列化请求体为JSON,需要自定义fetch逻辑以支持FormData:
// @/utils/api.ts import { createTRPCNext } from '@trpc/next'; import { httpBatchLink } from '@trpc/client'; import type { AppRouter } from '@/server/api/root'; export const api = createTRPCNext<AppRouter>({ config() { return { links: [ httpBatchLink({ url: '/api/trpc', async headers() { return { // 这里添加你的认证头(如JWT) }; }, // 自定义fetch处理FormData async fetch(url, options) { if (options?.body instanceof FormData) { return fetch(url, { ...options, // 移除手动设置的Content-Type,让浏览器自动添加multipart边界 headers: { ...options.headers, 'Content-Type': undefined }, }); } return fetch(url, options); }, }), ], }; }, ssr: false, });
后端实现(TRPC Router + Next.js)
1. 定义TRPC Mutation
在TRPC路由中解析FormData,将图片转换为PDF并保存:
// @/server/api/routers/map.ts import { z } from 'zod'; import { createTRPCRouter, publicProcedure } from '@/server/api/trpc'; import { PDFDocument } from 'pdf-lib'; import fs from 'fs/promises'; import path from 'path'; export const mapRouter = createTRPCRouter({ saveScreenshotAsPdf: publicProcedure .input(z.any()) // FormData无法用Zod直接校验,后续手动处理 .mutation(async ({ ctx }) => { // 从Request中解析FormData const formData = await ctx.req.formData(); const screenshotFile = formData.get('mapScreenshot') as File; if (!screenshotFile) throw new Error('未获取到截图文件'); // 将File转为ArrayBuffer const imgBuffer = await screenshotFile.arrayBuffer(); // 创建PDF并嵌入图片 const pdfDoc = await PDFDocument.create(); const page = pdfDoc.addPage(); const pngImage = await pdfDoc.embedPng(imgBuffer); // 调整图片尺寸适配PDF页面 const imgScale = Math.min(page.getWidth() / pngImage.width, page.getHeight() / pngImage.height); const scaledImg = pngImage.scale(imgScale); page.drawImage(scaledImg, { x: page.getWidth() / 2 - scaledImg.width / 2, y: page.getHeight() / 2 - scaledImg.height / 2, }); // 生成PDF Buffer const pdfBuffer = await pdfDoc.save(); // 保存到本地(生产环境建议用云存储如S3) const saveDir = path.join(process.cwd(), 'public', 'map-pdfs'); await fs.mkdir(saveDir, { recursive: true }); const pdfPath = path.join(saveDir, `${Date.now()}-map.pdf`); await fs.writeFile(pdfPath, pdfBuffer); return { success: true, pdfUrl: `/map-pdfs/${path.basename(pdfPath)}` }; }), });
2. 调整Next.js请求大小限制
如果截图较大,需要在next.config.js中增大请求体限制:
/** @type {import('next').NextConfig} */ const nextConfig = { serverBodySizeLimit: '10mb', // 根据实际需求调整 }; module.exports = nextConfig;
关键注意事项
- 避免Base64:Base64会让文件体积增加33%,极易触发请求大小限制,FormData是二进制传输的最优选择。
- 不要手动设置
Content-Type: multipart/form-data:浏览器会自动添加正确的边界标识,手动设置会导致解析失败。 - 生产环境存储:不要用本地文件系统保存PDF,建议上传到AWS S3、Cloudinary等云存储服务,避免服务器存储瓶颈。
内容的提问来源于stack exchange,提问作者Olav Ausland
相关产品推荐
相关产品推荐

