はじめに

私たちを取り巻くWeb技術は、もはや社会的なインフラとしてめまぐるしく進化しています。HTMLやCSSはもちろんのこと、JavaScriptやライブラリ、フレームワークなど、それぞれがニーズにキャッチアップする形で、機能強化を繰り返しています。その中でも、Web技術の中核に位置するにもかかわらず、意外と見過ごされがちなのがHTMLの進化です。本連載は、このHTMLと関連するJavaScript APIにフォーカスして、その新機能を手軽に試していただこうというものです。理解も利用もたやすいHTMLなので、ライトな気持ちで「こんなことができるようになったのか」を感じていただきます。

[NOTE]サンプルについて
本記事の配布サンプルは、以下のURLから入手できます。新機能を試していくので、ブラウザは最新である方がよいでしょう。本記事のサンプルは、執筆時点で最新のChromeで動作することを確認しています。後述する理由で、Firefox、Safariなどでは動作しないので注意してください。
https://github.com/wateryinhare62/mynavi_html/

連載第10回の目的

この回では、ローカルのファイルシステムにアクセスするFile System Access APIを試します。手始めとして、ダイアログボックスで選択したファイルを読み込んで表示したり、編集して保存したりする例を紹介します。

File System Access API

File System Access APIは、OSのファイルシステムを操作するJavaScript APIです。JavaScriptには、もともとファイルシステムの操作のためのFile System APIがあり、Living Standardとして標準化されています。File System Access APIは、このFile System APIを拡張したものであり、機能的にはそれらを含みます。このため、本記事では両者をほぼ同じものとして扱っています。
File System Access APIを使うことで、ファイルやディレクトリのユーザーによる選択、ファイルの読み込み、書き出し、ディレクトリの読み込み、ファイルやディレクトリの作成や削除といった操作が可能になります。これらを使うと、例えば以下のような活用が考えられるでしょう。

  • ブラウザで動く本格的なテキスト/コードエディタ
  • 画像や動画のローカル編集ツール
  • ブラウザベースの統合開発環境(IDE)
  • ローカルの専用データを使う家計簿やメモアプリ

実際にファイルシステムにアクセスしたり、ファイルとの入出力を行うには、いくつかのAPI群を組み合わせて使います。以下は、本記事で使っていくAPIです。

  • File System API:端末のファイルシステムにアクセスし、ファイルの読み取り、書き込み、管理機能を提供
  • File System Access API:File System APIを拡張して、ファイルやディレクトリ選択ダイアログを通したユーザー操作機能を提供
  • File API:ファイルについての情報や入出力機能を提供

File System Access APIはFile System APIの機能拡張としての位置付けであり、実際にはFile System APIを中心に、必要に応じてFile APIを使っていきます。File System Access APIについては、一部のメソッドがFirefox、Safari、スマートフォン用ブラウザで使えないことから、Limited Availableという位置付けです。Chrome、EdgeなどのChronium系ブラウザでは、ほとんどの機能を問題なく利用できます。

  • 図1:完成サンプル

    図1:完成サンプル

File System Access APIが利用できるか判定する

File System Access APIに限らず、その機能が利用できる環境であるのかの判定は必須です。以降のサンプルでは、ボタン押下などのイベントハンドラ内で判定関数を呼び出し、利用できる環境なのかをチェックしています(それぞれのサンプルではこの関数の定義と呼び出しコードは省略)。


