Google Base と Google Gears を使用してパフォーマンスの高いオフライン エクスペリエンスを実現する

「Google API を使用して優れた Ajax アプリケーションを構築する」シリーズの最初の記事。

Dion Almaer、Pamela Fox(Google)
2007 年 6 月

編集者注: Google Gears API はご利用いただけなくなりました

はじめに

Google Base と Google Gears を組み合わせて、オフラインで使用できるアプリケーションを作成する方法を説明します。この記事を読むと、Google Base API について詳しくなり、Google Gears を使用してユーザー設定やデータを保存、アクセスする方法を理解できます。

アプリについて

このアプリを理解するには、まず Google Base について知っておく必要があります。これは、商品、レビュー、レシピ、イベントなど、さまざまなカテゴリにわたるアイテムの大きなデータベースです。

各アイテムには、タイトル、説明、データの元のソースへのリンク(存在する場合)、およびカテゴリ タイプごとに異なる追加属性が注釈として付加されます。Google Base では、同じカテゴリのアイテムは共通の属性セットを共有するという事実を利用しています。たとえば、すべてのレシピには材料が含まれています。Google Base のアイテムは、Google ウェブ検索や Google 商品検索の検索結果に表示されることもあります。

デモアプリの Base with Gears では、Google Base で行う一般的な検索(「チョコレート」を使ったレシピや「ビーチを散歩」する人を探すなど)を保存して表示できます。これは、検索を登録して、アプリを再度開いたときや、アプリが 15 分ごとに更新されたフィードを探しに行ったときに、更新された結果を確認できる「Google ベース リーダー」と考えることができます。

アプリを拡張したいデベロッパーは、検索結果に新しい結果が含まれている場合にユーザーに視覚的に警告する機能、お気に入りのアイテム(オフラインとオンライン)をブックマーク(スター付き)できる機能、Google Base のようなカテゴリ固有の属性検索ができる機能などを追加できます。

Google Base データ API フィードの使用

Google Base は、Google Data API フレームワークに準拠した Google Base データ API を使用して、プログラムでクエリできます。Google Data API プロトコルは、ウェブでの読み取りと書き込みのためのシンプルなプロトコルを提供し、Picasa、スプレッドシート、Blogger、カレンダー、ノートブックなど、多くの Google サービスで使用されています。

Google Data API の形式は XML と Atom Publishing Protocol に基づいているため、読み取り/書き込みのインタラクションのほとんどは XML で行われます。

Google Data API に基づく Google Base フィードの例は次のとおりです。
http://www.google.com/base/feeds/snippets/-/products?bq=digital+camera

snippets フィードタイプは、一般公開されているアイテムのフィードを提供します。-/products を使用すると、フィードを商品カテゴリに制限できます。また、bq= パラメータを使用すると、フィードをさらに制限して、「デジタル カメラ」というキーワードを含む結果のみにすることができます。このフィードをブラウザで表示すると、一致する結果を含む <entry> ノードを含む XML が表示されます。各エントリには、一般的な著者、タイトル、コンテンツ、リンクの要素が含まれていますが、カテゴリ固有の追加属性(商品カテゴリのアイテムの「価格」など)も含まれています。

ブラウザの XMLHttpRequest のクロスドメイン制限により、JavaScript コードで www.google.com から XML フィードを直接読み取ることはできません。サーバーサイド プロキシを設定して XML を読み込み、アプリと同じドメインの場所に吐き出すこともできますが、サーバーサイド プログラミングは避けたいところです。幸いなことに、別の方法があります。

他の Google Data API と同様に、Google Base データ API には、標準の XML に加えて JSON 出力オプションがあります。JSON 形式で表示した、前述のフィードの出力は、次の URL になります。
http://www.google.com/base/feeds/snippets/-/products?bq=digital+camera&alt=json

JSON は、階層型ネストやさまざまなデータ型を可能にする軽量の交換形式です。さらに重要なのは、JSON 出力はネイティブの JavaScript コードそのものであるため、スクリプトタグで参照するだけでウェブページに読み込むことができ、クロスドメイン制限を回避できることです。

