おじんブログ

Webアプリに関する知見とか雑記です

Inputタグにアップされた画像ファイルのプレビューを オブジェクト URLと データ URL でやる方法で書き比べてみた

動機

アップロードした画像を使って画像を加工するなどの処理の場合、どのように取り出して取り扱うか、というのが全然整理できていなかったのでその整理を込めてサンプルコードを書いた。

File オブジェクトや addEventListener もあんまり書いたことがなかったので、ライブラリなしの Vanilla JS で書いてみた。

やりたいこと

  • input[type="file"] に入力された画像はファイル名しか表示されないので、画像がプレビューされてほしい
  • fileを取り出した後に <img> タグに表示するためにURLに変換する必要があるが、アプローチとして URL.createObjectURL() を使う、 FileReaderreadAsDataURL() を使うアプローチがあった

TL;DR

Codesandbox で試せる環境を作った。勉強も兼ねてVanilla + TypeScriptで型をつけながら書いています。

  • 動作的にはどっちでプレビューしても殆ど変わりはない
  • URL.createObjectURL() してオブジェクトURLを生成すると、開放しない限りはメモリ上に残り続ける
  • なのでオブジェクトURLが不要になったタイミング、今回の場合だと画像の読み込みが終わったタイミングで URL.revokeObjectURL() を実行する必要がある
  • 開放するとそのオブジェクトURLを使って画像を表示、というのができなくなるので、複数箇所で同じ画像を表示するなら URL.revokeObjectURL() を実行するタイミングを考える必要がある
  • データURLはURL自体にデータを持っているようなものなのでオブジェクトURLよりは比較的取り回しが楽に感じた

解説

入力したファイルの取り出し

const fileForm = document.getElementById("userFile");

function handleFiles(event: Event) {
  const target = event.target as HTMLInputElement;
  const files = target.files;
  // ...
}

fileForm?.addEventListener("change", handleFiles, false);

input[type="file"] に対して document.getElementById("userFile").files という形で取り出せますが、フォームへの入力が変化したタイミングで処理を行いたいので EventListener 経由でアクセスして取り出しています。

取り出したファイルデータは複数の File データを持つ FileList オブジェクトとして取り出されます。

フォームから受け取りデータが単一の File オブジェクトではなく FileListオブジェクトなのは、フォームにて複数のファイルが入力する場合があるためです。

file からオブジェクトURLの変換

Array.from(files).forEach((file) => {
  // if you would like to watch uploaded file info, remove comment
  // console.log(file)
  const image = new Image(300);
  image.addEventListener("load", (event: Event) => {
    const target = event.target as HTMLImageElement;
    URL.revokeObjectURL(target.src);
  });
  const imageUrl = URL.createObjectURL(file);
  image.src = imageUrl;
  image.title = file.name;
  previewPoint?.appendChild(image);
});

まず new Image() にてプレビュー画像を表示するための <img> タグを生成しています。width=300 としたいのでコンストラクターに引数として渡しています。

オブジェクトURLは不要になったら開放する必要があるため、 <img> タグにて画像の読み込みが終わった時点で URL.revokeObjectURL() するように EventListener で登録します。

取得したFileを URL.createObjectURL() を通してオブジェクトURLを発行し、それを <img> タグの src に入力し、画像の表示を行います。

最後にプレビューするポイントにプレビュー画像のタグを付与する。

file からデータURLの変換

Array.from(files).forEach((file) => {
  const reader = new FileReader();
  reader.addEventListener(
    "load",
    (event: Event) => {
      const image = new Image(300);
      image.title = file.name;
      image.src = reader.result as string;
      previewPoint2?.appendChild(image);
    },
    false
  );
  reader.readAsDataURL(file);
});

FileReader.readAsDataURL() でファイルデータを読み込むとデータURLとして変換され、 FileReader.result にて取り出すことが出来る。

FileReaderがFileを読み込んだタイミングで FileReader.result にデータURLが入るため EventListener にて Event を登録する。

画像のタグを付与するあたりはオブジェクトURLと一緒。

実はこの箇所はMDNのサンプルコードのままではある。

最後に

フォームで入力した画像を取り出して取り扱う方法を学習することができた。

動機としては React Image Crop というライブラリを使おうと思った際に、当然のようにオブジェクトURLとbase64 data が登場したが、曖昧な理解だと今後も詰まりそうだと思ったのでWeb APIの標準を通じて学習した。

https://github.com/DominicTobias/react-image-crop

上記のオブジェクトURLを使ったサンプルコードを書いた後に、MDNのサンプルに自分がやりたいのとほぼ同じ物があったのでぐぬぬ…となったが、TypeScriptで型付けるという形で発展させた。

Web アプリケーションからのファイルの使用