IndexedDB を使用した UI 要素のデータ バインディング

はじめに

IndexedDB は、クライアント側にデータを保存する優れた方法です。このトピックに関する MDN チュートリアルをまだご覧になっていない場合は、ぜひお読みください。この記事は、API と機能に関する基本的な知識があることを前提としています。IndexedDB を初めて使用する方も、この記事のデモで IndexedDB で何ができるかをご理解いただけたら幸いです。

このデモは、企業のシンプルなイントラネット アプリケーションの概念実証です。このアプリケーションでは、従業員が他の従業員を検索できるようになります。より迅速かつ快適なエクスペリエンスを提供するため、従業員データベースはクライアントのマシンにコピーされ、IndexedDB を使用して保存されます。このデモでは、予測入力形式で 1 人の従業員レコードを検索して表示するだけですが、このデモの優れている点は、このデータがクライアントで利用できると、他のさまざまな方法でも使用できることです。以下は、アプリケーションで行わなければならないことの基本的な概要です。

  1. IndexedDB のインスタンスを設定して初期化する必要があります。ほとんどの場合は簡単ですが、Chrome と Firefox の両方で動作させるには少し注意が必要です。
  2. データがあるかどうかを確認し、ない場合はダウンロードする必要があります。これは通常、AJAX 呼び出しを使用して行われます。このデモでは、架空のデータをすばやく生成するシンプルなユーティリティ クラスを作成しました。アプリケーションはこのデータを作成しているタイミングを認識し、それまでユーザーがデータを使用できないようにする必要があります。この作業を行うのは 1 回限りです。次回ユーザーがアプリケーションを実行するときには、このプロセスを行う必要はありません。より高度なデモではクライアントとサーバー間の同期オペレーションを処理しますが、このデモでは UI の側面に重点を置いています。
  3. アプリケーションの準備ができたら、jQuery UI の Autocomplete コントロールを使用して IndexedDB と同期できます。予測入力コントロールでは基本的なデータのリストや配列を作成できますが、API ではあらゆるデータソースに対応できます。これを使用して IndexedDB データに接続する方法を紹介します。

スタートガイド

このデモは複数のパートに分かれています。手始めに、まずは HTML の部分を見てみましょう。

<form>
  <p>
    <label for="name">Name:</label> <input id="name" disabled> <span id="status"></span>
    </p>
</form>

<div id="displayEmployee"></div>

そんなに多くないよね。この UI で注目すべき主な要素は 3 つあります。1 つ目は、予測入力に使用する「name」フィールドです。読み込みは無効になり、後で JavaScript によって有効になります。横にあるスパンは、ユーザーに最新情報を提供するために最初のシード時に使用されます。最後に、自動候補から従業員を選択すると、displayEmployee という ID の div が使用されます。

次に、JavaScript について見ていきましょう。取り上げるべきことがたくさんあるため、順を追って説明していきます。コードは最後にあるので、全体を確認できます。

まず、IndexedDB をサポートしているブラウザには、接頭辞に関する問題があります。以下に、Mozilla のドキュメントに記載されているコードを、アプリケーションが必要とする IndexedDB のコア コンポーネントにシンプルなエイリアスを指定するために変更した例を示します。

window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;

次に、このデモ全体で使用するグローバル変数をいくつか示します。

var db;
var template;

次に、jQuery document ready ブロックから開始します。

$(document).ready(function() {
  console.log("Startup...");
  ...
});

このデモでは、Handlebars.js を使用して従業員の詳細を表示します。これは後で使用しませんが、ここでテンプレートをコンパイルして、作業の邪魔にならないようにすることができます。Handlebars 認識型として設定されたスクリプト ブロックがあります。それほど複雑ではありませんが、ダイナミック HTML を表示しやすくなります。

<h2>, </h2>
Department: <br/>
Email: <a href='mailto:'></a>

これを JavaScript で次のようにコンパイルして戻します。

//Create our template
var source = $("#employeeTemplate").html();
template = Handlebars.compile(source);

それでは、IndexedDB の使用を始めましょう。まず、ファイルを開きます。

var openRequest = indexedDB.open("employees", 1);

IndexedDB への接続を開くと、データの読み取りと書き込みにアクセスできるようになりますが、その前に、objectStore があることを確認する必要があります。objectStore はデータベース テーブルのようなものです。1 つの IndexedDB に多数の objectStore を含めることができ、それぞれが関連するオブジェクトのコレクションを保持します。このデモはシンプルで、「employee」という名前の objectStore が 1 つだけです。indexdDB が初めて開かれたとき、またはコード内のバージョンが変更されたときに、onupgradeneeded イベントが実行されます。これを使用して、objectStore を設定できます。

