使用 Browser-fs-access 程式庫讀取及寫入檔案和目錄

瀏覽器處理檔案和目錄已久。File API 提供可在網路應用程式中代表檔案物件的功能,以及以程式輔助方式選取檔案物件和存取其資料的功能。不過,仔細一看,你就會發現,並非所有閃亮的東西都是黃金。

處理檔案的傳統方式

開啟檔案

開發人員可以透過 <input type="file"> 元素開啟及讀取檔案。最簡單的開啟檔案方式,如以下程式碼範例所示。input 物件會提供 FileList,在下方範例中,這個物件只包含一個 FileFile 是特定類型的 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 的瀏覽器支援表格。所有瀏覽器都標示為「不支援」或「待定」。
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 的「下載」資料夾,再從手機傳輸到電腦上開啟檔案,修改檔案並覆寫檔案,甚至將檔案儲存為新檔案。

iPhone 上的 Excalidraw 繪圖。
在 iPhone 上啟動 Excalidraw 繪圖作業,但該裝置不支援檔案系統存取 API,但可將檔案儲存 (下載) 至「下載」資料夾。
在電腦上的 Chrome 中,經過修改的 Excalidraw 繪圖。
在支援 File System Access API 的桌面上開啟及修改 Excalidraw 圖表,這樣就能透過 API 存取檔案。
覆寫原始檔案並加入修改內容。
使用對原始 Excalidraw 繪圖檔案所做的修改,覆寫原始檔案。瀏覽器會顯示對話方塊,詢問我是否同意。
將修改內容儲存至新的 Excalidraw 繪圖檔案。
將修改內容儲存至新的 Excalidraw 檔案。原始檔案則維持不變。

實際程式碼範例

以下是瀏覽器-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」按鈕。

iPhone 上的 Excalidraw 應用程式工具列,只有「Save」按鈕。
iPhone 上的 Excalidraw 應用程式工具列,僅有「Save」按鈕。
Chrome 電腦版上的 Excalidraw 應用程式工具列,其中有「Save」和「Save As」按鈕。
在 Chrome 中執行應用程式工具列,其中包含「儲存」和聚焦的「另存新檔」按鈕。

結論

從技術層面來說,所有新式瀏覽器都能處理系統檔案。在支援 File System Access API 的瀏覽器上,您可以允許真正的檔案儲存和覆寫 (而非僅下載),並讓使用者在任何地方建立新檔案,藉此改善使用體驗,同時在不支援 File System Access API 的瀏覽器上保留功能。browser-fs-access 會處理漸進式增強功能的細微差異,並盡可能簡化程式碼,讓您更輕鬆地處理這些問題。

特別銘謝

本文由 Joe MedleyKayce Basques 審查。感謝 Excalidraw 貢獻者對專案工作,以及查看我的提取要求。主頁橫幅:Unsplash 上的 Ilya Pavlov 提供。