IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Web development  >

Dojo 离线技术应用:支持离线功能的 Web 编辑器

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

莫 映, 高级工程师, IBM
张 顺, 软件工程师, IBM

2009 年 9 月 16 日

本文以一个完整的示例为读者介绍如何将 Dojo 离线技术应用于实际。贯穿全文的示例是一个在线的 Web 编辑器。在这个示例中,我们将完成:如何为一个已有的在线 Web 编辑器引入离线编辑功能,同时还包括如何编写相应的服务器端代码,以接受并同步来自客户端的编辑内容。通过阅读本文,读者能够了解到使用 Dojo 离线库的完整过程,以及使用过程中的个中细节与注意事项。

概述

使用 Dojo 离线功能可以在页面加载时保存页面内容及其相关的图片、CSS 与 JavaScript 等资源,如此,即便在离线情况下我们也能够继续对页面进行操作,并且操作内容可以被如实地保存于本地,并在与服务器的连接被恢复时自动同步至服务端。使用 Dojo 离线功能,可以有效提高用户访问的可靠性,并增进用户的体验。

在《 使用 Dojo 开发离线应用》一文中,我们介绍了 Dojo 离线功能的基本功能、实现原理及编程模式,本文中,笔者将以一个完整的示例为读者介绍如何将 Dojo 离线技术应用于实际。贯穿全文的示例是一个在线的 Web 编辑器。在这个示例中,我们将完成:如何为一个已有的在线 Web 编辑器引入离线编辑功能,同时还包括如何编写相应的服务器端代码,以接受并同步来自客户端的编辑内容。通过阅读本文,读者能够了解到使用 Dojo 离线库的完整过程,以及使用过程中的一些细节与注意事项。





回页首


Moxie 离线编辑器

Moxie 是一个包括服务器端与客户端实现的较为完整的 Dojo 离线应用,功能较为单一,程序结构清晰,代码简洁,是一个较好的 Dojo 离线示例应用程序,从中我们可以系统地学习如何使用 Dojo 离线库进行页面缓存和处理各种事件等。

我们在使用在线编辑器的过程中,经常会遇到由于编辑时间过长导致 session 过期,或是网络连接断开,编辑内容无法保存的情况。较为常见的解决方式是在服务器端设置一个较长的 session 过期时间或是在客户端每隔一段时间(在 session 过期时间以内)自动提交编辑器的内容。过长的 session 过期时间会导致安全问题,而客户端定期保存会导致不必要的网络流量,增大服务器压力,尤其是当编辑内容很大的时候,更是如此。Dojo 离线库的出现为这个问题提供了一种新的解决方式,无论是 session 过期还是网络中断,我们都可以先在客户端保存编辑内容,在与服务器连接恢复时再将客户端暂存的内容同步到服务器上。

Moxie 应用的主要功能是提供了一个基于浏览器的文本编辑器,我们可以为当前编辑的内容取一个名字,然后以 < 文件名,文件内容 > 键值对的方式提交到服务器保存。客户端还能够加载服务端已保存的文件列表,通过选择文件名加载对应的文件内容进行编辑。由于使用了 Dojo 离线库,即使在离线的情况下我们也能从本地读取文件内容进行编辑,或是将编辑的内容保存在本地,在与服务器连接恢复时再进行同步。

本应用的实现分为服务器端和客户端两个部分,服务器端是采用 Java 语言实现的 Web 应用,通过 servlet 接收请求,实现保存或读取文件内容的功能。客户端通过 Dojo 离线库和一个文本编辑器实现了文件离线编辑器的功能。由于服务器端实现特定于应用需求,不具有普遍意义,且服务器端功能较为简单,因此本文只概要介绍服务器端的功能和设计,不涉及具体编码细节,有兴趣的读者可以参考其源码。





回页首


如何运行

我们可以通过 HTTP 下载 Dojo Offline Demo,或者通过 SVN 下载,具体参见 获取 Dojo 源代码。如果您不希望下载源码,可以通过 Dojo 提供的基于 Web 的 track 工具 查看其内容。我们可以看到,其中包含了基于 Dojo Offline 的客户端代码,以及完成服务器端响应的 Java 代码。为了完成服务器端响应功能,zip 包中随附了一个轻量级的 Web 服务器—— Jetty,还有一个轻量级的数据库—— Derby。我们的服务器端代码主体是一个 Servlet,它将负责接收来自客户端的 HTTP 请求,并针对请求的类型对数据库进行相应存取。

