發布日期:2020 年 7 月 27 日
瀏覽器處理檔案和目錄已有一段時間。 File API 可在網頁應用程式中表示檔案物件,並以程式輔助方式選取檔案物件及存取資料。但只要仔細觀察,就會發現閃閃發光的並非都是黃金。
傳統的檔案處理方式
開啟檔案。
你可以使用 <input type="file"> 元素開啟及讀取檔案。最簡單的檔案開啟方式如以下程式碼範例所示。input 物件會提供 FileList,以我們的範例來說,這只包含一個 File。File 是特定類型的 Blob,可用於 Blob 可用的任何環境。
const openFile = async () => {
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.addEventListener('change', () => {
resolve(input.files[0]);
});
input.click();
});
};
開啟目錄
如要開啟資料夾 (或目錄),可以設定 <input webkitdirectory> 屬性。除此之外,其他一切運作方式都與上述相同。
儘管名稱有供應商前置字元,webkitdirectory 不僅適用於 Chromium 和 WebKit 瀏覽器,也適用於以 EdgeHTML 為基礎的舊版 Edge 和 Firefox。
儲存及下載檔案
傳統上,儲存檔案的方式僅限於下載檔案,這項功能是透過 <a download> 屬性運作。指定 Blob 後,您可以將錨點的 href 屬性設為可從 URL.createObjectURL() 方法取得的 blob: 網址。
const saveFile = async (blob) => {
const a = document.createElement('a');
a.download = 'my-file.txt';
a.href = URL.createObjectURL(blob);
a.addEventListener('click', (e) => {
setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
});
a.click();
};
問題
下載方式的一大缺點是無法執行傳統的開啟→編輯→儲存流程,也就是無法覆寫原始檔案。因此,每當您「儲存」時,作業系統預設的「下載」資料夾中,都會出現原始檔案的副本。
File System Access API
File System Access API 可大幅簡化開啟和儲存這兩項作業。 還能實現真正的節省。也就是說,你可以選擇檔案儲存位置,以及是否要覆寫現有檔案。
開啟檔案。
使用 File System Access API 時,只要呼叫一次 window.showOpenFilePicker() 方法,即可開啟檔案。這個呼叫會傳回檔案控制代碼,您可以透過 getFile() 方法取得實際的 File。
const openFile = async () => {
try {
// Always returns an array.
const [handle] = await window.showOpenFilePicker();
return handle.getFile();
} catch (err) {
console.error(err.name, err.message);
}
};
開啟目錄
呼叫 window.showDirectoryPicker() 開啟目錄,讓目錄可在檔案對話方塊中選取。
儲存檔案
儲存檔案的方式同樣簡單。
您可以透過 createWritable() 從檔案控制代碼建立可寫入的串流,然後呼叫串流的 write() 方法寫入 Blob 資料,最後呼叫 close() 方法關閉串流。
const saveFile = async (blob) => {
try {
const handle = await window.showSaveFilePicker({
types: [{
accept: {
// Omitted
},
}],
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
return handle;
} catch (err) {
console.error(err.name, err.message);
}
};
隆重推出 browser-fs-access
File System Access API 雖然很實用,但尚未廣泛推出。
這就是我將 File System Access API 視為漸進式強化的原因。 因此,我希望在瀏覽器支援時使用該方法,否則就採用傳統做法;同時,絕不讓使用者下載不支援的 JavaScript 程式碼,以免造成不必要的負擔。browser-fs-access 程式庫就是我對這項挑戰的回應。
設計理念
由於 File System Access API 日後仍可能變更,因此 browser-fs-access API 並非以該 API 為模型。也就是說,這個程式庫不是 polyfill,而是 ponyfill。您可以 (靜態或動態) 專門匯入所需功能,盡可能縮小應用程式大小。
可用的方法包括 fileOpen()、directoryOpen() 和 fileSave()。在內部,程式庫會偵測是否支援 File System Access API,然後匯入對應的程式碼路徑。
使用程式庫
這三種方法都直覺易用。
您可以指定應用程式接受的 mimeTypes 或檔案 extensions,並設定 multiple 旗標,允許或禁止選取多個檔案或目錄。如需完整詳細資料,請參閱 browser-fs-access API 說明文件。程式碼範例顯示如何開啟及儲存圖片檔案。
// The imported methods will use the File
// System Access API or a fallback implementation.
import {
fileOpen,
directoryOpen,
fileSave,
} from 'https://unpkg.com/browser-fs-access';
(async () => {
// Open an image file.
const blob = await fileOpen({
mimeTypes: ['image/*'],
});
// Open multiple image files.
const blobs = await fileOpen({
mimeTypes: ['image/*'],
multiple: true,
});
// Open all files in a directory,
// recursively including subdirectories.
const blobsInDirectory = await directoryOpen({
recursive: true
});
// Save a file.
await fileSave(blob, {
fileName: 'Untitled.png',
});
})();
示範
您可以在 GitHub 示範中查看實際運作的程式碼。原始碼也同樣位於該處。
瀏覽器檔案系統存取程式庫
我在閒暇時會為名為 Excalidraw 的可安裝 PWA 貢獻一己之力,這款白板工具可讓您繪製手繪風格的圖表。這項服務完全採用回應式設計,無論是小型手機還是大螢幕電腦,都能順暢運作。也就是說,無論各種平台是否支援 File System Access API,應用程式都必須處理這些平台上的檔案。因此非常適合使用 browser-fs-access 程式庫。
舉例來說,我可以在 iPhone 上開始繪圖、將圖片儲存 (技術上來說是下載,因為 Safari 不支援 File System Access API) 到 iPhone 的「下載」資料夾、在桌機上開啟檔案 (從手機傳輸檔案後)、修改檔案,然後覆寫檔案或另存為新檔案。
實際程式碼範例
以下是 Excalidraw 實際使用的 browser-fs-access 範例。這段文字摘錄自「/src/data/json.ts」。特別值得注意的是,saveAsJSON() 方法會將檔案控制代碼或 null 傳遞至 browser-fs-access 的 fileSave() 方法,導致該方法在提供控制代碼時覆寫檔案,否則會儲存至新檔案。
export const saveAsJSON = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
fileHandle: any,
) => {
const serialized = serializeAsJSON(elements, appState);
const blob = new Blob([serialized], {
type: "application/json",
});
const name = `${appState.name}.excalidraw`;
(window as any).handle = await fileSave(
blob,
{
fileName: name,
description: "Excalidraw file",
extensions: ["excalidraw"],
},
fileHandle || null,
);
};
export const loadFromJSON = async () => {
const blob = await fileOpen({
description: "Excalidraw files",
extensions: ["json", "excalidraw"],
mimeTypes: ["application/json"],
});
return loadFromBlob(blob);
};
使用者介面注意事項
無論是在 Excalidraw 或應用程式中,UI 都應配合瀏覽器的支援情況調整。如果系統支援 File System Access API (if ('showOpenFilePicker' in window) {}),除了「儲存」按鈕外,您還可以顯示「另存新檔」按鈕。以下螢幕截圖顯示 iPhone 和 Chrome 電腦版上,Excalidraw 的回應式主要應用程式工具列差異。
請注意,iPhone 上沒有「另存為」按鈕。
Chrome 上的 Excalidraw 應用程式工具列,其中「儲存」和「另存為」按鈕已成為焦點。
結論
從技術上來說,所有新式瀏覽器都能處理系統檔案。 在支援 File System Access API 的瀏覽器上,您可以允許真正儲存及覆寫 (不只是下載) 檔案,並讓使用者在任何位置建立新檔案,藉此提升體驗,同時在不支援 File System Access API 的瀏覽器上維持功能運作。browser-fs-access 會處理漸進式強化功能的細微差異,並盡可能簡化程式碼,讓您輕鬆開發。
特別銘謝
Joe Medley 和 Kayce Basques 已審查過本文。感謝 Excalidraw 貢獻者參與專案,並審查我的提取要求。