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

はじめに

IndexedDB は、クライアントサイドにデータを保存するための強力な方法です。まだご覧になっていない場合は、このトピックに関する MDN チュートリアルを読むことをおすすめします。この記事は、API と機能に関する基本的な知識があることを前提としています。IndexedDB を初めて目にする方でも、この記事のデモをご覧になれば、IndexedDB で何ができるかをご理解いただけると思います。

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

  1. IndexedDB のインスタンスを設定して初期化する必要があります。ほとんどの場合、これは簡単ですが、Chrome と Firefox の両方で動作させるには少し工夫が必要です。
  2. データがあるかどうかを確認し、ない場合はダウンロードする必要があります。通常、これは AJAX 呼び出しを介して行われます。このデモでは、フェイクデータをすばやく生成するためのシンプルなユーティリティ クラスを作成しました。アプリケーションは、このデータが作成されるタイミングを認識し、それまでユーザーがデータを使用できないようにする必要があります。この操作が必要なのは一度だけです。次回ユーザーがアプリケーションを実行するときは、このプロセスを実行する必要はありません。より高度なデモでは、クライアントとサーバー間の同期オペレーションを処理しますが、このデモでは 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 を介して有効になります。その横の span は、最初のシード中にユーザーに更新情報を提供するために使用されます。最後に、予測入力から従業員を選択すると、ID が displayEmployee の 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 を含めることができ、各 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 をチェックして、employee が含まれているかどうかを確認します。そうでない場合は、そうなるようにします。createIndex 呼び出しは重要です。キー以外に、データの取得に使用するメソッドを IndexedDB に指示する必要があります。ここでは、searchkey という名前のキーを使用します。これについては、後ほど説明します。

onungradeneeded イベントは、スクリプトを初めて実行するときに自動的に実行されます。実行されるか、今後の実行でスキップされると、onsuccess ハンドラが実行されます。シンプルな(そして不格好な)エラーハンドラを定義し、handleSeed を呼び出します。

先に進む前に、ここで何が起こっているのかを簡単に確認しておきましょう。データベースを開きます。オブジェクト ストアが存在するかどうかを確認します。存在しない場合は作成します。最後に、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 は非同期であるため、追加を追跡する単純な変数 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 の予測入力コントロールでは、任意のタイプのデータを返すことができますが、少なくとも値キーが必要です。値を、名前の適切な形式のバージョンに設定します。また、人物全体も返されます。その理由は後ほど説明します。まず、予測入力の動作を示すスクリーンショットをご覧ください。jQuery UI には Vader テーマを使用しています。

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