2010年10月27日星期三

使用 Dojo 开发离线应用

Dojo 离线功能简介

所谓离线,指的是用户能够在没有网络的环境下也可以进行工作,并且当网络环境恢复后,之前的操作能够自动地同步到服务器上。目前,离线应用可以分为 两种,一种是桌面应用,其中以 IBM Lotus Notes 为代表。另外一种则是近来比较热门的 Web 离线应用,其中最具代表的为 Google 的离线应用,如 Google Docs, Gmail, Google Reader 等。而本文介绍的 Dojo 离线功能,则是目前最为流行的实现第二种离线应用的框架。


回页首

Dojo 离线原理

Dojo Offline 是一个免费的开放源代码的离线开发工具,它可以轻松地让 Web 开发人员开发离线应用。Dojo Offline 构建于 Google Gear 之上,它使得在 Google Gear 上开发离线应用变得更加简单。Dojo Offline 对 Google Gears 的 API 进行了封装,能够更方便地以 JavaScript 的方式访问数据;另外,Dojo Offline 还提供了如下功能:

  1. Offline Widget:开发人员只要添加几行代码,就能够将这个离线的 widget 嵌入自己的应用中,之后,就能够自动获得网络反馈 , 同步消息以及离线操作配置说明等。
  2. 同步框架:保存离线数据,并且在网络恢复后将这些操作和服务器进行同步。
  3. 自动的网络和应用程序状态监控机制:离线应用可以基于这些监控结果来决定采取什么样的操作。
  4. slurp() 方法:通过 slurp() 方法来自动扫描页面,找出需要缓存到本地的资源,包括图片,样式,脚本等。这使得 Web 开发人员能够集中精力地去开发业务逻辑和展示页面,而不需要考虑具体哪些资源需要缓存到本地。
  5. 提供两种本地存储实现方法:Dojo Storage 和 Dojo SQL。Dojo Storage 使用哈希表来存储离线数据,为开发人员屏蔽了重量级的 Google Gears 的 SQL 使用方法,我们可以通过 Dojo Storage 将数据保存到 Google Gears 里去。Dojo SQL 通过执行 SQL 命令,对一般的 JavaScript 对象进行存取。
  6. 加密和解密关键字 ENCRYPT() 和 DECRYPT():提供对下载后的离线数据加密和解密方法。
  7. Dojo 其他模块的集成:如果用户选择 Dojo Offline,毫无疑问,其应用可以受益于 Dojo 的其他功能,比如完善的事件处理机制,Ajax 框架等等。

回页首

Google Gears 简介

Google Gears 是 Google 开发的一个自由和开源软件,以 BSD 许可证发布。主要是为了能够让用户可以在离线时能够继续使用 Web 应用。这套软件透过 SQLite 数据库让本地端能够把资料暂存起来。所以网页是透过缓存取得的,而不是从实际的网络上取得。而且,Web 相关的程序可以周期性的透过 Gears 将本地缓存的资料与网络上的资料做同步。如果网络暂时无法使用,这个同步过程将会延后,直到网络恢复为止。Google Gears 通过以浏览器扩展的方式添加 JavaScript API,使得浏览器的脚本能够访问本地的缓存数据库。该扩展目前支持 Firefox,Internet Explorer 和 Safari,并需工作于 Windows、Mac OS X,或是 Linux 下。

Google Gears 有几个主要的组件:

  1. 一个本地服务器,用来存储和读取离线程序资源(包括 HTML 文件 , JavaScript 文件 , 图片等)。
  2. 一个小型数据库(基于 SQLite 轻量数据库),用来储存本地数据。
  3. 一个工作池,用来让开发者将本地数据与服务器端后台同步。
  4. 一个桌面模型,可使网络程序的操作贴近桌面程序。
  5. 一个地理定位模型,能够让网络程序侦测到目前用户的地理位置。

回页首

开发 Dojo 离线应用程序

