跳至主要內容
Skip to content

JSON + Blob 混合上傳 — FormData 完全攻略

在實務開發中,你可能遇過這種需求:後端 API 要求同時接收 JSON 格式的資料(如使用者資訊)和 Blob 格式的檔案(如頭像圖片)

這種情況無法單純用 JSON 解決,本篇將教你如何使用 FormData 完美解決這個問題。


一、 問題情境

假設你正在開發一個「會員資料編輯」功能,API 規格如下:

POST /api/user/profile

需要傳送:
1. 使用者資訊 (JSON):{ name: "小明", email: "ming@example.com" }
2. 頭像圖片 (Blob):一張裁切過的 JPEG 圖片

二、 為什麼不能只用 JSON?

如同我們在第一篇討論的,JSON 不支援 Binary 型別。如果你嘗試這樣做:

javascript
// ❌ 這樣不行
const payload = {
  name: "小明",
  email: "ming@example.com",
  avatar: someBlob, // Blob 物件
};

fetch("/api/user/profile", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(payload),
});

JSON.stringify 會把 Blob 物件變成空物件 {},後端什麼都收不到。

替代方案比較

方案優點缺點
把圖片轉 Base64 放進 JSON程式碼簡單資料膨脹 33%,後端需解碼
分兩次請求邏輯清晰多一次網路往返,需處理交易一致性
使用 FormData標準做法,傳輸效率高需了解 multipart/form-data 格式

三、 FormData 完整解法

FormData 是瀏覽器原生支援的物件,專門用來建構 multipart/form-data 格式的請求。這種格式天生就支援混合傳輸文字與二進制資料。

基本用法

javascript
async function updateProfile(userData, avatarBlob) {
  const formData = new FormData();

  // 附加一般文字欄位
  formData.append("name", userData.name);
  formData.append("email", userData.email);

  // 附加 Blob 檔案
  // 第三個參數是檔名,後端通常需要這個資訊
  formData.append("avatar", avatarBlob, "avatar.jpg");

  const response = await fetch("/api/user/profile", {
    method: "POST",
    body: formData,
    // 注意:不要手動設定 Content-Type!
    // 瀏覽器會自動加上正確的 boundary
  });

  return response.json();
}

> **不要手動設定 `Content-Type`**

使用 FormData 時,瀏覽器會自動產生類似這樣的 header:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

如果你手動設定 Content-Type: multipart/form-data,會缺少 boundary 參數,導致後端無法解析。

附加巢狀 JSON 物件

如果你的 JSON 資料比較複雜(例如巢狀物件),可以把整個 JSON 序列化後作為一個欄位傳送:

javascript
async function updateProfile(userData, avatarBlob) {
  const formData = new FormData();

  // 方法一:把複雜物件轉成 JSON 字串
  formData.append(
    "data",
    JSON.stringify({
      name: userData.name,
      email: userData.email,
      preferences: {
        theme: "dark",
        notifications: true,
      },
    })
  );

  // 方法二:建立一個 JSON Blob(某些後端框架更好處理)
  const jsonBlob = new Blob([JSON.stringify(userData)], {
    type: "application/json",
  });
  formData.append("metadata", jsonBlob, "metadata.json");

  // 附加檔案
  formData.append("avatar", avatarBlob, "avatar.jpg");

  // 發送請求
  const response = await fetch("/api/user/profile", {
    method: "POST",
    body: formData,
  });

  return response.json();
}

四、 Axios 實戰範例

如果你使用 Axios,用法幾乎相同:

javascript
import axios from "axios";

async function uploadWithProgress(userData, avatarBlob, onProgress) {
  const formData = new FormData();
  formData.append("data", JSON.stringify(userData));
  formData.append("avatar", avatarBlob, "avatar.jpg");

  const response = await axios.post("/api/user/profile", formData, {
    // 不需要設定 Content-Type,Axios 會自動處理

    // 上傳進度監聽
    onUploadProgress: (progressEvent) => {
      const percent = Math.round(
        (progressEvent.loaded * 100) / progressEvent.total
      );
      onProgress?.(percent);
    },
  });

  return response.data;
}

// 使用範例
uploadWithProgress({ name: "小明" }, avatarBlob, (percent) =>
  console.log(`上傳進度: ${percent}%`)
);

五、 後端接收範例

為了讓你更完整理解整個流程,以下是後端的接收方式:

Node.js (Express + multer)

javascript
const express = require("express");
const multer = require("multer");

const app = express();
const upload = multer({ dest: "uploads/" });

app.post("/api/user/profile", upload.single("avatar"), (req, res) => {
  // 文字欄位
  const userData = JSON.parse(req.body.data);
  console.log("使用者資料:", userData);

  // 檔案資訊
  const file = req.file;
  console.log("檔案路徑:", file.path);
  console.log("原始檔名:", file.originalname);
  console.log("檔案大小:", file.size);

  res.json({ success: true });
});

Python (FastAPI)

python
from fastapi import FastAPI, UploadFile, Form
import json

app = FastAPI()

@app.post("/api/user/profile")
async def update_profile(
    data: str = Form(...),
    avatar: UploadFile = None
):
    user_data = json.loads(data)
    print(f"使用者資料: {user_data}")

    if avatar:
        contents = await avatar.read()
        print(f"檔案大小: {len(contents)} bytes")

    return {"success": True}

六、 進階技巧

多檔案上傳

javascript
function uploadMultipleFiles(files) {
  const formData = new FormData();

  // 使用相同的 key name,後端會收到陣列
  files.forEach((file) => {
    formData.append("files", file, file.name);
  });

  return fetch("/api/upload", {
    method: "POST",
    body: formData,
  });
}

帶有認證的上傳

javascript
async function authenticatedUpload(formData) {
  const token = localStorage.getItem("authToken");

  const response = await fetch("/api/upload", {
    method: "POST",
    headers: {
      // 只設定 Authorization,不要設定 Content-Type
      Authorization: `Bearer ${token}`,
    },
    body: formData,
  });

  return response.json();
}

取消上傳

javascript
function uploadWithCancel(formData) {
  const controller = new AbortController();

  const promise = fetch("/api/upload", {
    method: "POST",
    body: formData,
    signal: controller.signal,
  });

  return {
    promise,
    cancel: () => controller.abort(),
  };
}

// 使用範例
const { promise, cancel } = uploadWithCancel(formData);

// 5 秒後取消(如果還沒完成)
setTimeout(() => cancel(), 5000);

七、 常見錯誤排查

症狀可能原因解法
後端收到空物件手動設定了錯誤的 Content-Type移除 Content-Type header
中文檔名亂碼編碼問題使用英文檔名或 URL encode
上傳失敗 413檔案超過伺服器限制調整伺服器設定或壓縮圖片
CORS 錯誤後端未允許 multipart/form-data調整後端 CORS 設定

總結

概念說明
FormData瀏覽器原生物件,用於建構 multipart/form-data 請求
boundary分隔不同欄位的標記,由瀏覽器自動產生
Content-Type使用 FormData 時不要手動設定
JSON + Blob把 JSON 序列化後作為欄位,Blob 直接 append

掌握 FormData 後,你就能優雅地處理任何「混合上傳」的需求,這是現代 Web 應用的必備技能。


延伸閱讀


← 上一章:ArrayBuffer 操作 | 返回專題首頁