astris.design

[JS]Web Storageの仕様と実装 localStorage, sessionStorage

クライアントサイドJavaScriptには、Web Storage(ローカルストレージ/セッションストレージ)という、ブラウザにデータを保存する仕組みがあります。Web Storageの仕様と実装について、解説します(サンプルあり)。

【目次・Web Storageの仕様と実装】

Web Storage:ブラウザにデータを保存

Web Storageは、ブラウザにキーバリュー形式でデータを保存するものです。
Cookieのようにサーバに送信されることはなく、オリジン(スキーマ://ホスト名:ポート番号)ごとに、5~10MB程度のデータを保存することができます。
同一オリジンのページのみ、Web Storageに保存されたデータにアクセスすることができます。
また、Web Storageのデータは、開発者ツール(F12)の「アプリケーション」タブで、確認することができます。データの追加、編集、削除も可能です。
ストレージに保存できるデータは、文字列のみです。文字列以外を保存しようとすると、自動的に文字列化して保存されます。
そのため、オブジェクトを保存する場合は、JSONに変換してから保存するようにします。

Web Storageには、ローカルストレージ(local storage)とセッションストレージ(session storage)があります。
両者は、データの有効期間とスコープが異なります。

ローカルストレージは、データの有効期間は永続です。明示的に削除しない限り、ブラウザを閉じても、ページ遷移しても削除されません。ページを再開すれば、保存したデータを利用することができます。
データのスコープは、ドキュメントのオリジン単位です。同一オリジンのドキュメントを、複数のウィンドウで開いている場合、データを共有することができます。

セッションストレージは、データの有効期間はブラウザセッションとなります。ブラウザ(タブ)を閉じると削除されます。
データのスコープは、ドキュメントのオリジン単位かつ、ウィンドウ単位です。同一オリジンのドキュメントを開いている場合でも、別のウィンドウとデータを共有することはできません。

ローカルストレージとセッションストレージの比較
ストレージ有効期間スコープ
ローカルストレージ永続オリジン単位
セッションストレージブラウザセッションオリジン単位、ウィンドウ単位

上記の違い以外は、ローカルストレージもセッションストレージも、操作方法は全く同じです。

Web Storageは、WindowオブジェクトのlocalStorage/sessionStorageプロパティ(Storageオブジェクトを参照)でアクセスします。
Storageオブジェクトには、getItem/setItemメソッドがありますが、プロパティ構文やブラケット構文で直接データにアクセスすることができます。
データの削除は、removeItemメソッドや、delete演算子を使用します。あるオリジンの全てのデータを削除する場合は、clearメソッドを使用します。

HTML

storage.html
<form>
  <label for="job">Job:</label><br>
  <input type="text" name="job" id="job" value="Engineer"><br>
  <label for="position">Position:</label><br>
  <input type="text" name="position" id="position" value="SE"><br>
  <label for="skill">Skill:</label><br>
  <input type="text" name="skill" id="skill" value="JavaScript">
  <input type="button" id="save" value="Save">
  <input type="button" id="view" value="View">
</form>
<hr>
<table id="table"></table>
<script src="storage.js"></script>

JavaScript

storage.js
document.addEventListener(
  'DOMContentLoaded',
  () => {
    const storage = sessionStorage;
    const job = document.getElementById('job');
    const position = document.getElementById('position');
    const skill = document.getElementById('skill');
    const table = document.getElementById('table');
    document.getElementById('save').addEventListener(
      'click',
      () => {
        storage.job = job.value;
        storage.position = position.value;
        storage.skill = skill.value;
        alert('Saved to storage.');
      },
      false,
    );
    document.getElementById('view').addEventListener(
      'click',
      () => {
        if (storage.length < 1) {
          alert('Storage is empty.');
          return;
        }
        table.innerHTML = '';
        for (let i = 0, len = storage.length; i < len; i++) {
          const key = storage.key(i);
          const tr = document.createElement('tr');
          const td_key = document.createElement('td');
          const td_value = document.createElement('td');
          td_key.innerHTML = `${key}: `;
          td_value.innerHTML = storage[key];
          tr.appendChild(td_key);
          tr.appendChild(td_value);
          table.appendChild(tr);
        }
      },
      false,
    );
  },
  false,
);

ストレージ管理クラスの実装:キーの名前衝突対策

一つのオリジンに複数のアプリを実装し、ローカルストレージを使用する場合、キーが名前衝突することがあります。
そのため、複数アプリを実装する場合は、キーをアプリ名とし、値をオブジェクト(JSON)として保存すると、名前衝突を防止することができます。
ストレージを管理するオブジェクト(サンプルではAppStorage)を作成し、保存データのオブジェクトを隠蔽すると、直感的に使いやすくなります。

HTML

storage_object.html
<form>
  <label for="job">Job:</label><br>
  <input type="text" name="job" id="job" value="Engineer"><br>
  <label for="position">Position:</label><br>
  <input type="text" name="position" id="position" value="SE"><br>
  <label for="skill">Skill:</label><br>
  <input type="text" name="skill" id="skill" value="JavaScript">
  <input type="button" id="save" value="Save">
  <input type="button" id="view" value="View">
</form>
<hr>
<table id="table"></table>
<script src="storage_object.js"></script>

JavaScript

storage_object.js
class AppStorage {
  constructor(appName) {
    this.app = appName;
    this.storage = localStorage;
    this.data = JSON.parse(this.storage[this.app] || '{}');
    this.key = Object.keys(this.data);
  }
  get(key) {
    return this.data[key];
  }
  set(key, value) {
    this.data[key] = value;
  }
  save() {
    this.storage[this.app] = JSON.stringify(this.data);
    this.key = Object.keys(this.data);
  }
};
const storage = new AppStorage('AppName');
document.addEventListener(
  'DOMContentLoaded',
  () => {
    const job = document.getElementById('job');
    const position = document.getElementById('position');
    const skill = document.getElementById('skill');
    const table = document.getElementById('table');
    document.getElementById('save').addEventListener(
      'click',
      () => {
        storage.set('job', job.value);
        storage.set('position', position.value);
        storage.set('skill', skill.value);
        storage.save();
        alert('Saved to storage.');
      },
      false,
    );
    document.getElementById('view').addEventListener(
      'click',
      () => {
        if (storage.key.length < 1) {
          alert('Storage is empty.');
          return;
        }
        table.innerHTML = '';
        for (key of storage.key) {
          const tr = document.createElement('tr');
          const td_key = document.createElement('td');
          const td_value = document.createElement('td');
          td_key.innerHTML = `${key}: `;
          td_value.innerHTML = storage.get(key);
          tr.appendChild(td_key);
          tr.appendChild(td_value);
          table.appendChild(tr);
        }
      },
      false,
    );
  },
  false,
);

storageイベント:ストレージの更新を検知

ローカルストレージでは、別のウィンドウ(同一オリジン)でのストレージの更新を、storageイベントとして検知することができます。
ストレージを更新したウィンドウでは、イベントは発火しません。
セッションストレージは、スコープがウィンドウ単位なので、別のウィンドウとデータを共有することはできません。

storageイベントは、データの更新/反映だけでなく、メッセージ送信(ブロードキャスト)に利用することもできます。
また、メインウィンドウと操作パネルウィンドウを持つWebアプリを実装することもできます。

storageイベントにハンドラを登録するには、window.onstorageプロパティやwindow.addEventListenerメソッドを使用します。
StorageEventのプロパティは、以下になります。

StorageEventのプロパティ
プロパティ内容
key変更されたデータのキー
oldValue変更前のデータの値
newValue変更後のデータの値
urlストレージを更新したドキュメントのURL
storageArea変更されたStorageオブジェクト

HTML

storage_event.html
<form>
  <label for="job">Job:</label><br>
  <input type="text" name="job" id="job" value="Engineer"><br>
  <label for="position">Position:</label><br>
  <input type="text" name="position" id="position" value="SE"><br>
  <label for="skill">Skill:</label><br>
  <input type="text" name="skill" id="skill" value="JavaScript">
  <input type="button" id="save" value="Save & View">
</form>
<hr>
<table id="table"></table>
<script src="storage_event.js"></script>

JavaScript

storage_event.js
class AppStorage {
  constructor(appName) {
    this.app = appName;
    this.storage = localStorage;
    this.data = JSON.parse(this.storage[this.app] || '{}');
    this.key = Object.keys(this.data);
  }
  get(key) {
    return this.data[key];
  }
  set(key, value) {
    this.data[key] = value;
  }
  save() {
    this.storage[this.app] = JSON.stringify(this.data);
    this.key = Object.keys(this.data);
  }
  reload() {
    this.data = JSON.parse(this.storage[this.app] || '{}');
    this.key = Object.keys(this.data);
  }
};
const storage = new AppStorage('AppName');
document.addEventListener(
  'DOMContentLoaded',
  () => {
    const job = document.getElementById('job');
    const position = document.getElementById('position');
    const skill = document.getElementById('skill');
    const table = document.getElementById('table');
    document.getElementById('save').addEventListener(
      'click',
      () => {
        storage.set('job', job.value);
        storage.set('position', position.value);
        storage.set('skill', skill.value);
        storage.save();
        alert('Saved to storage.');
        table.innerHTML = '';
        for (key of storage.key) {
          const tr = document.createElement('tr');
          const td_key = document.createElement('td');
          const td_value = document.createElement('td');
          td_key.innerHTML = `${key}: `;
          td_value.innerHTML = storage.get(key);
          tr.appendChild(td_key);
          tr.appendChild(td_value);
          table.appendChild(tr);
        }
      },
      false,
    );
    window.addEventListener(
      'storage',
      (evt) => {
        alert(`Storage is updated.\n${evt.url}`);
        storage.reload();
        table.innerHTML = '';
        for (key of storage.key) {
          const tr = document.createElement('tr');
          const td_key = document.createElement('td');
          const td_value = document.createElement('td');
          td_key.innerHTML = `${key}: `;
          td_value.innerHTML = storage.get(key);
          tr.appendChild(td_key);
          tr.appendChild(td_value);
          table.appendChild(tr);
        }
      },
      false,
    );
  },
  false,
);

まとめ

クライアントサイドJavaScriptのWeb Storageについて、解説しました。
Web Storageには、ローカルストレージとセッションストレージがあります。 それぞれの用途は、以下のようになります。

  • ローカルストレージ:ページ再開時に使用するデータを保存
  • セッションストレージ:ブラウザセッション内でだけ使用するデータを保存