Google Data APIs では、JSON の読み込み後に実行するコールバック関数を使用して、「json-in-script」出力を指定することもできます。これにより、JSON 出力をさらに簡単に操作できるようになります。スクリプトタグをページに動的に追加し、それぞれに異なるコールバック関数を指定できるためです。

Base API の JSON フィードをページに動的に読み込むには、フィード URL(altcallback 値を追加)を含むスクリプトタグを作成してページに追加する次の関数を使用します。

function getJSON() {
  var script = document.createElement('script');

  var url = "http://www.google.com/base/feeds/snippets/-/products?bq=digital+camera";
  script.setAttribute('src', url + "&alt=json-in-script&callback=listResults");
  script.setAttribute('type', 'text/JavaScript');
  document.documentElement.firstChild.appendChild(script);
}

これで、コールバック関数 listResults は、唯一のパラメータとして渡された JSON を反復処理し、見つかった各エントリの情報を箇条書きで表示できるようになりました。

  function listTasks(root) {
    var feed = root.feed;
    var html = [''];
    html.push('<ul>');
    for (var i = 0; i < feed.entry.length; ++i) {
      var entry = feed.entry[i];
      var title = entry.title.$t;
      var content = entry.content.$t;
      html.push('<li>', title, ' (', content, ')</li>');
    }
    html.push('</ul>');

    document.getElementById("agenda").innerHTML = html.join("");
  }

Google Gears の追加

Google Data API を介して Google Base と通信できるアプリケーションができたので、このアプリケーションをオフラインで実行できるようにします。ここで Google Gears の出番です。

オフラインで動作するアプリケーションを作成する場合、さまざまなアーキテクチャの選択肢があります。オンラインとオフラインでアプリケーションがどのように動作するか(まったく同じように動作するか、など)について、自問自答することになります。検索などの一部の機能が無効になっている。同期はどのように処理しますか?)。

この例では、Gears を搭載していないブラウザのユーザーもアプリを使用できるようにしつつ、プラグインを搭載しているユーザーにはオフラインでの使用やよりレスポンスの良い UI のメリットを提供したいと考えています。

アーキテクチャは次のようになります。

  • 検索クエリの保存と、これらのクエリからの結果の返信を担当する JavaScript オブジェクトがあります。
  • Google Gears がインストールされている場合は、すべてをローカル データベースに保存する Gears バージョンが提供されます。
  • Google Gears がインストールされていない場合は、クエリを Cookie に保存し、結果全体は保存しないバージョンが提供されます(結果が Cookie に保存するには大きすぎるため、応答が若干遅くなります)。
このアーキテクチャの利点は、if (online) {} のチェックをショップ全体で行う必要がないことです。代わりに、アプリケーションには 1 つの Gears チェックがあり、その後で正しいアダプタが使用されます。


Gears ローカル データベースを使用する

Gears のコンポーネントの 1 つは、組み込みのローカル SQLite データベースです。シンプルなデータベース API が用意されています。MySQL や Oracle などのサーバーサイド データベースの API を使用したことがある方には、おなじみの API です。

ローカル データベースを使用する手順は非常に簡単です。

  • Google Gears オブジェクトを初期化する
  • データベース ファクトリ オブジェクトを取得してデータベースを開く
  • SQL リクエストの実行を開始する

これらを簡単に見ていきましょう。


Google Gears オブジェクトを初期化する

アプリケーションは、/gears/samples/gears_init.js の内容を直接読み取るか、コードを独自の JavaScript ファイルに貼り付ける必要があります。<script src="..../gears_init.js" type="text/JavaScript"></script> が起動すると、google.gears 名前空間にアクセスできるようになります。


データベース ファクトリ オブジェクトを取得してデータベースを開く
var db = google.gears.factory.create('beta.database', '1.0');
db.open('testdb');

この 1 回の呼び出しで、データベース スキーマを開くことができるデータベース オブジェクトが返されます。データベースを開くとき、同じオリジン ポリシー ルールでスコープが設定されるため、あなたの「testdb」が私の「testdb」と競合することはありません。


