Blob 與 Base64 的互轉實戰
在前端開發(如 Vue.js 或 React)處理圖片上傳或裁切時,經常需要在 Blob 與 Base64 之間轉換。本篇將提供完整的轉換程式碼,並點出常見的效能陷阱。
先分清楚:Base64 與 Data URL
很多人說「Base64 圖片」時,其實混在一起講了兩種字串:
| 名稱 | 範例 | 說明 |
|---|---|---|
| 純 Base64 | iVBORw0KGgoAAAANS... | 只有編碼後的內容 |
| Data URL | data:image/png;base64,iVBORw0KGgoAAAANS... | 包含 MIME Type 與 Base64 內容,可直接放進 img.src |
FileReader.readAsDataURL() 回傳的是 Data URL,不是只有純 Base64。後端 API 如果只要純 Base64,通常需要把逗號前面的前綴移除。
一、 情境 A:Blob 轉 Base64
用途
- 製作圖片預覽 (Preview)
- 將圖片存入
localStorage(因為 localStorage 只能存字串) - 透過 JSON API 傳輸小型圖片
程式碼
/**
* 將 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。
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 預覽
程式碼
/**
* 將 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:
function normalizeBase64(base64) {
return base64.replace(/-/g, "+").replace(/_/g, "/");
}三、 效能陷阱與最佳實踐
陷阱 1:大圖片轉 Base64 會造成 UI 卡頓
FileReader.readAsDataURL() 會把整張圖片轉成一個超長字串。如果圖片有 5MB,Base64 字串會變成約 6.7MB,而瀏覽器需要:
- 讀取整個檔案到記憶體
- 編碼成 Base64 字串
- 把字串塞進 DOM(如果你設定給
<img src="...">)
這整個過程可能讓 UI 凍結數秒。
解法:使用 Blob URL 做預覽
// ❌ 不推薦:轉 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 可能導致記憶體暴增。
// 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 時才釋放。
四、 完整範例:圖片裁切上傳流程
以下是一個常見的實戰流程:使用者選擇圖片 → 裁切 → 上傳。
// 假設你使用 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 URL | FileReader.readAsDataURL() | 回傳包含 data:...;base64, 前綴 |
| 純 Base64 → Blob | atob() + Uint8Array + new Blob() | 需自行提供 MIME Type |
| 圖片預覽 | URL.createObjectURL() | 用完要 revokeObjectURL() |
| Canvas → 上傳用 Blob | canvas.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?
跑下面這段,觀察輸出開頭:
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 拆成兩段
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()?