// Handle setup.
openRequest.onupgradeneeded = function(e) {

  console.log("running onupgradeneeded");
  var thisDb = e.target.result;

  // Create Employee
  if(!thisDb.objectStoreNames.contains("employee")) {
    console.log("I need to make the employee objectstore");
    var objectStore = thisDb.createObjectStore("employee", {keyPath: "id", autoIncrement: true});
    objectStore.createIndex("searchkey", "searchkey", {unique: false});
  }

};

openRequest.onsuccess = function(e) {
  db = e.target.result;

  db.onerror = function(e) {
    alert("Sorry, an unforseen error was thrown.");
    console.log("***ERROR***");
    console.dir(e.target);
  };

  handleSeed();
};

onupgradeneeded イベント ハンドラ ブロックで、オブジェクト ストアの配列である objectStoreNames に employee が含まれているかどうかを確認します。そうでない場合は、単純にそのようにしています。createIndex の呼び出しが重要です。IndexedDB に、キー以外でデータの取得に使用するメソッドを指示する必要があります。「searchkey」を使用しますこれについては少し説明します。

onungradeneeded イベントは、スクリプトを初めて実行したときに自動的に実行されます。ハンドラが実行されるか、以降の実行時にスキップされると、onsuccess ハンドラが実行されます。単純で不細工なエラーハンドラを定義してから、handleSeed を呼び出します。

先に進む前に、ここで起きていることを簡単に確認しましょう。データベースを開きます。オブジェクト ストアが存在するかどうかを確認します。存在しない場合は Google が作成します。最後に、handleSeed という名前の関数を呼び出します。では、このデモのデータ シードの部分に目を向けましょう。

データを見せて!

この記事の冒頭で述べたように、このデモでは、既知の全従業員のコピーを保存する必要があるイントラネット スタイルのアプリケーションを再作成しています。そのためには通常、従業員のカウントを返して一連のレコードを取得する方法を提供するサーバーベースの API を作成する必要があります。開始カウントをサポートし、一度に 100 人を返すシンプルなサービスを想像してみてください。これは、ユーザーが他の作業を中断している間、バックグラウンドで非同期に実行されることがあります。

このデモでは、簡単なことを行います。IndexedDB 内にあるオブジェクトの数を確認します。一定の数字を下回る場合は、偽のユーザーが作成されます。それ以外の場合は、シード部分は完了したと見なされ、デモの予測入力部分を有効にできます。handleSeed を見てみましょう

function handleSeed() {
  // This is how we handle the initial data seed. Normally this would be via AJAX.

  db.transaction(["employee"], "readonly").objectStore("employee").count().onsuccess = function(e) {
    var count = e.target.result;
    if (count == 0) {
      console.log("Need to generate fake data - stand by please...");
      $("#status").text("Please stand by, loading in our initial data.");
      var done = 0;
      var employees = db.transaction(["employee"], "readwrite").objectStore("employee");
      // Generate 1k people
      for (var i = 0; i < 1000; i++) {
         var person = generateFakePerson();
         // Modify our data to add a searchable field
         person.searchkey = person.lastname.toLowerCase();
         resp = employees.add(person);
         resp.onsuccess = function(e) {
           done++;
           if (done == 1000) {
             $("#name").removeAttr("disabled");
             $("#status").text("");
             setupAutoComplete();
           } else if (done % 100 == 0) {
             $("#status").text("Approximately "+Math.floor(done/10) +"% done.");
           }
         }
      }
    } else {
      $("#name").removeAttr("disabled");
      setupAutoComplete();
    }
  };
}

最初の行は、複数のオペレーションが互いに連結されているので少し複雑です。細かく見てみましょう。

db.transaction(["employee"], "readonly");

これにより、新しい読み取り専用トランザクションが作成されます。IndexedDB を使用したすべてのデータ オペレーションでは、なんらかのトランザクションが必要です。

objectStore("employee");

従業員オブジェクト ストアを取得します。

count()

count API を実行します。これはおわかりのとおり、カウントを実行します。

