使用 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 不僅適用於 Chromium 和 WebKit 瀏覽器,也適用於舊版 EdgeHTML 式 Edge 以及 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

File System Access API 可大幅簡化開啟和儲存作業,這樣做也能啟用「True 儲存」,也就是說,您不僅可以選擇檔案儲存位置,還能覆寫現有檔案。

開啟檔案

使用 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 日後可能還會變更,因此系統不會為瀏覽器-fs-access API 建立模型。換句話說,程式庫不是 polyfill,而是匿名填入。您可以只以靜態或動態方式匯入任何您需要的功能,盡可能縮減應用程式的大小。可用的方法為適當命名的 fileOpen()directoryOpen()fileSave()。在內部,程式庫功能會偵測是否支援 File System Access API,然後匯入對應的程式碼路徑。

使用 Browser-fs-access 程式庫

這三種方法相當直覺好用。 您可以指定應用程式接受的 mimeTypesextensions 檔案,並設定 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 的示範中查看上述程式碼的實際運作情形。其原始碼同樣可用。基於安全考量,跨來源子頁框無法顯示檔案選擇器,因此無法將示範嵌入本文。

野外的瀏覽器-fs-存取程式庫

閒暇時間,我為名為「Excalidraw」可安裝 PWA 貢獻一點點,這款白板工具可讓您以手繪圖的方式輕鬆繪製圖表。它完全回應能力,能在各種裝置 (包括小手機和配備大螢幕的電腦) 上順利運作。 這表示它需要處理所有平台上的檔案,不論檔案是否支援 File System Access API。因此非常適合用於 Browser-fs-access 程式庫。

舉例來說,我可以在 iPhone 上開始繪圖,然後將繪圖儲存 (技術上:下載,因為 Safari 不支援 File System Access API) 至 iPhone「Downloads」資料夾、在桌面開啟該檔案 (從手機轉移檔案之後)、修改並覆寫檔案,甚至另存新檔。

iPhone 上的 Excalidraw 繪圖。
在不支援 File System Access API 的情況下啟動 Excalidraw 繪圖,但支援儲存 (下載) 檔案到「下載」資料夾中的 iPhone。
桌面版 Chrome 中修改過的 Excalidraw 繪圖。
在支援 File System Access API 的電腦上開啟及修改 Excalidraw 繪圖,藉此透過 API 存取檔案。
修改修改後的原始檔案。
修改原始 EXcalidraw 繪圖檔案,覆寫原始檔案。瀏覽器會顯示對話方塊,詢問我這一切是否正常。
將修改內容儲存至新的 Excalidraw 繪圖檔案。
將修改內容儲存至新的 Excalidraw 檔案。原始檔案則維持不變。

實際程式碼範例

以下提供在 Excalidraw 中使用瀏覽器-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) {}),您也可以顯示「Save As」按鈕以及「Save As」按鈕。下方螢幕截圖顯示 iPhone 和 Chrome 電腦版的 Excalidraw 回應式主要應用程式工具列之間的差異。 請注意,iPhone 上沒有「另存新檔」按鈕。

在 iPhone 上透過「儲存」按鈕執行 Excalidraw 應用程式工具列。
只要在 iPhone 上提供「儲存」按鈕,就能執行擴充應用程式工具列。
Chrome 電腦版的 Excalidraw 應用程式工具列,上有「儲存」和「另存新檔」按鈕。
Chrome 的 Excalidraw 應用程式工具列,提供「儲存」和聚焦狀態的「另存新檔」按鈕。

結論

技術性檔案可以在所有新版瀏覽器中使用。在支援 File System Access API 的瀏覽器中,您可以允許真實儲存和覆寫 (不只是下載) 檔案,並且允許使用者在任何位置建立新檔案,同時在不支援 File System Access API 的瀏覽器上保持運作,藉此提供更優質的使用體驗。browser-fs-access 可處理逐一強化的強化功能,並盡可能簡化程式碼,讓您的生活更加輕鬆。

特別銘謝

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