コハム

Webクリエイターのコハムが提供する、Web制作に役立つ情報をまとめたブログです。

知らないと損する最強のクライアントサイドストレージ ~ IndexedDB完全ガイド ~

A complete guide to using IndexedDB

記事は上記記事を意訳したものです。

※当ブログでの翻訳記事は元サイト様に許可を得て掲載しています。


データストレージは、ユーザーデータからアプリケーションデータまで、ほとんどのWebアプリケーションにおいて重要な部分です。より高速で堅牢なWebアプリケーションの急速な開発に伴い、効率的なクライアントストレージが開発を支援するために必要となっています。

Webにおけるクライアントサイドストレージは、長年にわたって進化してきました。ユーザーデータを保存するためのクッキーから、ブラウザ内にSQLデータベースを保存することを可能にし、SQLに精通したユーザーが容易に堅牢なアプリケーションを構築できるようにしたWebSQL(現在は非推奨)の登場まで発展してきました。

IndexedDBはWebSQLの代替であり、前身よりも多くのストレージ容量を提供します。このチュートリアルでは、WebアプリケーションのデータストレージにIndexedDBを使用・設定する方法と、利用可能なAPIを使用してそのデータを操作する方法を探ります。

このチュートリアルで作成したプロジェクトが含まれる公開GitHubリポジトリへのリンクを見つけることができます。以下の内容をカバーします:

  1. IndexedDBとは?
  2. プロジェクトのセットアップ
  3. IndexedDBデータベースへのデータ保存
  4. データの取得と表示
  5. データベースからのデータ削除
  6. IndexedDBバージョンのインクリメント
  7. IndexedDB使用の欠点

それでは始めましょう!

IndexedDBとは?

IndexedDBは、クライアントサイドストレージのための低レベルAPIです。ブラウザで利用可能な完全な永続的NoSQLストレージシステムであり、以下のような様々な種類のデータを保存することができます:

  • ファイルやブロブ
  • 画像や動画
  • オブジェクト、リスト、配列などの構造化データ

IndexedDBは、キャッシュ、PWA、ゲームなど、様々なシナリオで使用でき、トランザクションもサポートしています。Webアプリの多様なニーズに効果的に対応するよう開発されています。

プロジェクトのセットアップ

IndexedDBはWeb上でネイティブに動作するため、特別なセットアップは必要ありません。まず、プロジェクトを格納する新しいディレクトリを作成します:

mkdir indexed-db && cd indexed-db

次に、アプリケーションを表示するためのindex.htmlファイルと、アプリケーションロジックを格納するindex.jsスクリプトファイルを作成します:

touch index.html index.js styles.css

IndexedDBデータベースへのデータ保存

このデータベースの利点を確認し、APIとの対話方法を学ぶために、基本的なTodoアプリケーションを作成します。データベースにデータを保存する方法を確認するために「追加」機能を有効にし、すべてのTodoを表示する機能と、APIのGETおよびDELETE機能をそれぞれ確認するための「削除」機能を追加します。

前のセクションで作成したindex.htmlを開き、以下のコードを追加します:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TODO APP</title>
    <script src="index.js" defer></script>
    <link href="style.css" rel="stylesheet">
</head>
<body>
    <h1>TODO APP</h1>
    <section>
        <aside class="view">
            <h2>TODOs</h2>
            <div class="todos">
                <ol></ol>
            </div>
        </aside>
        <aside class="add"> 
            <h2>Add Todo</h2>
            <form>
              <div>
                <label for="title">Todo title</label>
                <input id="title" type="text" required>
              </div>
              <div>
                <label for="desc">Todo description</label>
                <input id="desc" type="text" required>
              </div>
              <div>
                <button>Save</button>
              </div>
            </form>
        </aside>
    </section>
</body>
</html>

これにより、Webアプリの基本構造が作成されます。ここでは主に2つのことを行います:まず、データベースに保存されているすべてのTodoを表示/閲覧するセクションを作成し、次に、データベースにTodoを追加するセクションを作成します。

アプリケーションに基本的なスタイリングも追加しましょう。styles.cssファイルを開き、以下を追加します:

html {
  font-family: sans-serif;
}

body {
  margin: 0 auto;
  max-width: 800px;
}

header, footer {
  background-color: blue;
  color: white;
  padding: 0 20px;
}

.add, .view {
  padding: 30px;
  width: 40%;
}

.add {
  background: #ebe6e6; 
}
section {
  padding: 10px;
  background: #3182d4;
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: center;
}

