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

基于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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 11:27:45