SQL リクエストの実行を開始

これで、データベースに SQL リクエストを送信する準備が整いました。「select」リクエストを送信すると、目的のデータを反復処理できる結果セットが返されます。

var rs = db.execute('select * from foo where name = ?', [ name ]);

返された結果セットは、次のメソッドで操作できます。

booleanisValidRow()
voidnext()
voidclose()
intfieldCount()
stringfieldName(int fieldIndex)
variantfield(int fieldIndex)
variantfieldByName(string fieldname)

詳しくは、Database Module API のドキュメントをご覧ください。(編集者注: Google Gears API はご利用いただけなくなりました)。


GearsDB を使用して低レベル API をカプセル化する

一般的なデータベース タスクをカプセル化して、より便利にしたいと考えていました。たとえば、

  • アプリケーションのデバッグ時に生成された SQL を適切にロギングする方法が必要でした。
  • 例外をあちこちで try{}catch(){} するのではなく、1 か所で処理したかったのです。
  • データの読み取りや書き込みの際に、結果セットではなく JavaScript オブジェクトを処理したいと考えていました。

これらの問題を汎用的に処理するために、Database オブジェクトをラップするオープンソース ライブラリである GearsDB を作成しました。ここでは、GearsDB の使用方法について説明します。

初期設定

window.onload コードでは、依存するデータベース テーブルが適切に設定されていることを確認する必要があります。次のコードが実行されたときに Gears がインストールされている場合、ユーザーは GearsBaseContent オブジェクトを作成します。

content = hasGears() ? new GearsBaseContent() : new CookieBaseContent();

次に、データベースを開き、テーブルが存在しない場合は作成します。

db = new GearsDB('gears-base'); // db is defined as a global for reuse later!

if (db) {
  db.run('create table if not exists BaseQueries' +
         ' (Phrase varchar(255), Itemtype varchar(100))');
  db.run('create table if not exists BaseFeeds' + 
         ' (id varchar(255), JSON text)');
}

この時点で、クエリとフィードを保存するテーブルが作成されていることを確認します。コード new GearsDB(name) は、指定された名前のデータベースを開く処理をカプセル化します。run メソッドは、下位レベルの execute メソッドをラップしますが、コンソールへのデバッグ出力と例外のトラップも処理します。


検索キーワードの追加

アプリを初めて実行したときは、検索結果は表示されません。商品で Nintendo Wii を検索しようとすると、この検索語句が BaseQueries テーブルに保存されます。

Gears バージョンの addQuery メソッドは、入力値を取得して insertRow で保存することで、この処理を行います。

var searchterm = { Phrase: phrase, Itemtype: itemtype };
db.insertRow('BaseQueries', searchterm); 

insertRow は JavaScript オブジェクト(searchterm)を受け取り、テーブルへの INSERT を処理します。また、制約(たとえば、複数の「Bob」の挿入をブロックする一意性)を定義することもできます。ただし、ほとんどの場合、これらの制約はデータベース自体で処理されます。


すべての検索語句を取得する

過去の検索のリストを表示するために、selectAll という名前の便利な select ラッパーを使用します。

GearsBaseContent.prototype.getQueries = function() {
  return this.db.selectAll('select * from BaseQueries');
}

これにより、データベース内の行と一致する JavaScript オブジェクトの配列([ { Phrase: 'Nintendo Wii', Itemtype: 'product' }, { ... }, ...] など)が返されます。

この場合は、リスト全体を返してもかまいません。ただし、データ量が多い場合は、select 呼び出しでコールバックを使用して、返された各行が到着するたびに処理できるようにすることをおすすめします。

 db.selectAll('select * from BaseQueries where Itemtype = ?', ['product'], function(row) {
  ... do something with this row ...
});

GearsDB で役立つその他の選択メソッドを次に示します。