一般来说开发一个离线应用有以下几个步骤:

  1. 应用加载时缓存所引用的资源。
  2. 应用加载之后设置必要的离线事件处理函数。如:与服务器进行同步操作时的下载事件、下载完成事件,重放事件等。
  3. 检查应用是否在线,读取应用所需的数据。如果离线则从本地读取数据;如果在线则与服务器数据进行同步:首先上传离线数据(如果有的话),然后从服 务器上下载最新数据。
  4. 初始化应用程序。

下面我们会先分别介绍如何保存离线资源,保存和使用应用离线数据以及如何与服务器的数据保持同步等相关技术,然后以一个具体的例子说明如何按上述步 骤开发一个离线应用。

保存离线资源

为了使页面能够在离线情况下操作,在页面加载时必须要保存页面所引用的 HTML 文件,JavaScript 文件,CSS 文件,图片文件等资源,这样在与服务器断开连接时,页面便能够从本地缓存中提取到页面及相关的资源。Dojo 提供了一个功能非常强大的函数 slurp 来实现这一功能。slurp 函数在页面加载之后会自动分析页面所引用的资源,并得到一个资源文件的 url 列表,然后抓取 url 的内容,保存到 Google Gears 中。slurp 函数能够缓存大部分的资源文件,包括引用 CSS 文件里的图片,但是对于使用内联方式指定的 CSS 图片以及用 JavaScript 动态产生的引用 slurp 则无法进行缓存。这种情况可以通过调用 dojox.off.files.cache () 方法来手动添加要缓存的资源,具体用法详见后文。

Dojo Offline 还提供了在调试时查看已缓存资源文件的方法,首先以 debug 方式加载 Dojo,然后在页面和 Dojo Offline 加载完之后调用 dojox.off.files.printURLs() 函数。这个函数会在控制台以调试的方式打印出已缓存的资源文件的 url。

保存和使用离线数据

一般来说,离线应用不仅要缓存页面所引用的 HTML 文件,JavaScript,CSS,图片等资源,还要缓存页面所使用的数据。Dojo 对 Google Gears 的访问接口进行了封装,提供了 Dojo Storage 和 Dojo SQL 两种方式操作离线数据。通过这两种方式,用户可以方便、灵活地保存和读取离线数据。

  • Dojo Storage

提供了一种类似哈希表的方式对数据进行操作,数据以键值对的方式存取。

保存:dojox.storage.put(key,value) — key 是字符串类型,value 可以是字符串和可序列化的 JavaScript 对象,但是浏览器对象(如 DOM 节点和 XMLHttpRequest 对象)是不能够保存的。

读取:var value = dojox.storage.get(key) —获取字符串 key 对应的值。
清单 1

var car = {type: "Nissan", color: "white", price: 20000, optional:
"air-conditioner, stereo"};
dojox.storage.put("complexKey", car);

var value = dojox.storage.get("someKey");
var car = dojox.storage.get("complexKey");
if(car)
alert(car.type);

上述代码先将一个对象 car 放置在 Dojox.Storage 里,然后又从中取出并展示。

  • Dojo SQL

通过 Dojo SQL 对 Google Gears 关系存储层的封装,能够以更适合 JavaScript 对象的访问方式对关系表进行操作。如下所示,我们对 Documents 表进行了创建,插入,查询操作:
清单 2

//create Documents table if not exist
dojox.sql("CREATE TABLE IF NOT EXISTS DOCUMENTS ("
+ "fileName TEXT NOT NULL PRIMARY KEY UNIQUE,"
+ "content TEXT NOT NULL) ");
//insert a record
dojox.sql("INSERT INTO DOCUMENTS (fileName, content) VALUES (?, ?)", fileName, contents);
//query
var results = dojox.sql("SELECT * FROM DOCUMENTS WHERE fileName = ?", someFileName);

Dojo 查询操作返回的是一个记录对象的数组,如访问第一条记录的 fileName 字段,可以使用 results[0].fileName,访问 content 字段则可以使用 results[0].content。

