7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

株式会社ACCESSAdvent Calendar 2019

Day 11

ローカルの画像を読み込み、ブラウザ上でプレビュー表示する

Last updated at Posted at 2019-12-11

この記事は 株式会社 ACCESS Advent Calendar 2019 11 日目の記事です。

こんばんは! @diescake です。

2 日前の IndexedDB を単一のデータを格納するストレージとして扱うラッパー実装 に少し関連するんですが、今回は、ローカルの画像を読みだした際に、ブラウザ上で表示する実装を紹介します。

こちらも、へーしゃのお仕事で必要になりそうだったので、
プライベートの時間で実装検証したものを別リポジトリに整理しました。
特に問題なく扱えているので紹介します。

リポジトリ

なぜ必要になったか?

ローカルの選択した画像をブラウザ上でプレビュー表示するというユースケースがありました。

具体的には、Twitter や Facebook のメッセージ投稿機能をイメージして貰うとわかりやすいと思います。
このとき、写真添付ボタンをクリックしてローカルの写真を選択すると、選択した画像が投稿画面内にサムネイル表示されると思います。

ローカルファイルへアクセスするためには、<input type="file" /> 要素を利用する必要があります。
ここで、選択した画像をそのまま POST することは難しくはありませんが、ブラウザ上で表示するには一工夫が必要です。

ちなみに、その他にドラッグ&ドロップという手段もありますが、今回は要件に合わなかったため除外して考えています。

実装の紹介

リポジトリを見て貰っても良いですが、然程長くないので以下に掲載します。

HTML
<!-- 前略 -->
<body>
  <h1>Load local images through input elements.</h1>
  <div id="root"></div>
  <input id="file-input" type="file" multiple />
  <label class="button-add" for="file-input">+</label>
</body>
TypeScript
const fetchDataUrl = (file: File) =>
  new Promise<string>((resolve, reject) => {
    const fr = new FileReader();
    fr.onload = () => resolve(typeof fr.result === "string" ? fr.result : "");
    fr.onerror = err => reject(err);

    fr.readAsDataURL(file);
  });

const appendNewImage = (dataUrl: string) => {
  const e = document.createElement("img");
  e.src = dataUrl;
  document.body.appendChild(e);
};

const input = document.querySelector('input[type="file"]') as HTMLInputElement;

input.addEventListener("change", async () => {
  if (!input.files) {
    console.error("no files");
    return;
  }

  const files = Array.from(input.files);
  const dataUrls = await Promise.all(files.map(fetchDataUrl)).catch(() => []);

  dataUrls.forEach(appendNewImage);
});

ポイントは fetchDataUrl 関数の実装ですね。
fetchDataUrl の中で input 要素から受け取った File の配列を渡して FileReader#readAsDataURL に食わせています。

FileReader の interface は古き良きコールバック(あるいはイベントリスナ)をセットするスタイルで使いづらいので Promise を返すようにラップしています。

ちなみに、Chrome 76 からは fetchDataUrl のように Promise を返すラップ実装をせずとも、ネイティブで Promise を返す API が実装されています。

このリリースノートでは blob と記述されていますが、Blob は File の親クラスであるため、file に対してもこれらの API は呼び出しが可能です。

ただし、ブラウザのサポート状況はまだ弱いので、広いブラウザサポートが不要であったり、ポリフィルが効く状況でなければ従来通り、FileReader#read* 系の関数をを Promise でラップして利用するのが無難に思います。

その後、Promise.all で全ての fetchDataUrl の返した Promise の解決を待ってから、ブラウザ上に表示して終わりです。

最後に、input要素の属性に multiple を指定しています。
この属性を指定すると、ユーザはファイルを選択する際に複数選択が可能となります。

単純ですが、ユーザビリティ上非常に重要なので忘れないようにしたいところです。
ローカルでバリデーションする際の実装が若干面倒になるけど……。

パフォーマンス上の注意点

少し試した範囲ですが Android 実機上の Chrome ブラウザではファイルの読み出しに非常に時間がかかる(写真数枚で10秒に達するくらい)ことを確認しました。
(実機や OS のバージョン明確ではないのですが、2019/8 月頃で、それなりに新しい Android 実機と OS バージョン)

iOS Safari (iPhone XS Max / iOS11)と比較すると 5 倍以上読み込み時間に開きが出ました。
なので、Android の場合は、複数枚画像をアップロードする場合は、実用的なレベルに達しない可能性があります。

DEMO のページを各種端末のブラウザで開いて試してみるとわかると思います。

従って、Android のサポートが必要な場合は、プレビュー表示を諦めたり、あるいは、ファイル選択時に先行して S3 などのファイルサーバにアップロードしてしまい、そのアップロード先 URL を <img src=... で開くような仕組みを検討するのが良さそうです。

Qiita や GitHub issues などはこちらの仕組みを利用していますね。

さて、次回は、

――ええ、遠慮はいらないわ。
  全てのバグを潰してやって、 @illypong

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?