// File System Access APIが利用できるか判定する関数
function canUseFileSystem() {
  return ('showOpenFilePicker' in window) && window.isSecureContext;
}
…略…
// イベントハンドラ内で判定結果をページに反映する
readButton.addEventListener('click', async () => {
  if (!canUseClipboard()) {
    console.error('File System Access APIが使えないかセキュアコンテキストではありません。');
    return;
  }
  …略…
}

canUseFileSystem関数で使われている2つの式は、それぞれ以下のような意味です。

  • 'showOpenFilePicker' in window:グローバルオブジェクトwindowが、showOpenFilePickerという名前のオブジェクトを持つか(showOpenFilePickerメソッドが呼び出せるか)
  • window.isSecureContext:当該ウィンドウが、保護されたコンテキスト(セキュアコンテキスト)を満たしているか

保護されたコンテキストとは、認証と機密性の最低基準を満たしているウィンドウやワーカーをいいます。具体的にはhttpsなどの安全な接続である、必要な場合には認証されている、ファイルシステムのアクセスにユーザーの許可を得ている、などです。File System Access APIを含む多くのAPIや機能は、保護されたコンテキストでのみアクセス可能となっています。

サンプルに共通の構造

File System Access APIのメソッドは、呼び出し結果をPromiseオブジェクトで返します。呼び出しの終了/失敗のハンドリングにはasync/await構文を使用しています。これを受けて、本記事のサンプルは以下のような共通の構造となっています。

  1. イベントハンドラの関数はasyncな関数として定義する
  2. メソッド呼び出しをtry~catch構文で処理する
  3. メソッドはawait付きで呼び出す
  4. メソッド呼び出し失敗時の処理はcatchブロックに記述する

ファイルの読み込みと表示

まずは簡単なサンプルから始めます。ファイルピッカーによってファイルを選択し、そのファイルの種類を判定、読み出したテキスト/画像をtextarea/canvas要素に設定します。簡単なファイルビューワーとして機能するサンプルです。

ファイル読み込みと表示の流れ

ファイルを読み込み表示するまでの手順は以下の通りです。

  1. File System Access APIのshowOpenFilePickerメソッドでファイルを選択
  2. 取得したファイルハンドル(FileSystemFileHandleオブジェクト)からファイルオブジェクト(Fileオブジェクト)を取得
  3. ファイルオブジェクトのtypeプロパティで形式を判定
  4. テキスト形式であればファイルオブジェクトのtextメソッドでテキストを取得、textarea要素に設定
  5. 画像形式であればファイルオブジェクトからimg要素を生成、canvas要素に描画

リスト1は、ファイルを読み込むためのボタン、ファイル名表示のためのspan要素、テキストを収納するtextarea要素、画像を描画するためのcanvas要素を配置したHTMLです。コンテンツのための要素は、初期状態では不可視としています。

リスト1:file_read.html


<p><button id="readButton">ファイルを読み込む</button>&nbsp;
  <span id="filename"></span></p>
<textarea id="textContent" rows="20" cols="80" hidden></textarea>
<canvas id="imageContent" width="500" height="500" hidden></canvas>

[NOTE]CSSファイルとJavaScriptファイル
サンプルでは見た目を調整するためにCSSを適用していますが、本記事の内容と直接の関係はないので、CSSファイルの内容と読み込みのためのlink要素については掲載を割愛しています。また、JavaScriptコードの読み込みのscript要素も掲載を割愛しています。いずれも全容は配布サンプルを参照してください。

リスト2は、ボタンクリックで呼び出されるイベントハンドラのJavaScriptコードです。

リスト2:read_file.js


document.getElementById('readButton').addEventListener('click', async () => {
  …略…
  try {
    (1)テキストファイルを選択してファイルハンドルを取得する
    const [fileHandle] = await window.showOpenFilePicker({
      types: [
        {
          description: "Text Files",
          accept: {
            "text/*": [".txt", ".md", ".csv"],
          },
        },
        {
          description: "Image Files",
          accept: {
            "image/*": [".png", ".jpg", ".jpeg", ".gif"],
          },
        }
      ],
      excludeAcceptAllOption: true,
      multiple: false,
      id: "read",
      startIn: "documents",
    });
    (2)ファイルハンドルからFileオブジェクトを取得してファイル名を設定する
    const file = await fileHandle.getFile();
    document.getElementById('filename').textContent = file.name;
    (3)Fileオブジェクトがテキストであればそれを読み込みtextarea要素に設定する
    if (file.type.startsWith('text/')) {
      const contents = await file.text();
      textContent.value = contents;
      textContent.style.display = 'block';
      imageContent.style.display = 'none';
    (4)Fileオブジェクトが画像であればそれをcanvas要素に描画する
    } else if (file.type.startsWith('image/')) {
      const img = new Image();
      img.onload = () => {
        const targetCanvas = imageContent;
        const targetContext = targetCanvas.getContext('2d');
        targetCanvas.width = img.width;
        targetCanvas.height = img.height;
        targetContext.drawImage(img, 0, 0);
        textContent.style.display = 'none';
        imageContent.style.display = 'block';
      };
      img.src = URL.createObjectURL(file);
    }
  } catch (err) {
    console.error('ファイルの読み込みに失敗しました。', err);
  }
});

このサンプルは、showOpenFilePickerメソッドで選択したファイルの種類を判別し、それに応じてtextarea要素に設定、あるいはcanvas要素に描画するというものです。以降のサンプルにも通じる基本が詰まっているので、順番に説明します。

(1)テキストファイルを選択してファイルハンドルを取得する

showOpenFilePickerメソッドでユーザーにファイルを選択させます。ユーザーの選択を以て、そのファイルへのアクセスが許可されたと見なします。showOpenFilePickerメソッドは、ファイルハンドルを配列で返します。ただし、既定では選択できるファイルは1個なので、分割代入でファイルハンドルを単独で取得しています。リストのようにshowOpenFilePickerメソッドにはいくつかのオプションが指定でき、選択操作をカスタマイズできます。リスト2では、以下のようにオプションを指定しています。

  1. 選択対象のファイルの種類はテキストファイル(text/*)か画像ファイル(image/*)
  2. 拡張子フィルタに「すべてのファイル」を含まない
  3. 単一ファイルの選択とする
  4. ファイルピッカーごとにフォルダー位置を記憶
  5. 初期フォルダーはドキュメントフォルダー

個々のオプションの説明は、以下の通りです。

・typesオプション:選択可能なファイルの種類を指定
typesオプションにより、選択できるファイルの種類を指定できます。具体的には、ファイルピッカーの拡張子フィルタに反映されます。descriptionキーには拡張子フィルタに表示される説明テキスト(ここでは「Text Files」と「Image Files」)、acceptキーにはMIMEタイプ(text/*、image/*)と拡張子リスト(".txt", ".png"など)のペアを指定します。このように、typesオプションは配列として複数指定可能です。

・excludeAcceptAllOptionオプション:拡張子フィルタから「すべてのファイル」を除外
「すべてのファイル(*.*)」に相当する拡張子フィルタを非表示にします。既定値はfalseですが、リスト2ではtrueとして対象ファイル以外の選択肢が現れないようにしています。目的のファイル以外は選択させたくないという場合にはtrueに設定します。

・multipleオプション:複数ファイルを選択可能に
複数ファイルを選択可能にします。既定値はfalseで、リスト2でもfalseのままとしています。trueにすれば複数のファイルを選択していますが、分割代入で結果を受け取っているので、何個選択しても利用するのは最初の1個のみです。

・idオプション:ファイルピッカーごとにフォルダ位置を記憶
ファイルピッカーでファイルを選択して抜けるときに、そのフォルダ位置を記憶する識別名です。通常、ブラウザはフォルダ位置を記憶しますが、idオプションを指定すると、異なるファイルピッカー間でそれぞれフォルダ位置を記憶できます。複数のファイルピッカーを使っており、それぞれでフォルダ位置を記憶したい場合には便利です。

・startInオプション:初期フォルダを指定
idオプションが指定されない場合の初期フォルダを指定します。以下の予約済みのフォルダ(WellKnownDirectory)か、ディレクトリに相当するFileSystemHandleオブジェクトを指定しますが、通常は予約済みのフォルダを指定することが多いでしょう。リスト2では"documents"、すなわちドキュメントフォルダを指定しています。

"desktop"、"documents"、"downloads"、"music"、"pictures"、"videos"

(2)ファイルハンドルからFileオブジェクトを取得してファイル名を表示

ファイルハンドルからgetFileメソッドでFileオブジェクトを取得します。ファイルハンドルでは直接の読み書きができないので、File APIのFileオブジェクトをgetFileメソッドで取得する必要があります。Fileオブジェクトからはnameプロパティでファイル名を取得できるので、それをファイル名表示用のspan要素に設定しています。

(3)Fileオブジェクトがテキストであればそれをtextarea要素に設定する

Fileオブジェクトがテキスト形式であれば、テキストを読み込みtextarea要素に設定します。Fileオブジェクトのtextメソッドを使うと、ファイルの中身がプレーンテキストであるとして読み込みを実行します。なお、同種のメソッドには以下に挙げるものがあり、対応する形式でファイルの内容が返されます。

  • arrayBuffer:バイナリデータとしてArrayBufferで返す
  • bytes:バイナリデータとしてUint8Arrayを返す
  • slice:指定された範囲のデータを含む新しいBlobオブジェクトを返す
  • stream:内容を読み込むために使用できるReadableStreamオブジェクトを返す
  • text:UTF-8形式の文字列を返す

なお、形式はtypeプロパティを参照して、「text/」で始まっているかで判定しています。このへんの流れは第9回の記事も参考にしてください。

(4)Fileオブジェクトから画像であればそれをcanvas要素に描画する

Fileオブジェクトが画像形式であれば、その画像をcanvas要素に描画します。FileオブジェクトはBlobオブジェクトを継承しているので、画像のデータからimg要素を生成してcanvas要素に描画できます。ここも、第9回で紹介した手順とほぼ同様なので、そちらの記事を参考にしてください。

ブラウザ上でボタンをクリックし、適当なテキストファイルか画像ファイルを選択すると、それに応じて表示が行われることが確認できます(図2)。

  • 図2:ファイルの読み込みと表示

    図2:ファイルの読み込みと表示

ファイルへの書き込み

ファイルへの書き込みの例として、前節のサンプルをテキストのみに特化させて、ファイルの書き込みに対応させるサンプルを紹介します。編集したファイルを書き戻すという、簡単なテキストエディタ的なサンプルです。

ファイルの上書き保存

ファイル読み込みの際に取得したファイルハンドルを保存しておけば、それをそのまま使って上書き保存できます。リスト3は、リスト1に上書き保存のボタンを追加したHTMLです。

リスト3:file_write.html


<p><button id="openButton">ファイルを読み込む</button>&nbsp;
  <span id="filename"></span></p>
<p><button id="saveButton">ファイルを上書き保存する</button></p>
<textarea id="textContent" rows="20" cols="80"></textarea>

リスト4は、上書き保存ボタンに設定するイベントハンドラ部分のJavaScriptコードです。

リスト4:file_write.js


(1)読み込み時にファイルファイルハンドルを保存しておく
let savedFileHandle = null;
…略…
document.getElementById('openButton').addEventListener('click', async () => {
  …略…
  savedFileHandle = fileHandle;
  …略…
});

document.getElementById('saveButton').addEventListener('click', async () => {
  …略…
  try {
    const contents = textContent.value;
    (2)書き込み用のファイルハンドルを生成する
    const writable = await savedFileHandle.createWritable();
    (3)書き込みを実行する
    await writable.write(contents);
    (4)ファイルをクローズする
    await writable.close();
  } catch (err) {
    console.error('ファイルの保存に失敗しました。', err);
  }
});

ポイントは、以下の4つです。上書き保存なので、読み込み時に取得したファイルハンドルをそのまま使って書き込み操作を行います。

(1)ファイルの読み込み時に、ファイルハンドルを保存しておく
(2)createWritableメソッドで、そのファイルハンドルを元に書き込み用のファイルハンドルを生成する
(3)writeメソッドで、textarea要素のコンテンツを書き込む
(4)closeメソッドで、ファイルをクローズする

あらかじめファイルを読み込んでおき、適当に書き換えて上書き保存ボタンをクリックすると、ファイルが上書き保存されます。改めて読み込むか、適当なテキストエディタで読み込むと、ファイルが書き換えられていることを確認できます(図3)。ここでは、「猫」「ニャーニャー」を「イヌ」「ワンワン」に書き換えてみました。

  • 図3:ファイルの上書き保存

    図3:ファイルの上書き保存

ファイルの別名保存

上書き保存ではなくファイルを別名で保存するのも簡単です。保存用のファイルハンドルをshowSaveFilePickerメソッドで取得し、それに対して前述の書き込み操作を行うだけです。リスト5は、前述のリストに別名保存のボタンを追加したHTMLです。

リスト5:file_write.html

<p><button id="saveAsButton">ファイルを別名で保存する</button></p>

リスト6は、別名保存ボタンクリックで呼び出されるJavaScriptコードです。

リスト6:file_write.js


document.getElementById('saveAsButton').addEventListener('click', async () => {
  …略…
  try {
    const contents = textContent.value;
    const handle = await window.showSaveFilePicker({
      suggestedName: 'newfile.txt',
      …略…
    });
    const writable = await handle.createWritable();
    await writable.write(contents);
    await writable.close();
  } catch (err) {
    console.error('ファイルの保存に失敗しました。', err);
  }
});

保存しておいたファイルハンドルを使うのではなく、改めてshowSaveFilePickerメソッドで保存用のファイルハンドルを取得し、以降はcreateWritableメソッドなどで書き込み操作を行うのは上書き保存のサンプルと同様です。showSaveFilePickerメソッドで指定できるオプションはshowOpenFilePickerメソッドとほぼ同様ですが、multipleオプションが使えないこと、suggestedNameオプションで既定のファイル名を指定できる点が異なってきます。

まとめ

File System APIはいかがでしたでしょうか。ブラウザでローカルファイルの読み書きが可能になるということで、Webアプリの可能性が大きく広がることをお伝えできたのではないかと思います。次回は、ドラッグ&ドロップ操作によるファイル操作や、ディレクトリ内のファイル/フォルダの操作、そしてOPFSを紹介します。

WINGSプロジェクト 山内直(著) 山田 祥寛(監修)
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
RSS
X:@WingsPro_info(公式)@WingsPro_info/wings(メンバーリスト)
Facebook

<著者について>
WINGSプロジェクト所属のテクニカルライター。出版社を経てフリーランスとして独立。ライター、エディター、デベロッパー、講師業に従事。屋号は「たまデジ。」。