h1 {
  margin: 0;
}

ol {
  list-style-type: none;
}

div {
  margin-bottom: 10px;
}

index.jsファイルはアプリケーションの心臓部であり、アプリとIndexedDBの間の対話ロジックが含まれています。

まず、データベースを作成する必要があります。次に、各アイテムの詳細を保存するためのオブジェクトストア(SQLのテーブルに似ています)を作成して初期化できます。index.jsファイルを開き、以下のロジックを追加します:

let db;
const openOrCreateDB = window.indexedDB.open('todo_db', 1);

openOrCreateDB.addEventListener('error', () => console.error('Error opening DB'));

openOrCreateDB.addEventListener('success', () => {
  console.log('Successfully opened DB');
  db = openOrCreateDB.result;
});

openOrCreateDB.addEventListener('upgradeneeded', init => {
  db = init.target.result;

  db.onerror = () => {
    console.error('Error loading database.');
  };

  const table = db.createObjectStore('todo_tb', { keyPath: 'id', autoIncrement:true });

  table.createIndex('title', 'title', { unique: false });
  table.createIndex('desc', 'desc', { unique: false });
});

上記のように、todo_dbという名前のデータベースが作成され、次にtodo_tbという名前のオブジェクトストアが2つのインデックス、titleとdescで作成されます。これらのインデックスは、ストア内で値の重複を許可しており、SQLでテーブルを作成し、2つの列を作成するのと似ています。

次に、保存機能を追加するために、フォームに入力された値を取得し、データベースに保存します:

const todos = document.querySelector('ol');
const form = document.querySelector('form');
const todoTitle = document.querySelector('#title');
const todoDesc = document.querySelector('#desc');
const submit = document.querySelector('button');

form.addEventListener('submit', addTodo);

function addTodo(e) {
  e.preventDefault();
  const newTodo = { title: todoTitle.value, body: todoDesc.value };
  const transaction = db.transaction(['todo_tb'], 'readwrite');
  const objectStore = transaction.objectStore('todo_tb');
  const query = objectStore.add(newTodo);
  query.addEventListener('success', () => {
    todoTitle.value = '';
    todoDesc.value = '';
  });
  transaction.addEventListener('complete', () => {
    showTodos();
  });
  transaction.addEventListener('error', () => console.log('Transaction error'));
}

ストアに値を追加した後、ユーザーが新しいアイテムをリストに入力できるように、2つのフォームフィールドを空にします。次のセクションで説明するshowTodosメソッドを呼び出すことで、ビューを更新できます。

データの取得と表示

「Todo保存」機能が動作したことを確認するには、ブラウザの検査機能を開いて使用します。Chromeでは、ApplicationタブのStorageの下にIndexedDBを見ることができます。以下の画像のように、データベースを作成し、最初のTodoをtodo_tbオブジェクトストアに保存しました:

ページ読み込み時に利用可能なTodoを表示し、以前に追加および削除されたTodoのビューを提供するために、showTodosというメソッドを作成します:

function showTodos() {
  while (todos.firstChild) {
    todos.removeChild(todos.firstChild);
  }
  const objectStore = db.transaction('todo_tb').objectStore('todo_tb');
  objectStore.openCursor().addEventListener('success', e => {

    const pointer = e.target.result;
    if(pointer) {
      const listItem = document.createElement('li');
      const h3 = document.createElement('h3');
      const pg = document.createElement('p');
      listItem.appendChild(h3);
      listItem.appendChild(pg);
      todos.appendChild(listItem);
      h3.textContent = pointer.value.title;
      pg.textContent = pointer.value.body;
      listItem.setAttribute('data-id', pointer.value.id);
      const deleteBtn = document.createElement('button');
      listItem.appendChild(deleteBtn);
      deleteBtn.textContent = 'Remove';
      deleteBtn.addEventListener('click', deleteItem);
      pointer.continue();
    } else {
      if(!todos.firstChild) {
        const listItem = document.createElement('li');
        listItem.textContent = 'No Todo.'
        todos.appendChild(listItem);
      }

      console.log('Todos all shown');
    }
  });
}

このメソッドは、ストアからTodoを取得し、各アイテムをループして、それぞれのHTMLエレメントを作成します。各アイテムをWebページ上のol要素に追加し、各Todoのユニークなidをdata-idという名前のデータ属性に渡します。後でdeleteItem関数を扱う際に、このユニークなIDを使用して、ストアから削除する必要があるときに各Todoを識別します。

