使用 Google Base 和 Google Gears 打造效能優異的離線體驗

「運用 Google API 打造更優質的 Ajax 應用程式」系列的第一篇文章。

Google 的 Dion Almaer 和 Pamela Fox
2007 年 6 月

編輯附註: Google Gears API 已停止提供

簡介

我們將 Google Base 與 Google Gears 結合,示範如何建立可離線使用的應用程式。讀完本文後,您將更熟悉 Google Base API,並瞭解如何使用 Google Gears 儲存及存取使用者偏好設定和資料。

瞭解應用程式

如要瞭解這項應用程式,請先熟悉 Google Base,這基本上是涵蓋各種類別 (例如產品、評論、食譜、活動等) 的大型項目資料庫。

每個項目都會加上標題、說明、資料原始來源的連結 (如有),以及其他屬性 (因類別類型而異)。Google Base 會利用同一類別中的項目共用一組屬性的事實,例如所有食譜都有食材。 Google 網頁搜尋或 Google 產品搜尋的搜尋結果,有時甚至會顯示 Google Base 項目。

我們的示範應用程式「Base with Gears」可讓您儲存及顯示在 Google Base 上執行的常見搜尋,例如搜尋「巧克力」食譜 (真好吃!),或是搜尋「海灘散步」個人廣告 (真浪漫!)。你可以將其視為「Google Base Reader」,訂閱搜尋內容,並在重新開啟應用程式時查看更新結果,或讓應用程式每 15 分鐘尋找更新的動態消息。

開發人員如要擴充應用程式,可以新增更多功能,例如在搜尋結果包含新結果時以視覺化方式提醒使用者、讓使用者將喜愛的項目加入書籤 (離線 + 線上),以及讓使用者執行特定類別的屬性搜尋,例如 Google Base。

使用 Google Base 資料 API 資訊提供

Google Base 資料 API 符合 Google Data API 架構,因此可透過程式輔助方式查詢 Google Base。Google Data API 通訊協定提供簡單的通訊協定,方便您在網路上讀取及寫入資料,許多 Google 產品都使用這項通訊協定,包括 Picasa、試算表、Blogger、日曆和記事本等。

Google Data API 格式是以 XML 和 Atom 發布通訊協定為基礎,因此大部分的讀取/寫入互動都是以 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 動態饋給。我們可以設定伺服器端 Proxy,讀取 XML 並在與應用程式相同的網域位置傳回,但我們希望完全避免伺服器端程式設計。幸好還有其他替代方案。

與其他 Google Data API 相同,Google Base Data API 除了標準 XML 之外,也提供 JSON 輸出選項。先前看到的動態饋給輸出內容 (JSON 格式) 位於這個網址:
http://www.google.com/base/feeds/snippets/-/products?bq=digital+camera&alt=json

JSON 是一種輕量型交換格式,可進行階層式巢狀結構,以及使用各種資料型別。但更重要的是,JSON 輸出內容本身就是原生 JavaScript 程式碼,因此只要在指令碼標記中參照該內容,即可載入網頁,略過跨網域限制。

Google Data API 也可讓您指定「json-in-script」輸出內容,並提供回呼函式,在載入 JSON 後執行。這樣一來,我們就能動態將指令碼標記附加至網頁,並為每個標記指定不同的回呼函式,JSON 輸出內容的使用體驗也更加輕鬆。

因此,如要將 Base API JSON 動態動態載入網頁,我們可以建立下列函式,該函式會建立含有動態饋給網址 (附加 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) {}整個商店中進行檢查。應用程式會進行一次 Gears 檢查,然後使用正確的介面卡。


使用 Gears 本機資料庫

Gears 的其中一個元件是內建的本機 SQLite 資料庫,可供您直接使用。如果您先前使用過 MySQL 或 Oracle 等伺服器端資料庫的 API,應該會覺得這個簡單的資料庫 API 相當熟悉。

使用本機資料庫的步驟相當簡單:

  • 初始化 Google Gears 物件
  • 取得資料庫 Factory 物件,並開啟資料庫
  • 開始執行 SQL 要求

我們快速瀏覽一下這些內容。


初始化 Google Gears 物件

應用程式應直接讀取 /gears/samples/gears_init.js 的內容,或將程式碼貼到自己的 JavaScript 檔案中。取得 <script src="..../gears_init.js" type="text/JavaScript"></script> 後,您就能存取 google.gears 命名空間。


取得資料庫 Factory 物件並開啟資料庫
var db = google.gears.factory.create('beta.database', '1.0');
db.open('testdb');

這個呼叫會提供資料庫物件,讓您開啟資料庫結構定義。開啟資料庫時,系統會透過同源政策規則設定範圍,因此您的「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(){}
  • 我們希望在讀取或寫入資料時處理 JavaScript 物件,而非結果集。

為以一般方式處理這些問題,我們建立了 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),並為您處理插入資料表中的作業。您也可以定義限制 (例如,禁止插入多個「Bob」)。不過,您通常會在資料庫本身處理這些限制。


取得所有搜尋字詞

我們會使用名為 selectAll 的精選包裝函式,填入過去的搜尋記錄清單:

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

這會傳回與資料庫中資料列相符的 JavaScript 物件陣列 (例如 [ { Phrase: 'Nintendo Wii', Itemtype: 'product' }, { ... }, ...])。

在這種情況下,傳回完整清單即可。但如果資料量很大,您可能想在選取呼叫中使用回呼,以便在每個傳回的資料列傳入時對其進行操作:

 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 方法以字串形式傳回。接著,我們建立 feed 物件,並傳遞至 forceRow 方法。如果項目不存在,forceRow 會插入項目;如果項目存在,則會更新項目。


顯示搜尋結果

應用程式會在頁面的右側面板顯示特定搜尋的結果。以下說明如何擷取與搜尋字詞相關聯的動態消息:

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 檔案時,遇到這項限制。解決方法當然很簡單:您需要下載所有外部資源,並將其放在網域下。請注意,302 或 301 重新導向不會生效,因為 LocalServer 只接受 200 (成功) 或 304 (未修改) 伺服器代碼。

這會產生影響。如果將圖片放在 images.yourdomain.com,您就無法擷取圖片。www1 和 www2 無法互相查看。您可以設定伺服器端 Proxy,但這樣會失去將應用程式分割至多個網域的目的。

偵錯離線應用程式

偵錯離線應用程式稍微複雜一點。現在有更多測試情境:

  • 我處於連線狀態,應用程式完全在快取中執行
  • 我處於連線狀態,但未存取應用程式,且快取中沒有任何內容
  • 我處於離線狀態,但已存取應用程式
  • 我處於離線狀態,且從未存取過應用程式 (這可不是個好地方!)

為簡化這項作業,我們採用了以下模式:

  • 我們會在 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 追蹤檔案,但會產生下列影響:

  • 我們會成為好公民,為檔案建立版本,以便日後順利升級
  • ManagedResourceStore 有一項功能,可讓您將網址別名設為其他內容。有效的架構選擇是讓 gears_base.js 成為非 Gears 版本,並為其設定別名,這樣 Gears 本身就會下載 gears_base_withgears.js,其中包含所有離線支援功能。
就我們的應用程式而言,我們認為只要有一個介面,並以兩種方式實作該介面,會比較簡單。

希望您覺得「準備應用程式」課程有趣又簡單!如有任何疑問或想分享應用程式,請前往 Google Gears 論壇。