使用 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 可大幅簡化開啟和儲存這兩項作業。此外,這項功能也支援真正儲存,也就是說,你不僅可以選擇檔案儲存位置,還能覆寫現有檔案。

開啟檔案

使用 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 並非以該 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',
  });
})();

示範

您可以在 GitHub 的示範中查看上述程式碼的實際運作情形。原始碼也同樣可在此處取得。 基於安全考量,系統不允許跨來源子影格顯示檔案挑選器,因此本文無法嵌入示範。

瀏覽器 fs-access 程式庫

在空閒時間,我會為名為 Excalidraw可安裝 PWA 貢獻一小部分心力。這項白板工具可讓您輕鬆繪製手繪風格的圖表。這項功能完全採用回應式設計,可順暢運作於各種裝置,包括小型手機和大型電腦螢幕。也就是說,無論各種平台是否支援 File System Access API,應用程式都必須處理這些平台上的檔案。因此非常適合使用 browser-fs-access 程式庫。

舉例來說,我可以在 iPhone 上開始繪圖,然後將檔案儲存 (技術上來說是下載,因為 Safari 不支援 File System Access API) 到 iPhone 的「下載」資料夾,接著在桌機上開啟檔案 (從手機傳輸檔案後),修改檔案並覆寫變更,甚至另存為新檔案。

iPhone 上的 Excalidraw 繪圖。
在不支援 File System Access API 的 iPhone 上開始繪製 Excalidraw 圖案,但可將檔案儲存 (下載) 到「下載」資料夾。
電腦版 Chrome 上經過修改的 Excalidraw 繪圖。
在支援 File System Access API 的桌機上開啟及修改 Excalidraw 繪圖,因此可透過 API 存取檔案。
以修改內容覆寫原始檔案。
以對原始 Excalidraw 繪圖檔案所做的修改覆寫原始檔案。瀏覽器會顯示對話方塊,詢問我是否同意。
將修改內容儲存到新的 Excalidraw 繪圖檔案。
將修改內容儲存到新的 Excalidraw 檔案。原始檔案不會受到影響。

實際程式碼範例

以下是 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 上沒有「另存為」按鈕。

iPhone 上的 Excalidraw 應用程式工具列,只有「儲存」按鈕。
iPhone 上的 Excalidraw 應用程式工具列,只有「儲存」按鈕。
Chrome 電腦版上的 Excalidraw 應用程式工具列,顯示「儲存」和「另存新檔」按鈕。 Chrome 上的 Excalidraw 應用程式工具列,其中「儲存」和「另存為」按鈕已成為焦點。

結論

從技術上來說,所有新式瀏覽器都能處理系統檔案。 在支援 File System Access API 的瀏覽器上,您可以允許真正儲存及覆寫 (不只是下載) 檔案,並讓使用者在任何位置建立新檔案,藉此提升體驗,同時在不支援 File System Access API 的瀏覽器上維持功能。browser-fs-access 可處理漸進式強化功能中的細微差異,並盡可能簡化程式碼,讓您輕鬆開發。

特別銘謝

本文由 Joe MedleyKayce Basques 審查。感謝 Excalidraw 貢獻者參與專案,並審查我的提取要求。主頁橫幅,由 Unsplash 的 Ilya Pavlov 提供。