onsuccess = function(e) {

完了したら、このコールバックを実行します。コールバック内では、オブジェクトの数である結果値を取得できます。カウントがゼロの場合は、シードプロセスを開始します。

先ほど説明したステータス div を使用して、データの取得を開始することをユーザーに知らせます。IndexedDB は非同期の性質を持つため、追加を追跡するシンプルな変数を設定しました。ループを回して偽の人物を挿入します。この関数のソースはダウンロードできますが、次のようなオブジェクトが返されます。

{
  firstname: "Random Name",
  lastname: "Some Random Last Name",
  department: "One of 8 random departments",
  email: "first letter of firstname+lastname@fakecorp.com"
}

それだけで人物を定義できます。しかし、データを検索できるようにするには、特別な要件があります。IndexedDB では、大文字と小文字を区別せずにアイテムを検索する方法はありません。そのため、lastname フィールドを新しいプロパティ searchkey にコピーします。すでに説明したように、これはデータのインデックスとして作成する必要があるキーです。

// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();

これはクライアント固有の変更なので、バックエンド サーバー(この場合は架空のバックエンド サーバー)ではなく、ここで行います。

データベースの追加を効率良く実行するには、すべてのバッチ書き込みでトランザクションを再利用する必要があります。書き込みのたびに新しいトランザクションを作成すると、ブラウザはトランザクションごとにディスク書き込みを発生させ、多くのアイテムを追加するときにパフォーマンスが低下する可能性があります(「1,000 個のオブジェクトを書き込むのに 1 分かかる」と考えると、最悪です)。

シードの実行が完了すると、アプリケーションの次の部分である setupAutoComplete が実行されます。

予測入力を作成する

それでは、jQuery UI Autocomplete プラグインを使用してみましょう。ほとんどの jQuery UI と同様に、基本的な HTML 要素から始め、その要素に対してコンストラクタ メソッドを呼び出して拡張します。ここではプロセス全体を setupAutoComplete という関数に抽象化しました。では、そのコードを見てみましょう。

function setupAutoComplete() {

  //Create the autocomplete
  $("#name").autocomplete({
    source: function(request, response) {

      console.log("Going to look for "+request.term);

      $("#displayEmployee").hide();

      var transaction = db.transaction(["employee"], "readonly");
      var result = [];

      transaction.oncomplete = function(event) {
        response(result);
      };

      // TODO: Handle the error and return to it jQuery UI
      var objectStore = transaction.objectStore("employee");

      // Credit: http://stackoverflow.com/a/8961462/52160
      var range = IDBKeyRange.bound(request.term.toLowerCase(), request.term.toLowerCase() + "z");
      var index = objectStore.index("searchkey");

      index.openCursor(range).onsuccess = function(event) {
        var cursor = event.target.result;
        if(cursor) {
          result.push({
            value: cursor.value.lastname + ", " + cursor.value.firstname,
            person: cursor.value
          });
          cursor.continue();
        }
      };
    },
    minLength: 2,
    select: function(event, ui) {
      $("#displayEmployee").show().html(template(ui.item.person));
    }
  });

}

このコードの最も複雑な部分は、参照元プロパティの作成です。jQuery UI の Autocomplete コントロールを使用すると、ソース プロパティを定義できます。ソース プロパティは、どのようなニーズ(IndexedDB データも含む)に応じてカスタマイズできます。API はリクエスト(基本的にはフォーム フィールドに入力された内容)とレスポンスのコールバックを提供します。そのコールバックに結果の配列を返す必要があります。

まず、displayEmployee div を非表示にします。この div を使用して個々の従業員を表示し、以前に読み込んでいる場合は消去します。それでは検索を始めましょう

まず、読み取り専用トランザクション、result という配列、結果を予測入力コントロールに渡す oncomplete ハンドラを作成します。

入力に一致するアイテムを見つけるために、StackOverflow ユーザーの Fong-Wan Chau によるヒントを利用します。入力に基づくインデックス範囲を下限として、入力と文字 z を上限範囲の境界として使用します。ここでも、入力した小文字のデータに合わせて用語を小文字にしています。

完了したら、カーソルを開き(データベース クエリを実行するようなものだと考えて)、結果を反復処理できます。jQuery UI の予測入力コントロールを使用すると、あらゆる種類のデータを返すことができますが、少なくとも value キーが必要です。値を適切な形式の名前に設定します。また、人全体を返します。その理由は後で説明します。まず、こちらは実際の予測入力画面のスクリーンショットです。jQuery UI には Vader テーマを使用します。

これで、IndexedDB のマッチ結果が予測入力に返されるのに十分です。ただし、一致が選択されたときに詳細ビューを表示することもサポートする必要があります。先ほどの Handlebars テンプレートを使用する予測入力を作成するときに、select ハンドラを指定しました。