File System Access API 實戰:瀏覽器端本地檔案讀寫架構
瀏覽器檔案存取的演進
| 方案 | 讀寫能力 | 持久化 | 權限模型 | 效能 |
|---|---|---|---|---|
<input type="file"> |
唯讀 | ❌ | 無 | 中 |
| File + FileReader | 唯讀 | ❌ | 無 | 中 |
| IndexedDB | 讀寫位元組 | ✅ | 同源 | 低(序列化) |
| File System Access | 讀寫 | ✅ | 使用者授權 | 高 |
| OPFS | 讀寫 | ✅ | 同源沙箱 | 最高 |
File System Access API 讓瀏覽器真正具備了本地應用的檔案讀寫能力。
檔案選擇器:showOpenFilePicker / showSaveFilePicker
開啟檔案
const [fileHandle] = await window.showOpenFilePicker({
types: [{
description: '圖片檔案',
accept: { 'image/*': ['.png', '.jpg', '.webp'] },
}],
multiple: false,
});
const file = await fileHandle.getFile();
const arrayBuffer = await file.arrayBuffer();
儲存檔案
const handle = await window.showSaveFilePicker({
suggestedName: 'compressed.webp',
types: [{
description: 'WebP 圖片',
accept: { 'image/webp': ['.webp'] },
}],
});
const writable = await handle.createWritable();
await writable.write(compressedBlob);
await writable.close();
FileSystemFileHandle:讀寫與權限
讀取檔案內容
const fileHandle: FileSystemFileHandle = handle;
const file = await fileHandle.getFile();
const text = await file.text();
const buffer = await file.arrayBuffer();
const stream = file.stream();
寫入檔案
async function writeFile(handle: FileSystemFileHandle, content: Blob | string | BufferSource) {
const writable = await handle.createWritable();
await writable.write(content);
await writable.close();
}
權限查詢與請求
async function verifyPermission(handle: FileSystemFileHandle, readWrite = true) {
const options: FileSystemHandlePermissionDescriptor = { mode: readWrite ? 'readwrite' : 'read' };
if ((await handle.queryPermission(options)) === 'granted') return true;
if ((await handle.requestPermission(options)) === 'granted') return true;
return false;
}
權限模型:首次透過 picker 取得的 handle 自動獲得讀權限。寫權限需要使用者明確授權(requestPermission 會跳出提示)。
目錄存取與遍歷
const dirHandle = await window.showDirectoryPicker();
for await (const [name, handle] of dirHandle.entries()) {
if (handle.kind === 'file') {
const file = await handle.getFile();
console.log(`檔案: ${name}, 大小: ${file.size}`);
} else if (handle.kind === 'directory') {
console.log(`目錄: ${name}`);
}
}
遞迴查找檔案
async function* findFiles(dirHandle: FileSystemDirectoryHandle, pattern: RegExp) {
for await (const [name, handle] of dirHandle.entries()) {
if (handle.kind === 'file' && pattern.test(name)) {
yield { name, handle };
} else if (handle.kind === 'directory') {
yield* findFiles(handle, pattern);
}
}
}
for await (const pdf of findFiles(dirHandle, /\.pdf$/)) {
console.log(pdf.name);
}
Origin Private File System (OPFS)
OPFS 是瀏覽器為每個來源提供的私有沙箱檔案系統,無需使用者授權:
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle('work-data.bin', { create: true });
const subDir = await opfsRoot.getDirectoryHandle('cache', { create: true });
await opfsRoot.removeEntry('work-data.bin');
OPFS vs IndexedDB
| 特性 | IndexedDB | OPFS |
|---|---|---|
| 儲存型別 | 結構化資料 | 檔案(二進位) |
| 讀寫方式 | 交易 + 序列化 | 直接檔案 I/O |
| 大檔案效能 | 差(序列化開銷) | 優秀 |
| 隨機存取 | ❌ | ✅(Access Handle) |
| 容量 | 同源限制 | 同源限制 |
| Worker 支援 | ✅ | ✅ |
Access Handle:高效能隨機讀寫
OPFS 檔案支援 createSyncAccessHandle(),提供同步、隨機位置的檔案讀寫——效能接近原生檔案系統:
const fileHandle = await opfsRoot.getFileHandle('large-data.bin', { create: true });
const accessHandle = await fileHandle.createSyncAccessHandle();
const writeBuffer = new Uint8Array([1, 2, 3, 4, 5]);
accessHandle.write(writeBuffer, { at: 0 });
const readBuffer = new Uint8Array(5);
accessHandle.read(readBuffer, { at: 0 });
const fileSize = accessHandle.getSize();
accessHandle.flush();
accessHandle.close();
關鍵優勢:Access Handle 是同步的,可在 Worker 中使用,無需 async/await,效能比 WritableStream 快 10-100 倍。
實戰:PDF 合併的檔案讀寫架構
工具庫 PDF 合併 利用 File System Access API 實現「開啟→處理→儲存」的完整流程:
async function mergePdfWorkflow() {
const handles = await window.showOpenFilePicker({
types: [{ accept: { 'application/pdf': ['.pdf'] } }],
multiple: true,
});
const buffers: ArrayBuffer[] = [];
for (const handle of handles) {
const file = await handle.getFile();
buffers.push(await file.arrayBuffer());
}
const mergedPdf = await mergePdfs(buffers);
const saveHandle = await window.showSaveFilePicker({
suggestedName: 'merged.pdf',
types: [{ accept: { 'application/pdf': ['.pdf'] } }],
});
const writable = await saveHandle.createWritable();
await writable.write(mergedPdf);
await writable.close();
}
持久化檔案句柄
透過 IndexedDB 儲存 handle,下次開啟時恢復存取:
async function saveHandle(key: string, handle: FileSystemFileHandle) {
const db = await openDB('file-handles', 1, {
upgrade(db) { db.createObjectStore('handles'); },
});
await db.put('handles', handle, key);
}
async function loadHandle(key: string) {
const db = await openDB('file-handles', 1);
const handle: FileSystemFileHandle = await db.get('handles', key);
if (await verifyPermission(handle, true)) {
return handle;
}
return null;
}
常見問題
File System Access API 的瀏覽器支援?
Chrome/Edge 86+ 完整支援。Firefox 和 Safari 未支援。需要做特性偵測和降級處理。
OPFS 的儲存容量限制?
OPFS 與 Cache API、IndexedDB 共享同源儲存配額(通常為可用磁碟空間的 50%+)。可透過 navigator.storage.estimate() 查詢。
Access Handle 能在主執行緒用嗎?
可以,但同步 I/O 會阻塞主執行緒。最佳實踐是在 Worker 中使用 Access Handle,避免阻塞 UI。
如何處理大檔案寫入?
使用 WritableStream 的分塊寫入,避免一次性載入整個檔案到記憶體:
const writable = await handle.createWritable();
for await (const chunk of readableStream) {
await writable.write(chunk);
}
await writable.close();
總結
File System Access API 讓瀏覽器具備了本地應用級的檔案讀寫能力。showOpenFilePicker/showSaveFilePicker 提供使用者授權的檔案存取,FileSystemFileHandle 管理讀寫權限,OPFS 提供無需授權的沙箱儲存,Access Handle 實現高效能同步隨機讀寫。這是建構瀏覽器端檔案處理工具的核心基礎設施。
本站提供瀏覽器本地工具,免註冊即可試用 →