在瀏覽器直接讀寫本機檔案:File System Access API
傳統網頁要處理檔案,通常依賴 <input type="file"> 讓使用者選擇檔案,讀取後上傳或處理,處理完再透過 <a> 標籤下載。這種流程是「唯讀且一次性」的——你無法直接將修改寫回原本的檔案。
File System Access API 改變了這一切。它賦予網頁應用 (Web App) 直接讀寫使用者本機檔案系統的能力(需使用者明確授權),實現了如同 VS Code for Web 或 Photoshop Web 版的流暢體驗。
WARNING
相容性警告: 此 API 目前主要支援 Desktop Chromium 瀏覽器(Chrome, Edge, Opera)。 Safari 與 Firefox 尚未支援(Firefox 僅部分支援 Origin Private File System)。
一、 核心概念:FileHandle
API 的核心是 Handle(控制代碼),代表了一個檔案或資料夾的參照:
- FileSystemFileHandle: 代表一個檔案。
- FileSystemDirectoryHandle: 代表一個資料夾。
透過這些 Handle,我們可以重複讀取或寫入,而不需使用者每次都重新選擇檔案。
二、 封裝 useFileSystem Composable
我們將封裝一個 useFileSystem,提供開啟、儲存與另存新檔的功能,並管理目前的 FileHandle。
2.1 介面定義
export interface UseFileSystemReturn {
isSupported: boolean;
fileHandle: Ref<FileSystemFileHandle | null>;
fileData: Ref<string | null>;
openFile: (options?: OpenFilePickerOptions) => Promise<void>;
saveFile: (content: string) => Promise<void>;
saveAs: (content: string, options?: SaveFilePickerOptions) => Promise<void>;
}2.2 完整實作
// composables/useFileSystem.ts
import { ref } from 'vue';
export function useFileSystem() {
const isSupported = typeof window !== 'undefined' && 'showOpenFilePicker' in window;
const fileHandle = ref<FileSystemFileHandle | null>(null);
const fileData = ref<string | null>(null);
/**
* 開啟檔案
*/
const openFile = async (options: OpenFilePickerOptions = {}) => {
if (!isSupported) return;
try {
// 1. 顯示開啟檔案對話框 (回傳陣列,因為可選多檔)
const handles = await window.showOpenFilePicker(options);
const handle = handles[0]; // 我們只取第一個
// 2. 獲取 File 物件並讀取內容
const file = await handle.getFile();
const text = await file.text();
// 3. 更新狀態
fileHandle.value = handle;
fileData.value = text;
} catch (err: any) {
if (err.name !== 'AbortError') {
console.error('開啟檔案失敗:', err);
}
}
};
/**
* 儲存檔案 (寫回原檔)
*/
const saveFile = async (content: string) => {
if (!isSupported) return;
try {
// 如果沒有 handle,則轉為「另存新檔」
if (!fileHandle.value) {
return await saveAs(content);
}
// 1. 建立可寫入的 stream
const writable = await fileHandle.value.createWritable();
// 2. 寫入內容
await writable.write(content);
// 3. 關閉 stream (重要!這時才會真正寫入檔案)
await writable.close();
fileData.value = content;
} catch (err: any) {
console.error('儲存檔案失敗:', err);
}
};
/**
* 另存新檔
*/
const saveAs = async (content: string, options: SaveFilePickerOptions = {}) => {
if (!isSupported) return;
try {
// 1. 顯示儲存對話框,取得新的 handle
const newHandle = await window.showSaveFilePicker(options);
// 2. 寫入內容
const writable = await newHandle.createWritable();
await writable.write(content);
await writable.close();
// 3. 更新目前的 handle 與內容
fileHandle.value = newHandle;
fileData.value = content;
} catch (err: any) {
if (err.name !== 'AbortError') {
console.error('另存新檔失敗:', err);
}
}
};
return {
isSupported,
fileHandle,
fileData,
openFile,
saveFile,
saveAs
};
}三、 實戰範例:純網頁文字編輯器
這是一個具備「開啟」、「儲存」功能的簡易記事本。
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useFileSystem } from '@/composables/useFileSystem';
const { isSupported, fileHandle, fileData, openFile, saveFile, saveAs } = useFileSystem();
const content = ref('');
// 當 fileData 改變(例如開啟了新檔案),同步到編輯器內容
watch(fileData, (newVal) => {
if (newVal) content.value = newVal;
});
const handleOpen = () => {
openFile({
types: [{
description: 'Text Files',
accept: { 'text/plain': ['.txt', '.md', '.json'] },
}],
});
};
const handleSave = () => saveFile(content.value);
const handleSaveAs = () => saveAs(content.value);
</script>
<template>
<div v-if="!isSupported" class="error">
您的瀏覽器不支援 File System Access API (請使用 Chrome/Edge)。
</div>
<div v-else class="editor-container">
<div class="toolbar">
<button @click="handleOpen">📂 開啟舊檔</button>
<button @click="handleSave" :disabled="!fileHandle">💾 儲存</button>
<button @click="handleSaveAs">📑 另存新檔</button>
<span v-if="fileHandle" class="file-name">
目前編輯: {{ fileHandle.name }}
</span>
</div>
<textarea v-model="content" class="editor" />
</div>
</template>
<style scoped>
.toolbar {
padding: 10px;
background: #f0f0f0;
border-bottom: 1px solid #ccc;
display: flex;
gap: 10px;
align-items: center;
}
.editor {
width: 100%;
height: 400px;
padding: 15px;
font-family: monospace;
font-size: 14px;
border: none;
resize: vertical;
}
.file-name {
margin-left: auto;
font-size: 0.9em;
color: #666;
}
.error {
color: red;
padding: 20px;
text-align: center;
background: #fff0f0;
}
</style>四、 權限管理細節
4.1 權限提示
當你第一次呼叫 openFile 或 saveFile 時,瀏覽器會詢問使用者是否允許。
- 讀取權限:在
showOpenFilePicker選取檔案時自動授權。 - 寫入權限:在
createWritable時,若尚未授權寫入,瀏覽器會再次彈窗確認。
4.2 持久化權限 (Persisting Permissions)
目前的 File Handle 在頁面重新整理後就會失效(權限遺失)。若要持久化(例如:重新開啟網頁時能記住上次開啟的檔案),可以將 Handle 儲存在 IndexedDB 中。
不過,即使從 IndexedDB 取回 Handle,下次使用時瀏覽器仍會要求使用者再次點擊「驗證權限」,以確保安全性。
總結
File System Access API 大幅縮小了 Web App 與 Native App 之間的差距。它非常適合生產力工具(編輯器、繪圖軟體)使用。雖然目前支援度有限,但在 Electron 或 PWA 的場景中,它已經是不可或缺的重磅功能。