現役エンジニアが教える!実践JavaScript入門 〜ブラウザでのファイル操作編〜

公開日:2022-09-17
JavaScript
ファイル操作

https://youtu.be/PU8bwpXUJRc

今回扱う内容:ブラウザでのファイル操作

ブラウザでのファイル操作を理解しよう。

はじめに

私たちは普段からプロフィールページの登録に画像ファイルを選択したり、Webサービスからデータをダウンロードしてくるなど、ファイル操作を日常的に行います。
今回はWeb開発でもよく使われるファイル操作についてこちらのMDNの記事を参考に解説していきます。

https://developer.mozilla.org/ja/docs/Web/API/File_API/Using_files_from_web_applicationshttps://developer.mozilla.org/ja/docs/Web/API/File_API/Using_files_from_web_applications

用語の説明

一般的にファイル選択の実装をするには、こちらのMDNの記事にあるようにinput要素の type 属性に file を指定したものを使います。そして選択されたファイルをどう扱うかという動きについてはJavaScriptで記述します。
この節ではJavaScriptでファイルを扱う際に重要な用語について紹介します。

https://developer.mozilla.org/ja/docs/Web/HTML/Element/input/filehttps://developer.mozilla.org/ja/docs/Web/HTML/Element/input/file

FileオブジェクトとBlobオブジェクト

Fileオブジェクトとは先程のinput要素のtype属性にfileを指定した際に取得できる値の型です。
ファイルについての情報を提供したり、Fileオブジェクトを通じてWebページ内のJavaScriptからファイルの内容にアクセスできます。
なのでファイルを扱うためにブラウザで用意されているAPIだと思ってもらえれば大丈夫です。
一方でBlobオブジェクトは、Fileオブジェクトのベースになっているものであり、FileオブジェクトはBlobオブジェクトを継承しています。
Blobオブジェクトはブラウザからファイルのデータを作るとき、またインターネットから落としてきたデータをファイルとして扱うときに、新しくBlobオブジェクトを作成します。
説明だけではわかりにくい部分もあるので、次節で実際のファイル操作の例とともに2つの違いを見ていきましょう。

https://developer.mozilla.org/ja/docs/Web/API/Filehttps://developer.mozilla.org/ja/docs/Web/API/File

https://developer.mozilla.org/ja/docs/Web/API/Blobhttps://developer.mozilla.org/ja/docs/Web/API/Blob

ファイル操作の例

画像ファイルを扱う

まず初めにファイル操作の例として画像ファイルの扱い方について見ていきます。
htmlファイルでinputタグを用意し、JavaScriptファイルにinputタグの参照と次のようなEventListenerを登録します。
ファイルを選択することでe.target.filesからファイル情報を参照できます。またファイルを複数選択した場合も配列としてファイル情報を読み取ることができます。

  <input type="file" multiple> <!-- multiple: ファイルの複数選択可 -->
  <div class="preview"></div>  <!--画像を表示させる際に使用-->
  const inputRef = document.querySelector("input");
  inputRef.addEventListener("change", (e) => {
     console.log(e);
     console.log(e.target);
     console.log(e.target.files); //選択したファイル情報が表示される
  });

次にファイル情報を画面に表示させてみます。

画像の表示画像の表示

パターン1

次のように記述することで、選択した画像ファイルを図のように表示させることができます。
複数選択した画像はFileListという型で呼ばれ、for...ofを使うことでファイル1つずつを取り出して扱うことができます。
次にURL.createObjectURLメソッドに取り出したファイルを渡すことで、画像ファイルのURLを取得できます。
画像の表示には通常imgタグのsrcに画像が置かれているURLを参照させます。一方でローカルのファイルを参照させる場合はURL.createObjectURLメソッドにFileオブジェクトを渡すことでローカルのブラウザ内でのURLを生成し、生成したURLをsrcに参照させます。
この処理を選択したファイルの数だけ実行するので選択したファイル全てを画面に表示できます。

  const previewRef = document.querySelector(".preview");
  const handleImageFiles1 = (e) => {
    for (const file of e.target.files) {
      const item = document.createElement("div");
      const image = document.createElement("img");
      //URLを作成
      image.src = URL.createObjectURL(file);
      item.appendChild(image);
      previewRef.appendChild(item);
    }
  };
  inputRef.addEventListener("change", handleImageFiles1);

詳しいcreateObjectURLメソッドの内容についてはMDNの記事を参考にしてみてください。

https://developer.mozilla.org/ja/docs/Web/API/URL/createObjectURLhttps://developer.mozilla.org/ja/docs/Web/API/URL/createObjectURL

パターン2

次のように記述してもパターン1と同様に選択した画像を表示させることができます。
こちらではFileReaderオブジェクトを使っており、新しく作成したオブジェクトにreadAsDataURLメソッドを介してFileオブジェクトを渡すことでloadイベントが実行されます。実行内容は各画像ファイルを読み込んだ結果がresultの中に入っているので先ほどと同様にsrcに結果を渡して画像を表示します。

  const handleImageFiles2 = (e) => {
    for (const file of e.target.files) {
      const item = document.createElement("div");
      const image = document.createElement("img");

      const reader = new FileReader();
      reader.addEventListener("load", () => {
        image.src = reader.result;
        item.appendChild(image);
        previewRef.appendChild(item);
      });
      reader.readAsDataURL(file);
    }
  }

詳しいFileReaderオブジェクトについてはMDNの記事を参考にしてみてください。

https://developer.mozilla.org/ja/docs/Web/API/FileReaderhttps://developer.mozilla.org/ja/docs/Web/API/FileReader

