讀取及寫入檔案和目錄

發布日期:2020 年 7 月 27 日

瀏覽器處理檔案和目錄已有一段時間。 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 屬性設為可從 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 的瀏覽器支援表格。所有瀏覽器都標示為「不支援」或「需要啟用旗標」。
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 的「下載」資料夾、在桌機上開啟檔案 (從手機傳輸檔案後)、修改檔案,然後覆寫檔案或另存為新檔案。

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 應用程式工具列,其中「儲存」和「另存為」按鈕已成為焦點。

結論

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

特別銘謝

Joe MedleyKayce Basques 已審查過本文。感謝 Excalidraw 貢獻者參與專案,並審查我的提取要求。