selectOne(sql, args)一致する最初の JavaScript オブジェクトを返す
selectRow(table, where, args, select)通常、SQL を無視する簡単なケースで使用されます
selectRows(table, where, args, callback, select)selectRow と同じですが、複数の結果を対象とします。

フィードの読み込み

Google Base から結果フィードを取得したら、データベースに保存する必要があります。

content.setFeed({ id: id, JSON: json.toJSONString() });

... which calls ...

GearsBaseContent.prototype.setFeed = function(feed) {
  this.db.forceRow('BaseFeeds', feed);
}

まず、JSON フィードを取得し、toJSONString メソッドを使用して String として返します。次に、feed オブジェクトを作成し、forceRow メソッドに渡します。forceRow は、エントリが存在しない場合はエントリを INSERT し、既存のエントリを UPDATE します。


検索結果の表示

アプリでは、特定の検索結果がページの右側のパネルに表示されます。検索語句に関連付けられたフィードを取得する方法は次のとおりです。

GearsBaseContent.prototype.getFeed = function(url) {
  var row = this.db.selectRow('BaseFeeds', 'id = ?', [ url ]);
  return row.JSON;
}

行の JSON があるので、eval() してオブジェクトを取得できます。

eval("var json = " + jsonString + ";");

これで、JSON からコンテンツを innerHTML でページに挿入できるようになりました。


オフライン アクセスにリソース ストアを使用する

ローカル データベースからコンテンツを取得しているので、このアプリはオフラインでも動作するはずですよね?

いいえ。問題は、このアプリを起動するには、JavaScript、CSS、HTML、画像などのウェブ リソースを読み込む必要があることです。現在のところ、ユーザーが次の手順を行った場合、アプリは引き続き動作する可能性があります。オンラインで開始し、検索を行い、ブラウザを閉じずにオフラインに移行する。アイテムはブラウザのキャッシュに残っているため、この方法で対応できる可能性があります。しかし、そうでない場合はどうすればよいでしょうか?ユーザーが再起動後などにアプリに最初からアクセスできるようにしたいと考えています。

これを行うには、LocalServer コンポーネントを使用してリソースをキャプチャします。リソース(アプリケーションの実行に必要な HTML や JavaScript など)をキャプチャすると、Gears はこれらのアイテムを保存し、ブラウザからのリクエストをトラップしてそれらを返します。ローカル サーバーは交通整理の役割を果たし、ストアから保存されたコンテンツを返します。

また、ResourceStore コンポーネントも使用します。このコンポーネントでは、キャプチャするファイルをシステムに手動で指定する必要があります。多くのシナリオでは、アプリケーションをバージョン管理し、トランザクション方式でアップグレードできるようにする必要があります。リソースのセットでバージョンが定義されます。新しいリソースのセットをリリースするときは、ユーザーがファイルをシームレスにアップグレードできるようにする必要があります。このモデルを使用する場合は、ManagedResourceStore API を使用します。

リソースをキャプチャするために、GearsBaseContent オブジェクトは次の処理を行います。

  1. キャプチャが必要なファイルの配列を設定する
  2. LocalServer を作成する
  3. ResourceStore を開くか、新規作成する
  4. ページをストアにキャプチャするよう呼び出す
// Step 1
this.storeName = 'gears-base';
this.pageFiles = [
  location.pathname,
  'gears_base.js',
  '../scripts/gears_db.js',
  '../scripts/firebug/firebug.js',
  '../scripts/firebug/firebug.html',
  '../scripts/firebug/firebug.css',
  '../scripts/json_util.js',    'style.css',
  'capture.gif' ];

// Step 2
try {
  this.localServer = google.gears.factory.create('beta.localserver', '1.0');
} catch (e) {
  alert('Could not create local server: ' + e.message);
  return;
}

// Step 3
this.store = this.localServer.openStore(this.storeName) || this.localServer.createStore(this.storeName);

// Step 4
this.capturePageFiles();

... which calls ...

GearsBaseContent.prototype.capturePageFiles = function() {
  this.store.capture(this.pageFiles, function(url, success, captureId) {
    console.log(url + ' capture ' + (success ? 'succeeded' : 'failed'));
  });
}