在开始运行程序之前请先确认,我们所使用的 JDK 必须是在 1.5 及 1.5 版本以上。

运行服务器端程序十分简单。只需要在命令行下定位到在线编辑器的根目录处,然后输入如下命令即可:

java -jar editor.jar

随后,Jetty 服务器将会启动,包括 Derby 数据库在内,无需额外的配置,只待服务器启动完毕后,打开浏览器并输入:

http://localhost:8000/demos/offline/editor/editor.html

如此,你就可以访问到在线编辑器了。





回页首


服务器端的实现

如前文所述,服务器端的代码是用 Java 写就的。它由以下几个类构成:

Main.java

Document.java

Documents.java

MoxieException.java

MoxieServlet.java

其中的 Main.java 是一个辅助类,用于快速启动在线编辑器的服务端代码,此处我们使用了内嵌的 Derby 数据库,以及一个内嵌的微型 Web 服务器 Jetty。

Document.java 是用于描述文档对象的一个普通的 JavaBean,包含了一系列 getter 和 setter 方法。其中还有一个判断文档名称合法性的方法。

Documents.java 相当于数据访问对象,用于提供访问 Derby 数据库的相应方法。有关访问数据库的细节,请参见代码。

MoxieException.java 是我们定义的一个异常类,没有什么特别之处。

MoxieServlet.java 是服务器端代码的主体。该类暴露了一套 REST 风格的 API。

下面的类图总结了服务器端代码所用到的全部类:


图 1. 服务器端实现类图
图 1. 服务器端实现类图

REST 服务说明

在我们的服务器端代码中,我们将每一个文档当作是一个对象,并以文档名称对其进行标识。如:

/somePageName1

/somePageName2

为了查看一个文档,我们只需发起一个 GET 请求,并给予文档名称。假如文档不存在,服务器代码将返回 404 错误(Not Found);假如文档名称有误则返回 403 错误(Forbidden)。

