跳至主要內容
Skip to content

Blob 與 Base64 的互轉實戰

在前端開發(如 Vue.js 或 React)處理圖片上傳或裁切時,經常需要在 Blob 與 Base64 之間轉換。本篇將提供完整的轉換程式碼,並點出常見的效能陷阱。


先分清楚:Base64 與 Data URL

很多人說「Base64 圖片」時,其實混在一起講了兩種字串:

名稱範例說明
純 Base64iVBORw0KGgoAAAANS...只有編碼後的內容
Data URLdata:image/png;base64,iVBORw0KGgoAAAANS...包含 MIME Type 與 Base64 內容,可直接放進 img.src

FileReader.readAsDataURL() 回傳的是 Data URL,不是只有純 Base64。後端 API 如果只要純 Base64,通常需要把逗號前面的前綴移除。


一、 情境 A:Blob 轉 Base64

用途

  • 製作圖片預覽 (Preview)
  • 將圖片存入 localStorage(因為 localStorage 只能存字串)
  • 透過 JSON API 傳輸小型圖片

程式碼

javascript
/**
 * 將 Blob (或 File) 轉換為 Base64 字串
 * @param {Blob} blob
 * @returns {Promise<string>}
 */
function blobToBase64(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result); // result 是 Data URL
    reader.onerror = reject;
    reader.readAsDataURL(blob);
  });
}

// 使用範例
async function handleFileSelect(file) {
  const base64String = await blobToBase64(file);
  console.log(base64String);
  // 輸出: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
}

現代替代方案

如果你的目標瀏覽器支援較新的 API,也可以使用 arrayBuffer() 讀取後再編碼。下面這種寫法比直接 reduce 串接字串穩定,但仍不建議用在大檔案;大檔案應該避免轉 Base64。

javascript
async function blobToBase64Modern(blob) {
  const buffer = await blob.arrayBuffer();
  const bytes = new Uint8Array(buffer);
  const chunkSize = 0x8000;
  const chunks = [];

  for (let i = 0; i < bytes.length; i += chunkSize) {
    const chunk = bytes.subarray(i, i + chunkSize);
    chunks.push(String.fromCharCode(...chunk));
  }

  const binary = chunks.join("");
  const mimeType = blob.type || "application/octet-stream";

  return `data:${mimeType};base64,${btoa(binary)}`;
}

二、 情境 B:Base64 轉 Blob

用途

  • 圖片裁切後(Canvas 通常吐出 Base64),需要轉回 Blob 以便透過 API 上傳
  • 接收 Base64 格式的圖片資料,需要建立 Blob URL 預覽

程式碼

javascript
/**
 * 將 Base64 字串轉換為 Blob
 * @param {string} base64 - 完整的 Data URL,或純 Base64 字串
 * @param {string} mimeType - 可選;純 Base64 字串需自行提供 MIME Type
 * @returns {Blob}
 */
function base64ToBlob(base64, mimeType) {
  // 1. 解析 MIME Type (如果沒有提供)
  const match = base64.match(/^data:(.+?);base64,(.*)$/);

  if (!mimeType) {
    mimeType = match ? match[1] : "application/octet-stream";
  }

  // 2. 去除 Data URL 前綴後解碼;若本來就是純 Base64 則直接使用
  const rawBase64 = (match ? match[2] : base64).replace(/\s/g, "");
  const byteString = atob(rawBase64);

  // 3. 建立 ArrayBuffer 並填入資料
  const ab = new ArrayBuffer(byteString.length);
  const ia = new Uint8Array(ab);

  for (let i = 0; i < byteString.length; i++) {
    ia[i] = byteString.charCodeAt(i);
  }

  // 4. 建立並回傳 Blob 物件
  return new Blob([ab], { type: mimeType });
}

// 使用範例
const base64Image = "data:image/png;base64,iVBORw0KGgo...";
const blob = base64ToBlob(base64Image);
console.log(blob); // Blob { size: 1234, type: "image/png" }

如果你收到的是 URL-safe Base64(使用 -_),要先轉回標準 Base64:

javascript
function normalizeBase64(base64) {
  return base64.replace(/-/g, "+").replace(/_/g, "/");
}

三、 效能陷阱與最佳實踐

陷阱 1:大圖片轉 Base64 會造成 UI 卡頓

