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 で有効にします。その隣のスパンは、最初のシード中にユーザーにアップデートを提供するために使用されます。最後に、ID が displayEmployee の div が、自動候補から従業員を選択するときに使用されます。

では、JavaScript を見てみましょう。理解すべきことがたくさんあるので、順を追って見ていきます。コードの全体は、最後に記載されているので、全体を確認できます。

まず、IndexedDB をサポートするブラウザには、接頭辞の問題がいくつかあります。ここでは、アプリケーションに必要なコア IndexedDB コンポーネントにシンプルなエイリアスを提供するように変更した Mozilla ドキュメントのコードの一部を示します。

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 があることを確認する必要があります。オブジェクトストアはデータベース テーブルのようなものです。1 つの IndexedDB に多数の objectStore があり、それぞれが関連オブジェクトのコレクションを保持します。このデモはシンプルで、「employee」と呼ばれる 1 つの objectStore しか必要としません。indexedDB を初めて開いたとき、またはコードのバージョンを変更したときに、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 に従業員が含まれているかどうかを確認します。そうでない場合は、単純に処理を行います(createIndex 呼び出しが重要です)。キーの外部でデータの取得に使用するメソッドを IndexedDB に伝える必要があります。検索キーというキーを使用しますこれについて簡単に説明します。

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();
    }
  };
}

1 行目は複数のオペレーションが連結されているため、やや複雑です。詳しく見ていきましょう。

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

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

objectStore("employee");

employee オブジェクト ストアを取得します。

count()

count API を実行します。この API は、おわかりのように、カウントを行います。

onsuccess = function(e) {

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

前述のステータス div を使用して、データの取得が開始されることをユーザーに知らせます。IndexedDB の非同期の性質上、追加を追跡する単純な変数 done が設定されています。ループ オーバーして、架空の人物を挿入します。この関数のソースはダウンロードで参照できますが、次のようなオブジェクトが返されます。

{
  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 のオートコンプリート コントロールを使用すると、必要に応じて(IndexedDB データも含め)必要に応じてカスタマイズできるソース プロパティを定義できます。API はリクエスト(基本的にはフォーム フィールドに入力された内容)とレスポンスのコールバックを提供します。このコールバックに結果の配列を返す必要があります。

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

まず、読み取り専用トランザクション、result という配列、結果をオートコンプリート コントロールに渡すだけの oncomplete ハンドラを作成します。

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

完了したら、カーソルを開いて(データベース クエリの実行のようなもの)、結果を反復処理します。jQuery UI のオートコンプリート コントロールを使用すると、あらゆる種類のデータを返すことができますが、少なくとも value キーが必要です。値は、適切にフォーマットされた名前に設定します。また、そのユーザー全体を返します。理由はすぐにわかります。まず、ここに示すスクリーンショットは、オートコンプリートの実際の例です。jQuery UI に Vader テーマを使用します。

オートコンプリートに一致した IndexedDB の結果を返すには、これで十分です。ただし、一致が選択されたときに詳細ビューを表示できるようにする必要もあります。先ほどの Handlebars テンプレートを使用するオートコンプリートを作成するときに、選択ハンドラを指定しました。