要查看所有文档的清单,我们只需要发起一个 GET 请求,并给予形如“/*”这样的 URL 格式。

假如客户端在 HTTP 请求的 Accept 头中提供了“text/html”,服务端代码将返回一个简单的 HTML 页面,其中包含了一个所有编辑文档的清单:


清单 1.服务返回格式—— HTML
				 
	 <html> 
	 <body> 
		 <ul> 
			 <li><a href="somePageName1">somePageName1</a></li> 
			 <li><a href="somePageName2">somePageName2</a></li> 
		 </ul> 
	    </body> 
	 </html> 

相反,假如客户端提供的是“text/javascript”,那么我们将会以 JSON 格式返回:


清单 2.服务返回格式—— JSON
				 
	 [ 
		"somePageName1", "somePageName2"
	 ] 

假如要新建文档或更新已有文档,我们只需发起一个 POST 请求,并给予文档名称,同时提供文档的内容。对于文档内容,我们可以选择以经过编码的 URL 参数形式提交,或者以 HTML 文本格式作为 HTTP 请求的 body 进行提交。对于前者,Content-Type 头必须为“application/x-www-form-urlencoded”,并且参数名必须为“content”,参数值必须经过编码。如果是后者,Content-Type 头必须为“text/html”。

如果文档创建成功,则服务端返回 201(Created),如果文档已存在或文档名称不正确,则返回 403(Forbidden),如果文档更新成功,则返回 200。

对于文档的删除,由于 Safari 和 Opera 在处理 DELETE 请求时存在问题,所以我们除了提供直接发送 DELETE 请求的方式外,还提供了 POST 请求的方式,并在 HTTP 请求头中给予“X-Method-Override: DELETE”,以此来指示 DELETE 行为。假如删除成功,服务端将返回 410(Gone),假如文档不存在则返回 404 错误(Not Found),假如文档名称不合法则返回 403(Not Allowed)。

最后,我们还提供了一个以 GET 方式访问的 /download/ 链接。这将会返回一组 JSON 格式的数据,其中列出了所有文档的内容,例如:


清单 3.download 服务返回格式
				 
	 [ 
		 {fileName: "message1", content: "hello world"}, 
		 {fileName: "message2", content: "goodbye world"} 
	 ] 





回页首


客户端开发

客户端界面设计

Moxie 离线编辑器的界面主要有四个部分:

  • 状态显示:显示当前的动作状态,如加载文件,保存文件等。
  • 文件显示:包括两部分,一个是文件名输入框,显示当前编辑的文件名,用户也可以修改名字将当前编辑器的内容保存为另一个文件。文件列表显示已保存的所有文件名,用户可以通过选择文件名来加载不同的文件内容。
  • 离线 widget:离线库提供的 UI widget,用来显示当前程序的状态,如在线,离线,上传、下载等。使用该控件只需将父元素的 id 设置为"dot-widget"即可,Dojo 离线库在初始化时会查找 id 为"dot-widget"的元素并将离线 widget 作为子元素加入到该元素中。
  • 编辑器:显示和输入文件内容。

图 2. Moxie 客户端界面
图 2. Moxie 客户端界面

初始化

Moxie 客户端的初始化和一般的离线应用程序一样,主要分为 4 步,即设置应用程序名字供离线 widget 使用;调用 slurp 函数保存网页相关的离线资源;通过 dojo.connect 将 Moxie 的初始化函数与离线库加载事件绑定,即 dojox.off.ui 的 onLoad 事件,该事件标志 Dojo 离线库已经初始化完毕,Moxie 可以开始进行初始化工作;最后调用 Dojo 离线库的初始化函数,程序开始运行。


清单 4.离线库的初始化
				 
dojox.off.ui.appName = "Moxie";

dojox.off.files.slurp();

dojo.connect(dojox.off.ui, "onLoad", moxie, moxie.initialize);

dojox.off.initialize();

Moxie 应用本身的初始化又可以分为 5 步 , 即:初始化文件名输入框和编辑器的值;设置各元素的事件处理函数,本例设置了在文件名列表框值改变之后调用 directoryChange 函数和在单击保存按钮时调用 save 方法;随后创建数据库;然后再加载已保存的文件名;最后设置程序的离线事件处理器,以响应 onReplay,onSync 等离线事件。


清单 5.Moxie 的初始化
				 
var richTextControl = dijit.byId("storageValue");

// clear out old values

dojo.byId("storageKey").value = "";

richTextControl.setValue("Click Here to Begin Editing");

// initialize our event handlers

var directory = dojo.byId("directory");

dojo.connect(directory, "onchange", this, this.directoryChange);

dojo.connect(dojo.byId("saveButton"), "onclick", this, this.save);

// create our database

this._createDb();

// load and write out our available keys

this._loadKeys();

// setup our offline handlers

this._initOfflineHandlers();

加载文件列表

开发离线程序与普通 web 应用不同之处在于需要随时考虑在线与离线两种情况并分别进行处理。在线的情况下加载文件列表会调用服务器端的服务获取文件列表,而离线时则从本地数据库读取文件列表。程序的在线或离线状态可以通过查询 dojox.off.isOnline 来获取。

设置离线事件处理器

一般来说应用状态由离线变为在线时,我们需要将在离线时所做的操作重新执行一遍将所做的改动更新到服务器上,另外还要从服务器上下载最新的数据来与服务器保持同步。重新执行离线操作的事件是 dojox.off.sync.actions 对象的 onReplay 事件,下载最新数据的事件是 dojox.off.sync 对象的 onSync 事件,Dojo 离线库会自动检测应用的状态,在适当的时机触发相应的事件,开发人员只需实现处理的函数并与事件绑定即可。值得注意的是 finished 类型的 onSync 事件,这个事件是在上传、下载等事件结束之后触发的,我们可以在这个事件里更新应用的界面。


清单 6.设置离线事件处理器
				 
_initOfflineHandlers: function(){

// setup what we do when we are replaying our action

// log when the network reappears

dojo.connect(dojox.off.sync.actions, "onReplay", this, function(action, actionLog){

if(action.name == "save"){

this._save(action.key, action.value);

}

});

// handle syncing

dojo.connect(dojox.off.sync, "onSync", this, function(type){

// setup how we download our data from the server

if(type == "download"){

this._downloadData();

}else if(type == "finished"){

// refresh our UI when we are finished syncing

this._printAvailableKeys();

}

});

}

运行时的处理

Moxie 运行时的操作主要有两个:一个是保存当前编辑器的内容,一个是根据文件名加载文件内容。同样,所有的操作都需要考虑在线和离线两种情况。

保存文件

在线的时候,保存文件的操作通过 dojo.xhr 将文件名和文件内容上传到了服务器进行持久化保存。在本例中,由于 onReplay 事件处理函数中重用了 _saveOnline 方法将数据保存到了服务器,因此在 ajax 调用的 error 和 load 方法里对两种情况进行了分别处理。我们可以通过 dojox.off.sync.actions.isReplaying 来获取当前是否是处于 replaying 的状态。当然我们也可以在 onReplay 事件中调用其它方法来避免在 _saveOnline 中判断 replaying 状态。读者或许已经注意到了在 load 方法中本例并没有将数据更新到本地数据库中,如果此时网络断开,再从本地读取该文件时会丢失上次所做的操作。


清单 7.在线保存文件
				 
_saveOnline: function(key, value){

dojo.xhrPost({

url: "/moxie/" + encodeURIComponent(key),

content: { "content": value },

error: function(err){

var msg = "Unable to save file " + key + ": " + err;

if(!dojox.off.sync.actions.isReplaying){

alert(msg);

}else{

dojox.off.sync.actions.haltReplay(msg);

}

},

load: dojo.hitch(this, function(data){

this._printStatus("Saved '" + key + "'");

this._addKey(key);

if(!dojox.off.sync.actions.isReplaying){

this._printAvailableKeys();

}else{

dojox.off.sync.actions.continueReplay();

}

})

});

}

离线时,保存文件则先将该操作创建成一个动作对象,加到 dojox.off.sync.actions,以便网络恢复时重新执行,然后将数据保存到本地数据库中。


清单 8.离线保存文件
				 
_saveOffline: function(key, value){

var action = {name: "save", key: key, value: value};

dojox.off.sync.actions.add(action);

if(dojox.sql("SELECT * FROM DOCUMENTS WHERE fileName = ?", key).length){

dojox.sql("UPDATE DOCUMENTS SET content = ? WHERE fileName = ?", value, key);

for(var i = 0; i < this._documents.length; i++){

if(this._documents[i].fileName == key){

this._documents[i].content = value;

break;

}

}

}else{

dojox.sql("INSERT INTO DOCUMENTS (fileName, content) VALUES (?, ?)", key, value);

this._documents.push({fileName: key, content: value});

}

this._printStatus("Saved '" + key + "'");

this._addKey(key);

this._printAvailableKeys();

}

加载文件

当用户选择文件下拉列表框中的文件名时,程序会加载该文件的内容并显示在编辑器里。在线时从服务器端获取,离线时则从本地保存的数据中获取。同样为了保证数据的一致性,在从服务器端获取数据后应该更新本地数据库。





回页首


小结

Web 应用的缺点之一就是在离线的时候程序基本不可操作,而且还可能导致数据丢失,破坏数据完整性等问题。Dojo 离线库的出现为 Web 应用的离线问题提供了一种较好的解决方式,其使用也并不复杂。本文以 Moxie 离线编辑器为例详细介绍了使用 Dojo 离线库的开发过程,包括服务器端和客户端的设计和实现,读者可以以此为参考使用 Dojo 离线库设计和实现丰富的离线应用。



参考资料

学习

获得产品和技术
  • IBM 试用软件:使用这些可直接从 developerWorks 下载的软件构建您的下一个开发项目。


讨论


作者简介

/developerworks/cn/web/0906_dojo_offline_hehj/moying.jpg

莫映,现在IBM中国软件开发实验室Lotus开发中心工作,目前从事Lotus Quickr的Platform Service开发。热衷于Web 2.0相关技术及智能Web 2.0应用的构建。


/developerworks/cn/web/0906_dojo_offline_hehj/zhangshun.jpg

张顺,现在 IBM 中国软件开发实验室 Lotus 开发中心工作,目前从事 Lotus Quickr 的开发定制以及客户支持工作。对 Web 服务,Web2.0 相关技术有浓厚的兴趣。




对本文的评价








IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款