ブラウザは長い間、ファイルとディレクトリを処理してきました。File API は、ウェブ アプリケーションでファイル オブジェクトを表現する機能と、プログラムでファイル オブジェクトを選択してデータにアクセスする機能を提供します。しかし、よく見てみると、すべてが金でできているわけではありません。
ファイルの従来の処理方法
ファイルを開く
デベロッパーは、<input type="file">
要素を使用してファイルを開いて読み取ることができます。最も単純な形式では、ファイルを開く処理は次のコードサンプルのようになります。input
オブジェクトは FileList
を返します。次の例では、File
が 1 つだけ含まれています。File
は 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:
URL に設定できます。
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 は非常に優れていますが、まだ広く利用できるわけではありません。

これが、私が 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 ライブラリを使用する
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',
});
})();
デモ
上記のコードの動作は、GitHub のデモで確認できます。ソースコードも同様に利用できます。セキュリティ上の理由から、クロスオリジンのサブフレームでファイル選択ツールを表示することは許可されていないため、このデモをこの記事に埋め込むことはできません。
browser-fs-access ライブラリの実際の使用例
空いた時間には、Excalidraw というインストール可能な PWA に少しだけ貢献しています。これは、手書き風の図を簡単にスケッチできるホワイトボード ツールです。完全にレスポンシブで、小さなスマートフォンから大画面のパソコンまで、さまざまなデバイスで適切に動作します。つまり、ファイル システム アクセス API をサポートしているかどうかに関係なく、さまざまなプラットフォーム上のファイルを処理する必要があります。そのため、browser-fs-access ライブラリの候補として最適です。
たとえば、iPhone で描画を開始し、iPhone のダウンロード フォルダに保存(技術的には、Safari は File System Access API をサポートしていないため、ダウンロード)し、デスクトップでファイルを開き(スマートフォンから転送した後)、ファイルを変更して変更内容で上書きしたり、新しいファイルとして保存したりできます。




実際のコードサンプル
以下に、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) {}
)、[保存] ボタンに加えて [名前を付けて保存] ボタンを表示できます。以下のスクリーンショットは、iPhone と Chrome デスクトップでの Excalidraw のレスポンシブなメインアプリ ツールバーの違いを示しています。iPhone では [名前を付けて保存] ボタンが表示されないことに注意してください。
![iPhone の Excalidraw アプリのツールバー。[保存] ボタンのみが表示されている。](https://developer.chrome.com/static/docs/capabilities/browser-fs-access/image/excalidraw-app-toolbar-i-f6424de71f2f6.png?authuser=8&hl=ja)
![Chrome デスクトップの Excalidraw アプリのツールバーに、[保存] ボタンと [名前を付けて保存] ボタンが表示されている。](https://developer.chrome.com/static/docs/capabilities/browser-fs-access/image/excalidraw-app-toolbar-c-ce0b01551edeb.png?authuser=8&hl=ja)
まとめ
システム ファイルの操作は、技術的には最新のすべてのブラウザで可能です。File System Access API をサポートするブラウザでは、ファイルの真の保存と上書き(ダウンロードだけでなく)を可能にし、ユーザーが好きな場所に新しいファイルを作成できるようにすることで、ユーザー エクスペリエンスを向上させることができます。また、File System Access API をサポートしないブラウザでも機能が維持されます。browser-fs-access は、プログレッシブ エンハンスメントの微妙な処理を自動で行い、コードをできるだけシンプルにすることで、開発者の負担を軽減します。
謝辞
この記事は、Joe Medley と Kayce Basques によってレビューされました。プロジェクトに貢献し、私のプルリクエストをレビューしてくれた Excalidraw のコントリビューターに感謝します。ヒーロー画像: Unsplash の Ilya Pavlov