瀏覽器處理檔案和目錄已久。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
」,但「webkitdirectory
」不僅可以在 Chromium 和 WebKit 瀏覽器中使用,也能在舊版 EdgeHTML 架構及 Firefox 中使用。
儲存中 (而非下載) 檔案
傳統上,您只能下載檔案來儲存檔案,這項功能是透過 <a download>
屬性運作。在 Blob 中,您可以將錨點的 href
屬性設為 blob:
網址,您可以透過 URL.createObjectURL()
方法取得該網址。
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
檔案系統存取權 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 之後不會再建立模型。也就是說,該程式庫不是 polyfill,而是 ponyfill。您可以 (靜態或動態) 單獨匯入所需功能,盡可能讓應用程式盡可能縮小。可用的方法包括 fileOpen()
、directoryOpen()
和 fileSave()
。在內部,程式庫功能會偵測是否支援 File System Access API,然後匯入對應的程式碼路徑。
使用 Browser-fs-access 程式庫
這三種方法都很直覺易用。您可以指定應用程式接受的 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',
});
})();
示範
您可以在 Glitch 的示範中查看上述程式碼的實際運作情形。其原始碼同樣可用。基於安全性考量,跨來源子框架不得顯示檔案挑選器,因此無法將該示範嵌入本文。
實際使用的 browser-fs-access 程式庫
在空閒時間,我會為名為 Excalidraw 的可安裝 PWA 做出一點貢獻,這個白板工具可讓您輕鬆繪製手繪風格的圖表。這項功能完全回應式,可在各種裝置上順利運作,從小型手機到大螢幕電腦皆適用。也就是說,無論是否支援 File System Access API,它都需要處理所有不同平台上的檔案。因此很適合使用 Browser-fs-access 程式庫。
舉例來說,我可以在 iPhone 上開始繪圖,然後將圖片儲存 (技術上來說是下載,因為 Safari 不支援 File System Access API) 到 iPhone 的「下載」資料夾,再從手機傳輸到電腦上開啟檔案,修改檔案並覆寫檔案,甚至將檔案儲存為新檔案。
實際程式碼範例
以下是瀏覽器-fs-access 實際用於 Excalidraw 的範例。這段摘錄內容取自 /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) {}
),除了「Save」按鈕之外,您還可以顯示「Save As」按鈕。下方螢幕截圖顯示 iPhone 和 Chrome 電腦版上,Excalidraw 主應用程式工具列的回應式差異。請注意,iPhone 上缺少「Save As」按鈕。
結論
從技術層面來說,所有新式瀏覽器都能處理系統檔案。在支援 File System Access API 的瀏覽器上,您可以允許真正的檔案儲存和覆寫 (而非僅下載),並讓使用者在任何地方建立新檔案,藉此改善使用體驗,同時在不支援 File System Access API 的瀏覽器上保留功能。browser-fs-access 會處理漸進式增強功能的細微差異,並盡可能簡化程式碼,讓您更輕鬆地處理這些問題。
特別銘謝
本文由 Joe Medley 和 Kayce Basques 審查。感謝 Excalidraw 貢獻者對專案工作,以及查看我的提取要求。主頁橫幅:Unsplash 上的 Ilya Pavlov 提供。