FileReader.readAsDataURL() 會把整張圖片轉成一個超長字串。如果圖片有 5MB,Base64 字串會變成約 6.7MB,而瀏覽器需要:

  1. 讀取整個檔案到記憶體
  2. 編碼成 Base64 字串
  3. 把字串塞進 DOM(如果你設定給 <img src="...">

這整個過程可能讓 UI 凍結數秒。

解法:使用 Blob URL 做預覽

javascript
// ❌ 不推薦:轉 Base64 做預覽
const base64 = await blobToBase64(largeFile); // 可能卡頓 2-3 秒
imageElement.src = base64;

// ✅ 推薦:使用 Blob URL
const url = URL.createObjectURL(largeFile); // 瞬間完成
imageElement.src = url;

// 記得在不需要時釋放;圖片載入完成後再釋放比較保險
imageElement.onload = () => {
  URL.revokeObjectURL(url);
};

陷阱 2:忘記釋放 Blob URL

URL.createObjectURL() 會在瀏覽器記憶體中建立一個參照。如果你不主動呼叫 URL.revokeObjectURL(),這個參照會一直佔用記憶體直到頁面關閉。

WARNING

記憶體洩漏 如果你的應用頻繁建立預覽(例如圖片編輯器),忘記釋放 Blob URL 可能導致記憶體暴增。

javascript
// Vue 3 範例:在元件卸載時清理
import { ref, onUnmounted } from "vue";

const previewUrl = ref(null);

function setPreview(file) {
  // 先釋放舊的
  if (previewUrl.value) {
    URL.revokeObjectURL(previewUrl.value);
  }
  previewUrl.value = URL.createObjectURL(file);
}

onUnmounted(() => {
  if (previewUrl.value) {
    URL.revokeObjectURL(previewUrl.value);
  }
});

如果使用者會快速切換多張圖片,建議先釋放舊 URL,再建立新 URL;如果是下載連結或影片播放,則要等使用者不再需要那個 URL 時才釋放。


四、 完整範例:圖片裁切上傳流程

以下是一個常見的實戰流程:使用者選擇圖片 → 裁切 → 上傳。

javascript
// 假設你使用 cropper.js 或類似的裁切庫

async function handleCropAndUpload(cropper) {
  // 1. 取得裁切後的 Canvas
  const canvas = cropper.getCroppedCanvas({
    width: 800,
    height: 600,
  });

  // 2. Canvas 轉 Blob (比 toDataURL 更有效率)
  const blob = await new Promise((resolve, reject) => {
    canvas.toBlob((result) => {
      if (result) {
        resolve(result);
      } else {
        reject(new Error("Canvas 轉 Blob 失敗"));
      }
    }, "image/jpeg", 0.9);
  });

  // 3. 建立 FormData 上傳
  const formData = new FormData();
  formData.append("avatar", blob, "cropped-image.jpg");

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

  return response.json();
}

TIP

為什麼用 canvas.toBlob() 而不是 canvas.toDataURL()

  • toDataURL() 回傳 Base64 字串,你還需要再轉成 Blob 才能上傳
  • toBlob() 直接回傳 Blob,省去轉換步驟,效能更好

五、 快速參考表

需求方法注意事項
Blob → Data URLFileReader.readAsDataURL()回傳包含 data:...;base64, 前綴
純 Base64 → Blobatob() + Uint8Array + new Blob()需自行提供 MIME Type
圖片預覽URL.createObjectURL()用完要 revokeObjectURL()
Canvas → 上傳用 Blobcanvas.toBlob()需處理可能回傳 null 的情況
大檔案上傳FormData 直接傳 File / Blob不要先轉 Base64

總結

  • Blob → Base64/Data URL:使用 FileReader,但要注意大檔案的效能問題
  • Base64 → Blob:使用 atob() 解碼 + Uint8Array 建構
  • 圖片預覽:優先使用 URL.createObjectURL(),避免 Base64 的效能開銷
  • Canvas 上傳:使用 canvas.toBlob() 直接取得 Blob

常見誤解

誤解正確理解
readAsDataURL() 回傳純 Base64它回傳 Data URL,前面包含 MIME Type 前綴
Base64 適合所有圖片預覽大圖預覽更適合 Blob URL
Blob URL 用完可以永遠不管頻繁建立不釋放會造成記憶體累積
canvas.toDataURL()toBlob() 簡單所以更好上傳場景通常 toBlob() 更省記憶體與轉換成本

練習:轉換前先判斷格式

練習 1:Data URL 還是純 Base64?

跑下面這段,觀察輸出開頭:

javascript
const blob = new Blob(["Hello"], { type: "text/plain" });
const reader = new FileReader();

reader.onload = () => {
  console.log(reader.result);
  console.log(reader.result.startsWith("data:"));
};

reader.readAsDataURL(blob);
看答案

readAsDataURL() 會輸出類似 data:text/plain;base64,SGVsbG8= 的 Data URL。逗號後面的 SGVsbG8= 才是純 Base64 內容。

練習 2:把 Data URL 拆成兩段

javascript
const dataUrl = "data:text/plain;base64,SGVsbG8=";
const [header, payload] = dataUrl.split(",");

console.log(header);
console.log(payload);
console.log(atob(payload));

這個練習能驗證:Data URL = metadata 前綴 + Base64 內容。

自我檢查

  • readAsDataURL() 回傳純 Base64 嗎?
  • 預覽大圖片時,為什麼 Blob URL 通常比 Base64 好?
  • URL.revokeObjectURL() 太早呼叫可能會發生什麼事?
  • Canvas 上傳時,為什麼優先考慮 toBlob()

← 上一章:Bit/Byte/0-255 | 返回專題首頁 | 下一章:ArrayBuffer 操作 →