ここで重要なのは、自分のドメインのリソースのみをキャプチャできることです。この制限は、SVN トランクの元の「gears_db.js」ファイルから GearsDB JavaScript ファイルに直接アクセスしようとしたときに発生しました。もちろん、解決策は簡単です。外部リソースをダウンロードして、ドメインの下に配置する必要があります。LocalServer は 200(成功)または 304(変更なし)のサーバーコードのみを受け入れるため、302 または 301 のリダイレクトは機能しません。

これには影響があります。画像を images.yourdomain.com に配置した場合、画像をキャプチャすることはできません。www1 と www2 は互いに認識できません。サーバーサイド プロキシを設定することもできますが、アプリケーションを複数のドメインに分割する目的が損なわれます。

オフライン アプリケーションのデバッグ

オフライン アプリケーションのデバッグは、少し複雑になります。テストするシナリオが増えました。

  • アプリがキャッシュで完全に実行されている状態でオンラインになっている
  • オンラインだがアプリにアクセスしておらず、キャッシュに何も保存されていない
  • オフラインですが、アプリにアクセスしました
  • オフラインで、アプリにアクセスしたことがない(あまり良くない状況です)。

次のパターンを使用すると、簡単にできます。

  • ブラウザがキャッシュから何かを拾い上げていないことを確認する必要がある場合は、Firefox(または選択したブラウザ)でキャッシュを無効にします。
  • Firebug(および他のブラウザでのテスト用の Firebug Lite)を使用してデバッグします。console.log() を随所で使用し、万が一の場合に備えてコンソールを検出します。
  • ヘルパー JavaScript コードを次に追加します。
    • データベースをクリアして、クリーンな状態にすることができます。
    • キャプチャしたファイルを削除します。これにより、再読み込み時にインターネットにアクセスして再度取得します(開発のイテレーションを行う場合に便利です)。

デバッグ ウィジェットは、Gears がインストールされている場合にのみ、ページの左側に表示されます。コードをクリーンアップするためのコールアウトがあります。

GearsBaseContent.prototype.clearServer = function() {
  if (this.localServer.openStore(this.storeName)) {
    this.localServer.removeStore(this.storeName);
    this.store = null;
  }
}

GearsBaseContent.prototype.clearTables = function() {
  if (this.db) {
    this.db.run('delete from BaseQueries');
    this.db.run('delete from BaseFeeds');
  }
  displayQueries();
}

まとめ

Google Gears の操作は実際にはかなり簡単です。GearsDB を使用して Database コンポーネントをさらに簡単にし、手動の ResourceStore を使用しました。これは、この例では問題なく動作しました。

最も時間を費やすのは、データをオンラインで取得するタイミングと、オフラインで保存する方法の戦略を定義することです。データベース スキーマの定義に時間をかけることが重要です。今後スキーマを変更する必要がある場合は、現在のユーザーがすでにデータベースのバージョンを使用しているため、その変更を管理する必要があります。つまり、データベースのアップグレードごとにスクリプト コードを配布する必要があります。これを最小限に抑えることが重要です。リビジョン管理に役立つ小さなライブラリである GearShift を試してみることをおすすめします。

ManagedResourceStore を使用してファイルを追跡することもできますが、その場合は次のようになります。

  • Google は、将来のクリーンなアップグレードを可能にするために、ファイルにバージョンを付けます。
  • ManagedResourceStore には、URL を別のコンテンツにエイリアス設定できる機能があります。有効なアーキテクチャの選択肢としては、gears_base.js を Gears 以外のバージョンにして、それをエイリアス化し、Gears 自体が gears_base_withgears.js をダウンロードして、オフライン サポートをすべて利用できるようにする方法があります。
このアプリでは、インターフェースを 1 つだけ用意して、そのインターフェースを 2 つの方法で実装する方が簡単だと考えました。

Gearing up アプリケーションを楽しく簡単に利用していただけたなら幸いです。ご質問や共有したいアプリがある場合は、Google Gears フォーラムにご参加ください。