与服务器同步数据

离线操作所产生的新数据,在与服务器连接可用时需要上传到服务器;并且,在该用户离线期间,其他用户也可能已经更改过数据,因此重新连接服务器时需 要下载最新的数据。Dojo 的同步操作发生的时机有两个,一是在页面首次加载并且在线的时候,或是离线后又检测到连接恢复的时候,具体包括 3 个步骤:

  1. 下载并缓存最新的页面资源,包括由 slurp 函数以及用户手动缓存的资源。Dojo Offline Widget 会自动更新相应的状态,如下图所示:

    图 1. 下载状态
    下载状态

  2. 上传用户在离线期间所创建的数据。

    图 2. 上传状态
    上传状态

  3. 下载服务器最新数据进行展示。

    图 3. 同步成功
    同步成功

    如上所示,Dojo Offline 提供了一个 UI 控件来通知用户当前的状态,如下载,上传以及网络连接状态等。Dojo Offline Widget 处理了大部分的逻辑,开发人员可以直接在页面中使用该控件。使用 Offline Widget 很简单,只需在页面加入一个占位符即可。Offline Widget 也提供了扩展机制,以更改所使用的图片和样式来跟页面风格保持一致。

Dojo 离线应用示例

Dojo Offline 随附的开发包中提供了 3 个例子,下面以 hello world 为例说明如何构建一个简单的离线应用。hello world 程序模拟了用户与服务器简单的消息交互。用户界面包括一个输入框和一个发送按钮,在输入框里输入文本,点击发送按钮将消息发送到服务器。为了保持用例的简 单,实际上消息并没有真正被发送到服务器,而只是简单地将其弹出。在应用离线时,点击发送按钮则会先在本地保存该消息,当应用重新连接时再重新执行该操 作。

  1. 首先加载 Dojo 及离线所使用的 JavaScript 文件。

    清单 3

    <script type="text/javascript" src="../../../../dojo/dojo.js"
    djConfig="isDebug: true"></script>
    <script type="text/javascript" src="../../../../dojox/off/offline.js">
    </script>

    当然,我们也可以用 dojo.require 方法进行加载:

    清单 4

    <script type="text/javascript" src="../../../../dojo/dojo.js"
    djConfig="isDebug: true"></script>
    <script type="text/javascript" >
    dojo.require(“dojox.off.offline”);
    </script>
  2. 设置应用程序的名字,这个名字会显示在 Dojo Offline Widget 上。

    清单 5

    dojox.off.ui.appName = "Hello World";
  3. 缓存页面资源文件。要使离线功能正常工作,这个函数必须在 Dojo Offline 初始化之前调用,即 dojox.off.initialize 函数调用之前。

    清单 6

    dojox.off.files.slurp();
  4. 初始化应用程序,设置离线操作的一些事件响应函数。

    清单 7

    initialize: function(){
    console.debug("helloWorld.initialize");
    var self = this;
    dojo.connect(dojox.off.sync, "onSync", this, function(type){
    if(type == "download"){
    self._messages.push("Hi! This is fake downloaded data!");
    dojox.storage.put("messages", self._messages);
    dojox.off.sync.finishedDownloading();
    }else if(type == "finished"){
    this._messages = [];
    dojox.storage.remove("messages");
    }
    });
    dojo.connect(dojox.off.sync.actions, "onReplay", this,
    function(action, actionLog){
    if(action.name == "new hello world message"){
    var message = action.data;
    self.sendMessage(message);
    actionLog.continueReplay();
    }
    });
    if(!dojox.off.isOnline){
    this.loadOfflineData();
    }
    this.printMessages();
    }

    可以看到,此处最主要的两个操作是设置 dojox.off.sync 对象的 onSync 事件和 dojox.off.sync.actions 对象的 onReplay 事件。其中,onSync 用来处理与服务器的同步,download 事件发生在第一次加载页面或是网络重新连接的时候,在该事件中我们应该从服务器下载数据,并在下载完毕之后调用 dojox.off.sync.finishedDownloading() 方法,以便告诉 Dojo Offline Widget 更新当前状态。finished 事件则说明从服务器下载数据结束,可以更新应用程序了。

    Dojo Offline 使用 action log 来记录用户在离线时所执行的操作,并在重新获得网络连接时再次执行这些操作,将数据上传到服务器。

    一般来说,程序初始化时首先要判断应用是否在线,如果不在线则加载本地数据进行页面展示。值得注意的是,这些事件何时触发是由 Dojo Offline 的同步框架根据当前应用程序状态决定的,开发人员只需设置相应的事件处理函数即可。

  5. 保存离线数据,这是通过创建 action log 来实现的。在与服务器重新连接时 action log 会自动重新执行。

    清单 8

    saveOfflineSend: function(message){
    this._messages.push(message);
    dojox.storage.put("messages", this._messages);
    var action = {name: "new hello world message", data: message};
    dojox.off.sync.actions.add(action);
    alert("This message has been saved for sending when we go back online");
  6. Dojo 的离线功能要在页面加载完及 Dojo Offline 初始化之后才能工作,因此页面本身的代码要在 Dojo Offline 初始化完成之后执行。Dojo Offline 的初始化完成可以用 dojox.off.ui 的 onLoad 事件来标识。我们首先将页面的初始化函数绑定到 dojox.off.ui 的 onLoad 事件,然后调用 dojox.off.initialize 函数对 Dojo Offline 进行初始化。

    清单 9

    dojo.connect(dojox.off.ui, "onLoad", helloWorld, "initialize");
    dojox.off.initialize();
  7. 加载 Dojo Offline Widget。实际上任何 html 元素,只要 id 为“dot-widget”都可以作为 Offline Widget 的占位符。Dojo Offline 初始化的时候会对这个元素进行填充。

    清单 10

    <div id="dot-widget"></div>
  8. 为页面加入展示元素。

    清单 11

    <div id="helloMessage" style="margin: 1em;">
    <label for="helloInput" style="margin-right: 0.2em;">
    Enter Hello World Message:
    </label>
    <input name="helloInput" id="helloInput" style="margin-right: 0.2em;">
    <button id="sendMessage" onclick="helloWorld.send()" style="margin-right: 0.2em;">
    Send</button>
    </div>
    <p>Debug output:</p>
  9. 运行 hello world

    hello world 示例程序不涉及服务器端代码,因此并不一定需要部署才能执行。但是为了便于测试在线 / 离线操作,最好还是通过服务器访问。

    在 Dojo Offline 里随附的例子中,Moxie 编辑器是一个包括服务器和客户端的完整应用程序,它使用 Jetty 作为 Web 容器,使用 Derby 作为数据库。我们可以启动 Moxie 应用所在的 Jetty 容器来访问 hello world 应用。

    首先请安装 jdk1.5 或以上版本,在 dojox/off/demos/editor/server/ 目录下,运行:

    java -jar editor.jar

    在浏览器地址栏输入 http://localhost:8000/dojox/off/demos/helloworld/helloworld.html 即可访问 hello world 应用。可以通过关闭 / 打开 Jetty 来观察 hello world 和 Dojo Offline Widget 在离线、在线及同步时的行为。


回页首

高级议题

网页资源的手工缓存

尽管利用 slurp() 方法可以为我们完成很多自动化的工作,但一些出现在 JavaScript 中的动态加载的资源是 slurp() 所无法分析得到的。因此,在实际应用中作为补充,我们往往还需要将一些 slurp 过程中的“漏网之鱼”手工加入本地缓存中。而 Dojo Offline 也为我们提供了相应的功能以支持这一需求。

手工将网络资源加入本地缓存的方法十分简单,例如,假设我们要将一个名为 big_and_little_fish.jpg 的文件缓存起来,可以执行如下方法:
清单 12

dojox.off.files.cache("images/big_and_little_fish.jpg");

不仅如此,Dojo Offline 还提供了一次性指定多个文件的功能,调用方法如下所示:
清单 13

dojox.off.files.cache([
"images/fish1.png",
"images/fish2.png",
"scripts/some_dynamic_fish.js"
]);

我们可以多次调用 cache() 方法,假如调用过程中指定了多个同名文件,则最后一次被缓存的文件将会替换前面的同名文件。利用这一方法,再结合前述的 slurp() 方法,我们就可以很好的将离线所需的全部资源在本地缓存起来了。

数据的加密与解密

将数据下载到本地进行缓存是离线功能得以实现的重要步骤,但是假如这些数据中包含有敏感信息,例如身份证号码,而本地环境又不甚安全时,则对数据进 行加密处理是很有必要的。前面曾经提到,我们在利用 Dojo Offline 进行数据存储时可以选择使用 Dojo SQL。而 Dojo SQL 允许我们很方便的对数据的某些字段进行加密和解密。

假设我们以前面的 DOCUMENT 表为例,假定其中的“content”字段属于敏感信息。

Dojo SQL 提供了两个特殊的关键字,ENCRYPT() 和 DECRYPT(),分别用以实现加密和解密操作。以 content 为例,调用方式类似如下:
清单 14

var password = "password";
dojox.sql("INSERT INTO DOCUMENTS VALUES (?,ENCRYPT(?))", "helloworld.txt",
"sensitive content", password,
function(results, error, errorMsg){
if(error){ alert(errorMsg); return; }
});

在上述代码中,我们在后一个字段加上了 ENCRYPT 关键字,声明对其进行加密。除此以外,我们还提供了一个 password 参数,这是底层加密机制所必须的。当我们启用应用系统时,系统会提示输入这一密码。出于对本地数据安全性的考虑,请不要将其存于 Cookie,Dojo Storage,或是 Dojo SQL 中,否则他人就可以轻易利用这一密码解开存于本地的敏感数据。另外,上述代码里还定义了一个回调函数。该回调函数将会在加密过程结束之后被调用。如果加密 成功,results 对象中将会包含经过加密后的数据;如果加密失败,则 error 值将会为 true,并且 errorMsg 中会包含对错误原因的描述信息。

和加密一样,对数据进行解密的过程也十分简单。我们只需对需要解密的字段调用 DECRYPT 关键字即可。示例代码如下:
清单 15

var password = "password";
dojox.sql("SELECT fileName, DECRYPT(content) FROM DOCUMENTS",
password,
function(results, error, errorMsg){
if(error){ alert(errorMsg); return; }
// go through decrypted results
alert("First document info: "
+ results[0].fileName + " "
+ results[0].content);
});

在上述代码中,我们利用 alert 语句将加密后的第一条结果数据打印了出来。其中的 password 必须与加密时所使用的完全一致,否则解密将会失败,结果数据将仍然是加密后的样子。

假如我们要对多个字段进行加密,Dojo SQL 的 ENCRYPT 还有一种很方便的书写方法,即:ENCRYPT(?, ?);同样的,对于 DECRYPT 也存在类似的书写方式,已完成一次性对多个数据字段进行解密:DECRYPT(fileName, content)

定制 Offline Widget

虽然利用 Dojo Offline Widget 可以省去我们很多的功夫,但是在某些情况下,其默认行为并不一定能够很好的满足我们的需要,例如:默认的样式风格与我们的应用程序不一致,此时,我们就可 以对 Offline Widget 进行一定程度的定制。

事实上,我们在页面上所见到的 Offline Widget 有其对应的模板文件,该文件位于 dojox/off/resources/offline-widget.html,其中包含有一系列的 HTML 元素,这些元素都有相应的 ID,比如:dot-widget-network-indicator,Offline Widget 就是利用默认的 CSS 文件对其进行控制的,该 CSS 文件位于:dojox/off/resources/offline-widget.css。我们可以通过自定义 CSS 的方式来改变 HTML 元素的样式,从而改变 Offline Widget 在页面中的展现风格。

除了改变展现风格,有时我们可能还需要改变展现的内容,我们可以将 offline-widget.html 中的内容选择性的复制到我们自己的 HTML 页面中,这样我们就可以根据自己的需要随意设计 Offline Widget 的展现内容,而无需定义一个 ID 为 dot-widget 的特定 DIV 了。

此外,如果我们只是想修改 Offline Widget 所使用的默认图标的话,则还有更为简单的做法。那就是对如下几个 JavaScript 变量进行修改:

  • dojox.off.ui.onlineImagePath:表示在线状态的图标
  • dojox.off.ui.offlineImagePath:表示离线状态的图标
  • dojox.off.ui.rollerImagePath:表示同步正在进行的图标
  • dojox.off.ui.checkmarkImagePath:表示同步结束的图标

例如,如果我们想将表示在线状态的绿色小圆球状图标替换为自己设计的图标,则可以修改如下:

dojox.off.ui.onlineImagePath = “images/myonlineball.png”;

最后,假如我们对默认的“Learn How”页面不甚满意的话,则还可以对 dojox/off/resources/learnhow.html 进行修改,其对应的 CSS 样式也定义与 dojox/off/resources/offline-widget.css 中。

性能问题

Dojo Offline 每次在对数据进行同步时,始终都会刷新缓存于本地的资源,这一点有可能会对同步的性能构成较大的影响,因为毕竟并非每一次同步都需要刷新所有的本地数据。 为了提高同步的效率,Dojo Offline 提供了一种可选的方案,即由应用程序提供一个名为 version.js 的特殊文件,并将其置于应用程序的主路径下。假如该文件存在的话,则同步期间 Dojo Offline 会去读取该文件的内容,并判断其内容是否改变,以此来选择性的刷新本地缓存文件。

我们在 version.js 文件中所提供的内容其实非常简单,可以是一个字符串,也可以是一个数字,形式不限。例如:”03-15-2009.9″。只要能够让 Dojo Offline 判断出内容的变化即可。

除了同步时候的性能问题外,另一个可能会遇到的性能问题是数据的存储。当我们使用 Dojo SQL 来存储数据时,必须先将底层所依赖的 Google Gears 的存储数据库打开,而后才能进行数据的存储,而当数据存储完毕之后,还需将数据库关闭。通常情况下,我们并不需要关心这一点,因为 Dojo SQL 会自动为我们完成这一切,每次当我们执行一句 SQL 语句时,底层代码都会自动处理数据库的打开和关闭。

不过,假如我们是在批量的向本地缓存插入数据的话,比如在一个循环次数为上百次的 for 循环中执行数据插入操作,那么每执行一次 SQL 语句都要打开和关闭一次数据库或许是一种很低效做法。为了提高效率,我们可以在批量操作数据之前通过调用 dojox.sql.open() 手工将数据库打开,等所有操作完成之后后再通过调用 dojox.sql.close() 手工将其关闭。插入数据期间,Dojo SQL 会自动判断数据库的连接状态,并保持连接一直处于打开状态,直至手工关闭为止:
清单 16

var dataSet = getDataSet(); // get an array that has a bunch of data
//in it
dojox.sql.open();
for(var i = 0; i < dataSet.length; i++){
dojox.sql("INSERT INTO DOCUMENTS (fileName, content) VALUES (?, ?)",
dataSet[i].fileName, dataSet[i].content);
}
dojox.sql.close();

回页首

总结

本文介绍了 Dojo Offline 的强大功能。通过阅读本文,读者能够了解到 Dojo Offline 的基本工作原理和一定的高级定制功能,从而掌握如何使用 Dojo Offline 进行 Web 离线应用程序开发的技能。


学习

没有评论:

发表评论