https://developer.mozilla.org/ja/docs/Web/API/FileReader/readAsDataURLhttps://developer.mozilla.org/ja/docs/Web/API/FileReader/readAsDataURL

createObjectURLとreadAsDataURLの違い

2パターン画像表示について紹介しましたが2つの手法は、createObjectURLはBlobを参照するURLを生成し、readAsDataURLはデータURLを読み取るという点に違いがあります。パターン1とパターン2で作成したimg.srcをコンソールに表示させると分かるのですが、createObjectURLでは作られたURLには画像データは含まれておらずデータの参照先だけが生成されます。一方でreadAsDataURLでは作られたURLに画像のデータが入っています。
というのも画像データはテキストに変換でき、そのデータ形式を「BASE64」といいます。そしてreadAsDataURLでは画像のテキストデータをURLに含ませています。

それぞれのデメリットとしては、createObjectURLはメモリの扱い方に注意が必要です。Blobを参照するURLを生成するとメモリ上にデータが残ってしまうのでメモリの解放を適宜行わないとパフォーマンスの劣化につながります。一方でreadAsDataURLではデータのサイズが大きくなつてしまう点に注意が必要です。画像ファイルをテキストに変換するとファイルサイズが通常20%〜30%増えてしまうという問題があります。適宜使用目的に合わせて使い分けてください。

テキストファイルを扱う

次にテキストファイルの扱い方について見ていきます。今回は次のような内容のcsvファイルを読み込み、画像と同様にブラウザに表示させます。

csvファイルcsvファイルの中身
テキストの表示テキストの表示

コード内容は画像の表示で記述したものとほぼ同じで、次のようになっています。新しく作成したFileReaderオブジェクトにreadAsTextメソッドを介してloadイベントが実行されます。readAsTextメソッドではファイルの中身をテキストとして読むことができます。そしてsplitmapを使ってリスト処理を行うことでテキストの中身を3×3の2次元配列に変換し、join(",")を使って「,」繋がりのテキストに戻してからブラウザに表示します。
リスト処理についてはJSマスター第5回を参照してください。

  const inputRef = document.querySelector("input");
  const previewRef = document.querySelector(".preview");
  const hadleCsvFiles = (e) => {
    for (const file of e.target.files) {
      //URLを作成
      const reader = new FileReader();
      reader.addEventListener("load", () => {
        const result = reader.result;
        console.log(result);
        const lines = result.split(/\n/).map((line) => line.split(","));
        console.log(JSON.stringify(lines));
        console.log(lines);
        lines.forEach((line) => {
          const item = document.createElement("p");
          item.textContent = line.join(",");
          previewRef.appendChild(item);
        });
      });
      reader.readAsText(file)
    }
  };
  inputRef.addEventListener("change", handleCsvFiles);

ファイルを保存する

最後にファイルの保存について見ていきます。今回先程のcsvのファイルの内容と同じデータをdataという変数に持たせています。このデータをcsv形式としてダウンロードするという機能を実装します。コードは次のようになっており、はじめにcsvデータはテキストデータであるので定義したdatareduce,join,sliceを使うことでテキストに変換します。次にBlobオブジェクトを作成します。この時文字化け対策のためbomというデータを先頭につけます。また第2引数のオブジェクトにはtypeを渡しており、text/csvとしています。このように記述することでnew Blobよりbomとcsvデータを足したデータのBlobオブジェクトを作成します。最後にaタグのdownloadプロパティにファイル名を渡しclickを実行することで、画面上でもaタグのダウンロードリンクが押されたのと同じ処理を実行できます。このように自分でファイルを作る際にはBlobオブジェクトを作ります。

  const data = [
    ["0001", "itemA", "500yen"],
    ["0002", "itemB", "300yen"],
    ["0003", "itemC", "400yen"],
  ];

  const handleCsvDownload = () => {
    const csvData = data
      .reduce((a, b) => a + b.join(",") + "\n", "")
      .slice(0, -1);
    const a = document.createElement("a");
    const bom = new Unit8Array([0xef, 0xbb, 0xbf]); //文字化け対策
    const blob = new Blob([bom, csvData], {type: "text/csv"});

    a.href = URL.createObjectURL(blob); //URLを作成
    a.download = "download.csv";
    a.click();
  }
  buttonRef.addEventListener("click", handleCsvDownload)

またファイルの保存方法としてライブラリを使う方法もあります。今回はFileSaverというライブラリを使った方法をご紹介します。

https://github.com/eligrey/FileSaver.js/https://github.com/eligrey/FileSaver.js/

使い方として一番簡単なやり方は次のようにscriptタグに読み込ませるというやり方です。

<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.0/FileSaver.min.js"></script>

すると先程のコードでaタグに関する記述がいらなくなるので`handleCsvDownload`関数を次のように書き換えるだけでファイルの保存が実装できます。また、aタグが使えないブラウザ環境もある一方で、FileSaverでは様々なブラウザに対応しているので、現実的には実績のあるライブラリを使うことをお勧めします。

  const handleCsvDownload = () => {
    const csvData = data
      .reduce((a, b) => a + b.join(",") + "\n", "")
      .slice(0, -1);
    const bom = new Unit8Array([0xef, 0xbb, 0xbf]);
    const blob = new Blob([bom, csvData], {type: "text/csv"});
    saveAs(blob, "download.csv") //ファイルの保存
  }

まとめ

ブラウザでファイル操作をする方法について解説しました。今回はコードの簡略化のため、ファイルサイズの制限やファイル拡張子の指定などを行いませんでしたが、実際の開発ではそういった点に注意しながら本内容を役立てていただけると嬉しいです。
次回はブラウザ上でのデータ保存について解説します。