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 上执行的常见搜索,例如搜索包含“巧克力”(美味)的食谱或包含“海滩漫步”(浪漫)的个人广告。您可以将其视为“Google Base 阅读器”,让您订阅搜索内容,并在您重新访问该应用时查看更新后的结果,或者让该应用每 15 分钟查找一次更新后的 Feed。
希望扩展该应用的开发者可以添加更多功能,例如在搜索结果包含新结果时以视觉方式提醒用户、让用户为喜爱的商品(离线 + 在线)添加书签(加星标),以及让用户执行特定类别的属性搜索,例如 Google Base。
使用 Google Base Data API Feed
Google Base 数据 API 符合 Google Data API 框架,可用于以编程方式查询 Google Base。Google Data API 协议提供了一种用于在网络上读取和写入数据的简单协议,许多 Google 产品都使用该协议,例如 Picasa、Google 表格、Blogger、Google 日历、Google 笔记本等。
Google Data API 格式基于 XML 和 Atom 发布协议,因此大多数读取/写入互动都采用 XML 格式。
以下是基于 Google Data API 的 Google Base Feed 的示例:
http://www.google.com/base/feeds/snippets/-/products?bq=数码相机
snippets
Feed 类型可提供公开可用的商品 Feed。借助 -/products
,我们可以将 Feed 限制为仅包含特定商品类别。而 bq=
参数可让我们进一步限制 Feed,使其仅包含包含关键字“数码相机”的结果。如果您在浏览器中查看此 Feed,您会看到包含 <entry>
节点(其中包含匹配结果)的 XML。每个条目都包含典型的作者、标题、内容和链接元素,但还附带其他特定于类别的属性(例如“商品”类别中的“价格”)。
由于浏览器中 XMLHttpRequest 的跨网域限制,我们无法在 JavaScript 代码中直接读取来自 www.google.com 的 XML Feed。我们可以设置一个服务器端代理来读取 XML,并将其输出到与应用相同的网域中的某个位置,但我们希望完全避免服务器端编程。幸运的是,还有其他方法。
与其他 Google Data API 一样,Google Base Data API 除了标准 XML 之外,还提供 JSON 输出选项。我们之前看到的 Feed 的 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 Feed 动态加载到网页中,我们可以使用以下函数,该函数会创建一个包含 Feed 网址(附加了 alt
callback
值)的脚本标记,并将其附加到网页中。
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 的浏览器上的用户仍能使用该应用,同时让安装了该插件的用户能够享受离线使用和响应速度更快的界面带来的好处。
我们的架构如下所示:
- 我们有一个 JavaScript 对象,负责存储您的搜索查询并返回这些查询的结果。
- 如果您安装了 Google Gears,则会获得一个将所有内容存储在本地数据库中的 Gears 版本。
- 如果您未安装 Google Gears,则会获得一个将查询存储在 Cookie 中且根本不存储完整结果的版本(因此响应速度稍慢),因为结果太大,无法存储在 Cookie 中。
if (online) {}
。现在,应用只需进行一次 Gears 检查,然后使用正确的适配器即可。
使用 Gears 本地数据库
Gears 的一个组件是嵌入式本地 SQLite 数据库,可供您随时使用。如果您之前使用过服务器端数据库(如 MySQL 或 Oracle)的 API,那么您会发现有一个简单的数据库 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');
通过此调用,您将获得一个数据库对象,该对象可让您打开数据库架构。当您打开数据库时,它们会通过同源政策规则进行限定,因此您的“testdb”不会与我的“testdb”冲突。
开始执行 SQL 请求
现在,我们可以向数据库发送 SQL 请求了。当我们发送“选择”请求时,会收到一个结果集,我们可以遍历该结果集以获取所需数据:
var rs = db.execute('select * from foo where name = ?', [ name ]);
您可以使用以下方法对返回的结果集执行操作:
boolean | isValidRow() |
void | next() |
void | close() |
int | fieldCount() |
string | fieldName(int fieldIndex) |
variant | field(int fieldIndex) |
variant | fieldByName(string fieldname) |
如需了解详情,请参阅数据库模块 API 文档。(编者注:Google Gears API 已不再可用)。
使用 GearsDB 封装低级别 API
我们希望封装一些常见的数据库任务,让它们更方便。例如:
- 我们希望在调试应用时,能够以一种简洁的方式记录生成的 SQL。
- 我们希望在一个位置处理异常,而不是在各处都使用
try{}catch(){}
。 - 我们希望在读取或写入数据时处理 JavaScript 对象,而不是结果集。
为了以通用方式处理这些问题,我们创建了 GearsDB,这是一个封装了 Database 对象的开源库。现在,我们将展示如何使用 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)'); }
此时,我们确信已有一个表来存储查询和 Feed。代码 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' }, { ... }, ...]
)。
在这种情况下,返回完整列表即可。但如果您有大量数据,可能需要在 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 相同,但适用于多个结果。 |
加载 Feed
从 Google Base 获取结果 Feed 后,我们需要将其保存到数据库:
content.setFeed({ id: id, JSON: json.toJSONString() }); ... which calls ... GearsBaseContent.prototype.setFeed = function(feed) { this.db.forceRow('BaseFeeds', feed); }
我们首先获取 JSON Feed,然后使用 toJSONString
方法将其作为字符串返回。然后,我们创建 feed
对象并将其传递给 forceRow
方法。如果条目尚不存在,forceRow
将插入一个条目;如果条目已存在,则会更新现有条目。
显示搜索结果
我们的应用会在网页的右侧面板中显示指定搜索的搜索结果。以下是我们如何检索与搜索字词相关联的 Feed:
GearsBaseContent.prototype.getFeed = function(url) { var row = this.db.selectRow('BaseFeeds', 'id = ?', [ url ]); return row.JSON; }
现在,我们有了行的 JSON,可以对其进行 eval()
以获取对象:
eval("var json = " + jsonString + ";");
我们已经准备就绪,可以开始将 JSON 中的内容通过 innerHTML 插入到网页中了。
使用资源存储区进行离线访问
由于我们是从本地数据库获取内容,因此此应用也应该可以离线运行,对吧?
当然不是。问题在于,要启动此应用,您需要加载其 Web 资源,例如 JavaScript、CSS、HTML 和图片。目前,如果用户执行以下步骤,应用可能仍能正常运行:启动在线模式,进行一些搜索,不关闭浏览器,进入离线模式。这可能有效,因为相应内容仍会保留在浏览器的缓存中。但如果情况并非如此呢?我们希望用户在重新启动等操作后能够从头开始访问应用。
为此,我们使用 LocalServer 组件并捕获资源。当您捕获资源(例如运行应用所需的 HTML 和 JavaScript)时,Gears 会保存这些项,还会捕获来自浏览器的请求以返回这些项。本地服务器将充当交通警察,并从商店返回已保存的内容。
我们还使用了 ResourceStore 组件,该组件需要您手动告知系统要捕获哪些文件。在许多情况下,您需要对应用进行版本控制,并以事务方式允许升级。一组资源共同定义一个版本,当您发布一组新资源时,您希望用户能够无缝升级文件。如果您的模型属于这种情况,则您将使用 ManagedResourceStore API。
为了捕获资源,GearsBaseContent 对象将:
- 设置需要捕获的一系列文件
- 创建 LocalServer
- 打开或创建新的 ResourceStore
- 调用以将网页捕获到商店中
// 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 无法相互查看。您可以设置服务器端代理,但这会违背将应用拆分到多个网域的初衷。
调试离线应用
调试离线应用稍微复杂一些。现在,您可以测试更多场景:
- 我处于在线状态,应用完全在缓存中运行
- 我处于在线状态,但未访问过该应用,并且缓存中没有任何内容
- 我处于离线状态,但已访问过该应用
- 我处于离线状态,并且从未访问过该应用(这可不是个好情况!)
为了简化操作,我们使用了以下模式:
- 当我们需要确保浏览器不会只是从缓存中提取某些内容时,我们会停用 Firefox(或您选择的浏览器)中的缓存
- 我们使用 Firebug(以及 Firebug Lite,用于在其他浏览器上进行测试)进行调试;我们到处都使用
console.log()
,并检测控制台以防万一 - 我们添加了辅助 JavaScript 代码,以执行以下操作:
- 让我们能够清除数据库,从头开始
- 移除捕获的文件,以便在您重新加载时,系统会再次从互联网获取这些文件(在迭代开发时很有用);
仅当您安装了 Gears 时,调试 widget 才会显示在页面左侧。它包含用于清理代码的标注:
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 使数据库组件更加简单,并使用了手动 ResourceStore,这在我们的示例中效果很好。
您花费最多时间的地方是确定何时在线获取数据以及如何离线存储数据的策略。花时间定义数据库架构非常重要。如果您日后确实需要更改架构,则需要管理该更改,因为当前用户已拥有数据库的某个版本。这意味着,您需要随任何数据库升级一起提供脚本代码。显然,尽量减少这种情况是有帮助的,您可以尝试使用 GearShift,这是一个可帮助您管理修订的小型库。
我们也可以使用 ManagedResourceStore 来跟踪文件,但会产生以下后果:
- 我们会成为优秀的公民,对文件进行版本控制,以便将来能够顺利升级
- ManagedResourceStore 有一项功能,可让您将一个网址别名化为另一段内容。一种有效的架构选择是让 gears_base.js 成为非 Gears 版本,并为其设置别名,这样 Gears 本身就会下载 gears_base_withgears.js,其中包含所有离线支持。
希望您觉得“准备应用”有趣又简单!如果您有任何疑问或想分享应用,请加入 Google Gears 论坛。