File System Access API 實戰:瀏覽器端本地檔案讀寫架構

技术架构(更新於 2026年6月15日)

瀏覽器檔案存取的演進

方案 讀寫能力 持久化 權限模型 效能
<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 實現高效能同步隨機讀寫。這是建構瀏覽器端檔案處理工具的核心基礎設施。

本站提供瀏覽器本地工具,免註冊即可試用 →

#File System Access#OPFS#文件句柄#本地文件#读写权限