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

はじめに

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

このデモは、企業向けのシンプルな概念実証のイントラネット アプリケーションです。このアプリでは、社員が他の社員を検索できます。より迅速でスムーズなエクスペリエンスを提供するため、従業員データベースはクライアントのマシンにコピーされ、IndexedDB を使用して保存されます。このデモでは、単純に自動入力スタイルの検索と単一の従業員レコードの表示を提供していますが、このデータをクライアントで利用できるようになれば、他のさまざまな方法でも使用できます。アプリケーションで行う必要がある基本的な処理の概要は次のとおりです。

  1. IndexedDB のインスタンスを設定して初期化する必要があります。ほとんどの場合、これは簡単ですが、Chrome と Firefox の両方で動作させるのは少し難しいです。
  2. データがあるかどうかを確認し、ない場合はダウンロードする必要があります。通常、これは AJAX 呼び出しで実行されます。このデモでは、偽のデータをすばやく生成するためのシンプルなユーティリティ クラスを作成しました。アプリは、このデータを作成するタイミングを認識し、それまではユーザーがデータを使用できないようにする必要があります。この操作が必要なのは一度だけです。ユーザーが次回アプリを実行するときに、このプロセスを実行する必要はありません。より高度なデモでは、クライアントとサーバー間の同期オペレーションを処理しますが、このデモでは UI の側面に重点を置いています。
  3. アプリケーションの準備ができたら、jQuery UI のオートコンプリート コントロールを使用して 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 には、Google が重視している主な要素が 3 つあります。1 つ目は、自動入力に使用されるフィールド「name」です。読み込み時に無効になり、後で JavaScript によって有効になります。隣接するスパンは、最初のシード時にユーザーに更新情報を提供するために使用されます。最後に、自動補完から従業員を選択するときに、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(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 があり、それぞれが関連するオブジェクトのコレクションを保持します。このデモはシンプルで、1 つの objectStore(「employee」という名前)のみが必要です。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 という関数を呼び出します。では、デモのデータ シーディング部分に移りましょう。

Gimme Some Data!

この記事の冒頭で説明したように、このデモでは、既知のすべての社員のコピーを保存する必要がある、イントラネット スタイルのアプリケーションを再作成します。通常、これは、従業員の数を返すことができ、レコードのバッチを取得する方法を提供できるサーバーベースの 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)が呼び出されます。

Autocomplete を作成する

次に、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 を非表示にします。これは、個々の社員を表示するために使用され、以前に読み込まれた社員が存在する場合は、その社員を消去するために使用されます。これで検索を開始できます。

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

入力に一致するアイテムを見つけるには、StackOverflow ユーザーの Fong-Wan Chau によるヒントを活用しましょう。入力に基づくインデックス範囲を下限として、入力に文字 z を追加した範囲を上限として使用します。また、入力したデータに合わせて、キーワードは小文字に変換されています。

準備ができたら、カーソルを開き(データベース クエリの実行と考えてください)、結果を反復処理します。jQuery UI の自動入力コントロールでは、任意のデータ型を返すことができますが、少なくとも値キーが必要です。値は、名前の適切な書式設定されたバージョンに設定します。また、人物全体も返されます。その理由は後ほど説明します。まず、オートコンプリートが動作している様子のスクリーンショットをご紹介します。jQuery UI には Vader テーマを使用しています。

これだけで、IndexedDB の一致結果を自動入力に返すことができます。また、一致が選択された場合に、その一致の詳細ビューを表示できるようにしたいと考えています。前述の Handlebars テンプレートを使用した自動入力を作成するときに、選択ハンドラを指定しました。