🥌

アップロードされた画像をlocalStorageに格納する方法

に公開

🌱 はじめに

ユーザーによりアップロードされた画像をサーバーやDBなどに格納せず、localStorageに格納したくなったので調査した結果を記載。

🪇 前提

以下で書いてます
📣 言語 → TypeScript
🚋 フレームワーク → Nuxt3

🧑‍💻 基本のコード

<template>
  <input type="file" accept="image/jpg,image/jpeg,image/png,image/gif" @change="uploadImage">
</template>
<script setup lang="ts">
  const uploadImage = () => {
    console.log('ここにlocalStorageに格納する処理を記載');
  };
</script>


上記のコードを実行してみた様子

ここからはこのscriptタグの中身を書き換えていく。

📬 まずはファイルの取得

const uploadImage = (event: Event) => {
  const target = event.target;
  if (!(target instanceof HTMLInputElement)) return;

  const selectedFile = target.files?.[0];
  if (!selectedFile) return;

  console.log('🐹選択した画像🐹:', selectedFile);
};


これで一旦アップロードした画像がコンソールに表示されるようになった

📥 localStorageに格納しようとしてみる

const uploadImage = (event: Event) => {
  const target = event.target;
  if (!(target instanceof HTMLInputElement)) return;

  const selectedFile = target.files?.[0];
  if (!selectedFile) return;

  console.log('🐹選択した画像🐹:', selectedFile);
  // 追加
  window.localStorage.setItem('uploadedImage', selectedFile);
};


実行結果

でもこれでは [object File] という文字列が入っているだけで画像は入ってない

これはlocalStorageのvalueがStringのみ許容しているから。
https://developer.mozilla.org/ja/docs/Web/API/Storage/setItem

keyValue
作成または更新したいキーに対して渡したい値を持つ文字列です。

なので以下2つの方法で実施

  1. 画像自体を文字列化してlocalStorageに格納
  2. 画像にアクセスできるURLを発行してそのURLを格納

1️⃣ 画像自体を文字列化してlocalStorageに格納

data: URL としてファイルの情報自体を文字列にしてからlocalStorageに格納する。

const uploadImage = async (event: Event) => {
  const target = event.target;
  if (!(target instanceof HTMLInputElement)) return;

  const selectedFile = target.files?.[0];
  if (!selectedFile) return;

  const imageString = await new Promise<string>((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(String(reader.result));
    reader.onerror = () => reject(reader.error);
    reader.readAsDataURL(selectedFile);
  });
  window.localStorage.setItem('uploadedImage', imageString);
};

data:image形式で格納できた!

ちなみに

const imageString = await new Promise<string>((resolve, reject) => {
  const reader = new FileReader();
  reader.onload = () => resolve(String(reader.result));
  reader.onerror = () => reject(reader.error);
  reader.readAsDataURL(selectedFile);
});

について解説。

const reader = new FileReader();

画像読み込みツールであるFileReader() を用意しておく。

reader.onload = () => resolve(String(reader.result));

用意ができたら reader.result を文字列にしてreturnする
※ resolveは成功時のreturnって感じ

reader.onerror = () => reject(reader.error);

用意ができなかったら reader.error をreturnする
※ rejectは失敗時のreturnって感じ

reader.readAsDataURL(selectedFile);

ここでアップロードされたファイルの読み込みを開始する
ここに記載されているようにresultに欲しい文字列が入るので、 resolve() にはreader.resultを格納する。

result 属性には、ファイルのデータを表す、base64 エンコーディングされた data: URL の文字列が格納されます。

2️⃣ 画像にアクセスできるURLを発行してそのURLを格納

URL.createObjectURL()を使用して画像に対する一時的なオブジェクトURLを発行しlocalStorageに格納する

const uploadImage = (event: Event) => {
  const target = event.target;
  if (!(target instanceof HTMLInputElement)) return;

  const selectedFile = target.files?.[0];
  if (!selectedFile) return;

  const objectUrl = URL.createObjectURL(selectedFile);
  window.localStorage.setItem('uploadedImageObjectUrl', objectUrl);
};

🌮 2つの方法を比較

画像自体の文字列化 URLを発行
特徴 画像データを文字列化 一時的にアクセスできるURLを発行
メリット 別タブからアクセス可能 localStorageの容量を逼迫せず格納できる
デメリット localStorageの容量に依存 別タブからアクセスできない
同じくリロードでも破棄される

🎚️ 補足: localStorageの容量

localStorageの容量は 5MiBまでと記載されているため、画像自体の文字列化の場合は5MiBを超過する画像はエラーになってしまう。
ちなみに、5MiBは5.24288MBくらいとのこと。

Uncaught (in promise) QuotaExceededError: Failed to execute 'setItem' on 'Storage': Setting the value of 'uploadedImage' exceeded the quota.

🎡 補足: Base64への変換によるデータ量の増加

data: URLを用いた方法だとデータ量が4/3倍になる。参考

文字列やファイルを Base64 にすると、通常、その文字列のソースよりもおよそ 3 分の 1 大きくなります

したがってdata: URLで格納する場合は 5MiB * 3/4 = 3.75MiB ほどの画像しか格納できないということになる。

📖 まとめ

案として2つ書いたがどちらにも良し悪しがあり、 容量の大きい画像を格納し、別タブで参照したい 場合とかは役割を果たせなさそう...
調べてみたら IndexedDB とか Cache とかのやり方もありそうなので調査。

追記

IndexedDBについて記事にしました↓
https://zenn.dev/mt5/articles/71cca8e5c1341b

Cacheについて記事にしました↓
https://zenn.dev/mt5/articles/db6a3857f8d83c

Discussion