ページ読み込み時にTodoを取得するために、openOrCreateDBの成功イベントリスナーを以下のように修正します:

openOrCreateDB.addEventListener('success', () => {
  console.log('Successfully opened DB');
  db = openOrCreateDB.result;
  showTodos();
});

データベースからのデータ削除

最後に、このデータベースのDELETE APIをテストし、Todoリストアプリ用の削除機能を作成しましょう:

function deleteItem(e) {
  const todoId = Number(e.target.parentNode.getAttribute('data-id'));
  const transaction = db.transaction(['todo_tb'], 'readwrite');
  const objectStore = transaction.objectStore('todo_tb');
  objectStore.delete(todoId);
  transaction.addEventListener('complete', () => {
    e.target.parentNode.parentNode.removeChild(e.target.parentNode);
    alert(`Todo with id of ${todoId} deleted`)
    console.log(`Todo:${todoId} deleted.`);
    if(!todos.firstChild) {
      const listItem = document.createElement('li');
      listItem.textContent = 'No Todo.';
      todos.appendChild(listItem);
    }
  });
  transaction.addEventListener('error', () => console.log('Transaction error'));
}

これは、メソッドに渡されたユニークなIDを使用して特定のTodoを削除し、Webページから要素を削除します。ストア内の最後のTodoアイテムを削除すると、Todoリストの代わりに「Todoなし」というメッセージを表示します。

Todoがデータベースから削除されたことを確認するには、Webページを検査し、Applicationタブをクリックします。見ると分かるように、todo_tbオブジェクトストアには現在アイテムが含まれていません:

最終的なWebアプリケーションは以下のようになります:

IndexedDBバージョンのインクリメント

IndexedDBでは、開発者がデータベースのバージョンをインクリメントすることも可能です。データベースを開く際に、希望するバージョン番号を指定します:

window.indexedDB.open('todo_db', 1);

データベースが存在しない場合、指定されたバージョンで作成されます。データベースがすでに存在する場合、バージョン番号がチェックされます。

open メソッド呼び出し時に指定されたバージョン番号が既存のバージョンよりも高い場合、onUpgradeNeeded イベントを介してバージョン変更イベントがトリガーされます。このイベントにより、データベーススキーマの変更やデータ移行を実行することができます。

ここで注意すべき点は、新しいストアを作成する際に新しいオプションを追加するために以前のオブジェクトストアを削除すると、古いストア内の他のすべてのデータも削除されてしまうことです。データベースをアップグレードする前に、古いコンテンツを読み出して他の場所に保存するよう注意してください。

IndexedDB使用の欠点

IndexedDBはクライアントのWebブラウザに依存しているため、通常は個人ユーザーや小規模なアプリケーションに適しています。かなりの量のデータを扱うことができますが、大規模なアプリケーションや多数の人々が使用するアプリケーションでIndexedDBを使用する場合には、いくつかの考慮事項があります。

スケーラビリティの制限

IndexedDBはWebブラウザ内で動作するため、クライアントサイド環境の機能とリソースに制限されます。同時に多数のユーザーを処理する必要がある、または非常に高いスループットが必要なシナリオには適していない可能性があります。

データサイズの制限

異なるWebブラウザは、IndexedDBに保存できるデータの最大量に制限を設けています。これらの制限はブラウザによって異なり、数メガバイトから数百メガバイトまで範囲があります。

これらの制限を認識し、それに応じてアプリケーションを設計することが非常に重要です。データストレージが不足すると、QuotaExceededErrorエラーが発生し、データベースに新しいデータを保存できなくなります。

同期の課題

IndexedDBは、クライアント間のデータ同期や分散環境での競合の処理のためのビルトインメカニズムを提供していません。これらのシナリオを処理するためには、カスタムの同期ロジックを実装する必要があります。アプリケーションの異なるインスタンス間でのデータの一貫性と同期の維持が複雑になります。

したがって、より大規模なアプリケーションや複数の人が使用するアプリケーションの場合、サーバーサイドのデータベースやクラウドベースのストレージソリューションを使用する方が効率的です。

結論

この記事では、Web上のデータベースであるIndexedDBについて学び、JavaScriptを使用してWebアプリケーションデータを保存するためにどのように対話するかを学びました。

この記事を楽しんでいただき、Web上でアプリケーションデータをローカルに管理する新しい方法を学んでいただければ幸いです。お読みいただきありがとうございました!

©コハム