browser-fs-access ライブラリを使用したファイルとディレクトリの読み取りと書き込み

ブラウザは長い間、ファイルやディレクトリを扱うことができました。 File API ウェブ アプリケーションでファイル オブジェクトを表す機能を提供します。 プログラムで選択してデータにアクセスできます でも、近くで見ると、キラキラしているものは金だけじゃない。

従来のファイル処理方法では

ファイルを開く

デベロッパーは、 <input type="file"> 要素です。 最もシンプルな形式では、ファイルを開くコードは次のコードサンプルのようになります。 input オブジェクトは FileList を提供します。 以下のケースでは、1 つだけ 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 に設定できます。URL は、 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() メソッドを 1 回呼び出すだけです。 この呼び出しはファイル ハンドルを返し、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 と同様に まだ広く普及していません

<ph type="x-smartling-placeholder">
</ph> File System Access API のブラウザ サポートの表。すべてのブラウザが「サポートなし」とマークされる“旗の後ろ”です <ph type="x-smartling-placeholder">
</ph> File System Access API のブラウザ サポートの表。 (出典)。
をご覧ください。

File System Access API は漸進的な機能強化として表示されているのはそのためです。 ブラウザが対応していれば そうでない場合は従来のアプローチを使用します サポートされていない JavaScript コードを不必要にダウンロードすると、ユーザーに罰則が科されません。 browser-fs-access ライブラリが私の答えです。

デザイン理念

File System Access API は今後も変更される可能性があるため、 browser-fs-access API はそれをモデル化していません。 つまり、ライブラリはポリフィルではありません。 ポニーフィルです。 アプリを可能な限り小さくするために必要な機能はすべて、(静的または動的に)排他的にインポートできます。 利用可能なメソッドは、 fileOpen() directoryOpen()fileSave()。 内部的には、このライブラリ機能は File System Access API がサポートされているかどうか、 対応するコードパスをインポートします

browser-fs-access ライブラリを使用する

この 3 つの方法は直感的に使用できます。 アプリが受け入れ可能な 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 ライブラリの実用化

自由時間には、仕事に少し インストール可能な PWA Excalidraw という 手描きのような図を簡単にスケッチできるホワイトボード ツールです。 応答性に優れ、小型のスマートフォンから大画面のパソコンにまで幅広く対応しています。 つまり、さまざまなプラットフォームでファイルを処理する必要がある File System Access API をサポートしているかどうかを確認できます。 そのため、browser-fs-access ライブラリには適しています。

たとえば、iPhone で描画を開始したり、 保存する(技術的にはダウンロードします。Safari は File System Access API をサポートしていないため) iPhone のダウンロード フォルダに移動して、デスクトップでファイルを開く(スマートフォンから転送した後に) 変更して上書きすることも、新しいファイルとして保存することもできます。

<ph type="x-smartling-placeholder">
</ph> iPhone に表示された Excalidraw の絵。
File System Access API はサポートされていないが、ファイルを [ダウンロード] フォルダに保存(ダウンロード)できる iPhone で Excalidraw 描画を開始する。
で確認できます。 <ph type="x-smartling-placeholder">
</ph> パソコン上の Chrome に表示された Excalidraw の改変した図形描画。
File System Access API がサポートされており、API 経由でファイルにアクセスできるデスクトップで Excalidraw 図形描画を開いて変更します。
で確認できます。 <ph type="x-smartling-placeholder">
</ph> 変更で元のファイルを上書きします。
元の Excalidraw 図形描画ファイルに変更を加えて、元のファイルを上書きします。ブラウザに、問題がないか確認するダイアログが表示されます。
で確認できます。 <ph type="x-smartling-placeholder">
</ph> 変更内容を新しい 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);
};

UI に関する考慮事項

Excalidraw のアプリでも、 UI をブラウザのサポート状況に適応させる必要があります。 File System Access API がサポートされている場合(if ('showOpenFilePicker' in window) {}) [Save] ボタンに加えて [Save As] ボタンを表示できます。 以下のスクリーンショットは、iPhone と Chrome デスクトップの Excalidraw のレスポンシブ メインアプリのツールバーの違いを示しています。 iPhone では [名前を付けて保存] ボタンがありません。

<ph type="x-smartling-placeholder">
</ph> [保存] が表示された iPhone の Excalidraw アプリ ツールバー] ボタンを離します。
[保存] ボタンのみ操作できる iPhone の Excalidraw アプリ ツールバー
で確認できます。 <ph type="x-smartling-placeholder">
</ph> [Save] が表示された Chrome デスクトップの Excalidraw アプリ ツールバー[名前を付けて保存]] ボタンを離します。
Chrome の Excalidraw アプリ ツールバー。[保存] とフォーカスされた [名前を付けて保存] ボタンが表示されている。

まとめ

システム ファイルの操作は、技術的にはすべての最新のブラウザで機能します。 File System Access API をサポートするブラウザでは、 (ダウンロードだけでなく)真の意味での保存と上書きが可能で、 ユーザーはどこでも新しいファイルを作成できます File System Access API をサポートしていないブラウザでも機能します。 browser-fs-access で簡単に プログレッシブ エンハンスメントの細かな部分に対応し、コードをできる限りシンプルにします。

謝辞

この記事は Joe Medley によってレビューされ、 Kayce BasquesExcalidraw への貢献に感謝します pull リクエストを確認してもらう必要があります。 ヒーロー画像: Ilya Pavlov、Unsplash より