跳转到主要内容

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在 developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。

所有提交的信息确保安全。

  • 关闭 [x]

当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

所有提交的信息确保安全。

  • 关闭 [x]

模式 + GWT + Ajax = 易用性!

用 Ajax、GWT 和设计模式增强您的网站

Federico Kereki, 系统工程师, 自由职业者
http://www.ibm.com/developerworks/i/p-fkereki.jpg
Federico Kereki 是一名来自乌拉圭的系统工程师,拥有超过 20 年的开发、咨询和大学教学的经验。目前,他正在与各种各样的缩略词打交道:SOA、GWT、Ajax、PHP,当然还有 FLOSS!

简介: Google Web Toolkit (GWT) 让我们可以更轻松地开发复杂的网站。通过与一些增强易用性的设计模式和 Asynchronous JavaScript and XML (Ajax) 相结合,这些技术可以为应用程序提供更流畅的外观和感觉,让应用程序比典型的网页更接近传统桌面程序。

发布日期: 2009 年 9 月 09 日
级别: 中级 其他语言版本: 英文
访问情况 : 2800 次浏览
评论: 


根据 Jargon, Acronyms, and Buzzwords (JAB) 索引的排名,本文标题中的词得分都很高,但是现在要把它们结合在一起。本文研究几个与 GWT 和 Ajax 相结合的编程模式,它们可以产生更好的 Web 用户体验和更快的响应速度。如果您不熟悉 JAB 索引,请不要担心:这是我刚刚发明的!

易用性

让我们从右到左依次讨论标题中的四个词,首先是易用性(usability)。对于网站来说,“容易使用” 意味着具有清晰的屏幕布局和工作流逻辑,不需要特殊培训,等等。在这里,我主要关注响应时间。仅仅有速度还不能提供易用性(尽管性能差的网站的易用性肯定不好),但是我考虑的工具可以在不损害站点其他方面的情况下提高性能。

Ajax

Ajax 让客户机能够在后台从服务器获取数据,这会向用户提供更流畅的体验;设计良好的 Ajax Web 应用程序可以提供与标准(安装的)程序相似的外观和感觉。在 Ajax 出现之前,对于所有数据请求,用户都要等待服务器响应。有了 Ajax,用户可以继续浏览页面,客户机会在后台悄悄地获取数据。(实际上,不一定非使用 Ajax;适当地使用 iframe,也可以产生相同的效果,但是方法比较复杂)。Ajax 能够让用户避免停顿,这是提高应用程序易用性的重要一步,因此标题中的等式包含 Ajax。

Google Web Toolkit

GWT 是一个完全开放源码的 Java™ 开发框架,它让我们能够完全使用 Java 语言开发 Ajax 应用程序。请注意:对于服务器端使用 Java 代码原本就很常见(比如 applet),这里的关键是现在可以把 Java 代码编译为 JavaScript 代码,然后用户的浏览器执行这些 JavaScript 代码。

GWT 会透明地应用 Ajax。客户机应用程序几乎可以像使用客户端 servlet 一样使用服务器端 servlet,这意味着客户机和服务器可以共享类和代码(但有一些限制),可以实现更 “丰富” 的客户机。在 GWT 出现之前,客户机-服务器交互需要复杂的编程,而且不能对客户机使用 Java 代码。GWT 突破了这个障碍,简化了高易用性站点的开发。

模式

什么是模式 ?在软件工程中,模式代表对于常见问题可以广泛应用的概括性解决方案。它不是直接解决方案,而是一个 “路径”,实现的细节要由程序员提供。模式提供一种经过测试、已经证明有效的方案,有助于更快地开发软件。

模式最初是作为一个建筑学概念出现的,常常与建筑进行类比。举一个经常被引用的例子:窗子 是一个让阳光能够照进房间的解决方案,但是它没有指定窗子的具体形状或风格。对于一个问题,可能有多个模式(天窗或开放式天井也可以解决采光问题),要由设计师决定应用哪个模式。本文讨论几个性能问题,提供适合 GWT 的解决方案,您可以在自己的网站上使用它们。


测试数据库

构建大型测试数据库

为了测试服务器端 servlet 和客户机的速度,我需要一个足够大的数据库:我使用 MaxMind 的免费城市表(见 参考资料)。我增加了 International Organization for Standardization (ISO) 3166 国家编码表以及 ISO 3166-2 和 Federal Information Processing Standards (FIPS) 10-4 地区编码表。城市数据使用 AK(代表阿拉斯加州)这样的编码而不是数字的 ISO 编码。通过逗号分隔的值 (CSV) 文件提供所需的数据,很容易装载到 MySQL 表中。

也可以使用随机(或接近随机)的数据,但是城市信息更有条理。生成随机数据的工具见 参考资料

我使用一个简单的数据库(见清单 1),其中包含世界上的国家、地区和城市信息。我需要一个足够大的表和开放的数据库(见边栏 “构建大型测试数据库”),大约包含三百万条记录,才能满足我的需要。对于后面的示例,请注意:

  • 国家 由一个编码标识(比如 US 代表美国),还有一个名称。
  • 国家包含地区,地区由一个编码标识(编码常常是数字的,在这个国家内是惟一的),还有一个名称。
  • 城市 属于某一国家的某一地区,城市信息包括(纯 ASCII)名称、带重音符的名称(可能包含外文字符)、人口数(如果人口数未知,就是 0)、纬度和经度。城市名称在这个国家的这个地区内是惟一的;仅在美国就有大约 30 多个名为 Springfield 的城市!



清单 1. 数据库创建代码
				
CREATE DATABASE world
  DEFAULT CHARACTER SET latin1
  COLLATE latin1_general_ci;

USE world;

CREATE TABLE countries (
  countryCode char(2) NOT NULL,
  countryName varchar(50) NOT NULL,
  PRIMARY KEY (countryCode)
  KEY countryName (countryName)
);

CREATE TABLE regions (
  countryCode char(2) NOT NULL,
  regionCode char(2) NOT NULL,
  regionName varchar(50) NOT NULL,
  PRIMARY KEY (countryCode,regionCode),
  KEY regionName (regionName)
);

CREATE TABLE cities (
  countryCode char(2) NOT NULL,
  cityName varchar(50) NOT NULL,
  cityAccentedName varchar(50) NOT NULL,
  regionCode char(2) NOT NULL,
  population bigint(20) NOT NULL,
  latitude float(10,7) NOT NULL,
  longitude float(10,7) NOT NULL,
  KEY `INDEX` (countryCode,regionCode,cityName),
  KEY cityName (cityName),
  KEY cityAccentedName (cityAccentedName)
);
         

我开发了一个 GWT 项目(完整的源代码清单见 下载),它包含一个简单的菜单(图 1)和两个同样简单的 Web 表单:Cities Creator(用于在数据库中添加新城市 — 见图 2)和 Cities Browser(用于分页浏览任何国家地区中的城市 — 见图 3)。我以最简单的方式编写这个项目,尽可能避免多余的东西,因为我的目的是展示模式。我使用 GWT 1.5.3 和 MySQL 5.0;在 OpenSUSE version 10.3 Linux® 上运行的 Eclipse Ganymede 中进行开发。


图 1. 示例应用程序的主菜单,显示两个可用表单
示例应用程序的主菜单,显示两个可用表单

图 2. Cities Creator 表单用于在数据库中添加新城市
Cities Creator 表单用于在数据库中添加新城市

图 3. 如果用户输入重复的城市名,Ajax 后台检查会发出警告,突出显示这个控件
如果用户输入重复的城市名,Ajax 后台检查会发出警告,突出显示这个控件

模式:预检验

客户机-服务器计算的原则是在服务器端检查所有东西。(即使在调用服务器之前检验数据,其他用户所做的修改也可能导致原来正确的数据无效。例如,原来存在的文章现在可能已经消失了)。但是,不希望让用户等待客户机-服务器往返,然后才提示一个简单的错误。解决方案:在后台对服务器端检查例程执行一个 Ajax 调用;如果有错误,就警告用户,突出显示不正确的字段。

研究一下 CitiesCreatorForm 类和它的 addDuplicateCityNameCheck 方法。假设用户不应该输入已经存在的城市名。假设服务器端的 cityExists 服务会检查重复的城市名,那么在 cityName 文本框上添加一个 ChangeListener;如果用户选择国家和地区并输入城市名,就调用服务,检查城市名是否是重复的。

但是,代码有一个小问题。假设一个输入速度很快的用户输入一个(重复的)城市名,但是马上意识到了错误并纠正了错误。过了一会儿,他收到警告,指出这个字段是错误;但是,实际上这个字段现在是正确的。可以在 CitiesCreatorForm2 类中采取一种简单的纠正措施(见清单 2):保存服务参数,当收到服务的响应时,检查表单上的值是否仍然与参数相同;如果不同,就不做任何事情。


清单 2. 预检验模式伪代码
				
create a new ChangeListener that will:
    get the form field values needed for the check
    if all fields are filled
        save the form field values
        call the server-side service to perform the check
        on callback:
            get the form field values again
            if the current values match the saved values,
                if there was an error,
                    highlight the fields
                    warn the user
                otherwise
                    reset fields to normal

assign the created ChangeListener to all involved form fields
            


模式:代码共享

检查不需要都在服务器上执行,而且客户端检查越多,应用程序就越敏捷。在使用传统的 Web 开发工具时,这意味着所有检查都要编写两次(一次用于服务器,一次用于客户机),但是 GWT 允许在两端使用相同的 Java 代码。服务器端代码可以使用所有 Java 特性,而客户端代码要编译为 JavaScript 代码,而且受到 JavaScript 语言的限制。例如,JavaScript 代码不能使用文件;因此不能在客户端使用 java.io。

代码共享模式实现类的客户端版本,然后针对服务器扩展它,可以使用所有 Java 特性。因为客户端代码只能操作自己的有限的对象,所以需要两个特殊方法:一个构造函数,可以接收客户端对象并使用它创建服务器端对象;一个方法,可以根据服务器端对象生成客户端对象。

ClientCityDataServerCityData 类演示这个模式。客户端代码需要实现 IsSerializable 接口,以便能够在客户机和服务器之间来回发送对象。ServerCityData 类只能在服务器上使用,它包含上面提到的两个特殊方法。


模式:缓存

到目前为止,GWT 帮助我提高了性能,但是它在一个方面会损害性能:缓存。当浏览器请求页面时,它会先搜索自己的缓存。如果在缓存中找到了所需的结果,它就不调用服务器,直接提供数据。(当然,在数据放进缓存之前必须满足许多条件,但是这与目前的问题无关)。问题在于,当 GWT 调用 servlet 时,它通过不可缓存的 Ajax 过程实现 Remote Procedure Call (RPC),所以即使反复请求相同的数据,浏览器也不会使用缓存,每次请求都会有延迟,见图 4。


图 4. Cities Browser 可以分页浏览一个地区的城市
Cities Browser 表单的屏幕图。其中显示 Country/Region 下拉框和另一个显示 'Select a region...' 的下拉框。

如果页面需要以前得到过的(不变的)信息,那么可以通过设置本地缓存来提高性能,见清单 3。在调用服务器之前,检查所需的数据是否已经装载了;如果是这样,就跳过调用。当然,不要对频繁变动的信息使用缓存。如果信息会随着时间的推移过时,那么添加时间戳以避免使用陈旧数据。


清单 3. 缓存模式伪代码
				
class_with_cache code:
    define class attributes for the cache (a hash map, array, whatever)
    set the cache to empty

    whenever new data are asked for:
        check if the asked data are already in the cache
        if so,
            get the data from the cache
            perform whatever needs be done with it
        otherwise,
            display an appropriate "loading" message
            call a server-side service to get the data
            on callback:
                put the data in the cache
                perform whatever needs be done with it
            

在提供的代码中,有三个此过程的示例 — 都针对 CitiesBrowser 类。最简单的一个示例应用于显示所有国家的列表框(见 CountryList 类)。它的基本实现对于每个对象都调用服务器(请求国家列表)。修改后的 CountryListWithCache 类把国家列表保存在一个类变量中,让所有对象可以共享它;实际上,只在创建第一个对象时调用服务器。

我还需要地区列表框(见 RegionList 类),它的内容应该取决于当前选择的国家。全世界有几千个地区,获取所有地区是不实际的。为了实现缓存(见 RegionListWithCache 类),我使用一个散列映射;当国家改变时(见 changeCountry 方法),首先检查是否已经获取了这个国家的地区。

最后一个示例(见 CitiesGridCitiesGridWithCache)有点复杂。地区可以有许多城市,所以信息必须分页显示。我使用一个散列映射作为类属性,但是必须构造一个包含国家、地区和页面起点的键,见 LoadCities 方法。


模式:预抓取

当需要从服务器向客户机发送大量数据时,需要采取某种措施。如果提前知道用户将要请求哪些信息,可以使用 Ajax 机制提前请求数据。不一定总能提前猜出用户将要请求什么,所以可能会犯错误,取得一些不需要的数据;必须权衡考虑这种风险和不采用预抓取时肯定会出现的延迟。

但是要注意:不要太过分,不要预抓取所有东西,否则会适得其反!自从低带宽的拨号调制解调器时代以来,浏览器一直限制客户机-服务器连接的数量。这个限制甚至影响了 Hypertext Transfer Protocol (HTTP) 1.1 标准(“一个客户机到任何服务器或代理的连接数量不应该超过两个”)。如果您向主机发送多个请求,只有两个请求会(同时)发出,其他请求会排队,这会导致比一般情况下更长的延迟。

CitiesBrowserWithCacheAndPreFetching 展示实现预抓取所需的修改。首先,修改 loadCities 方法,让它不要总是一装载数据就马上显示在屏幕上:在预抓取时,不显示任何数据。第二,当显示一个页面时(见 showCities 方法),预抓取下一个页面(这是符合逻辑的猜测),但是不显示它。最后,当用户选择一个国家和地区时,预抓取前两个页面以备显示,见清单 4。注意,如果代码请求预抓取已经获得的页面,实现的逻辑会避免不必要的服务器调用。


清单 4. 预抓取模式伪代码
				
class_with_cache_and_pre-fetching code:
    define class attributes for the cache (a hash map, array, whatever)
    set the cache to empty

    load_data method:
        check if the asked data are already in the cache
        if so,
            get the data from the cache
            perform whatever needs be done with it
        otherwise,
            display an appropriate "loading" message
            call a server-side service to get the data
            on callback:
                put the data in the cache
                if data were needed (as opposed to prefetched),
                    perform whatever needs be done with it

    processing_data method:
        call the load_data method to get that data
        call the load_data method to get extra (prefetched) data
            


模式:线程模拟

现在考虑处理器密集型任务,比如处理大量 XML 或显示大量数据。如果任务花费的时间太长,用户就会收到消息:在 Firefox 浏览器上,“A script on this page may be busy, or it may have stopped responding. You can stop the script now, or you can continue to see if the script will complete”;在 Windows® Internet Explorer® 上,“Stop running this script? A script on this page is causing Internet Explorer to run slowly. If it continues to run, your computer may become unresponsive”。更糟糕的是,如果用户注意到这个警告并停止脚本,那么实际上就取消了您的客户端程序!

这个错误通常可以用线程来解决,但是 GWT 不允许这么做,因为 JavaScript 语言只提供一个执行线程,所以编译后的线程化代码无法正常工作。Ajax 为服务器端处理提供了解决方法,但是不适用于客户机。幸运的是,有两个模式可以解决这个问题;我应用它们显示城市页面。

基于计时器的解决方案

GWT 提供一个 Timer 类,其中的 schedule() 方法与 JavaScript 语言自己的 setTimeout() 方法相似。原理是先做一部分工作并存储值,以后可以继续处理(经过超时之后),这样会在其间释放处理器,见清单 5。检查是否需要继续处理;用户可能决定向前或向后翻页,对于前面的页面数据不再处理。


清单 5. 用计时器模拟线程
				
define a class that extends Timer:
    define attributes so it can save its parameters
    define attributes so it can save local variables from run to run
    define attributes so it can save form field values

    on construction:
        save the received parameters
        initialize local variables for the process
        save the current form field values
        display a "loading" message

    run() method:
        if the current form field values match the saved values:
            execute some process, updating the local variables
            if there's still more work to be done
                schedule another process in a short while

whenever you want to simulate a thread with a timed method:
    create an object of the new class above, with appropriate parameters
    execute its run() method
            

CitiesGridWithCacheAndPreFetchingAndTimer 类演示这个模式。私有的 TimedCitiesDisplay 类扩展 Timer 类。在构造时,它接收一个城市列表并初始化一个迭代器;它还保存当前的国家、地区和页面,以便在以后检查是否必须继续处理。run() 方法处理一批城市;如果还有更多的城市,它会调度下一次运行,下一次运行从原来停止的位置开始继续处理,如图 5 所示。


图 5. 在处理过程期间显示城市(一些城市还没有装载)
Cities Browser 表单的屏幕图。其中的 Country/Region 下拉框显示 'Uruguay',另一个下拉框显示 'Colonia'。下面有 'First 50 cities'、'Previous 50' 和 'Next 50' 按钮。

为了让这个解决方案工作顺畅,要研究每一步可以执行的最大工作量和各步之间的时间间隔。如果采用大量短步骤,那么计算机的响应性更好,但是也意味着要等待更长时间才能得到所有数据。但是,如果步骤长,可能导致 “busy script” 消息,这也不好。必须通过试验找到最佳折衷点。

GWT 自己的延期命令

延期命令(Deferred command) 是 GWT 特有的特性,它可以提供更好的解决方案。延期命令排队等待,当处理器空闲时执行,见清单 6。解决方案:把处理划分为短的步骤,但是用延期命令替代 Timer。GWT 会决定什么时候运行下一个计算步骤。


清单 6. 用延期命令模拟线程
				
define a class that extends IncrementalCommand:
    define attributes so it can save its parameters
    define attributes so it can save local variables from run to run
    define attributes so it can save form field values

    on construction:
        save the received parameters
        initialize local variables for the process
        save the current form field values
        display a "loading" message

    execute() method:
        if the current form field values match the saved values:
            execute some process, updating the local variables
            if there's still more work to be done
                return true, so it will run again shortly afterwards
            otherwise,
                return false (the job is done)
        otherwise,
            return false (situation changed)

whenever you want to simulate a thread with a deferred command:
    create an object of the new class above, with appropriate parameters
    use the addCommand() to add your new object to the processing queue
            

CitiesGridWithCacheAndPreFetchingAndDeferredCommands 类演示这个模式。它与计时器解决方案的主要差异是命令排队等待,如果必须把处理发送回队列等待下一次运行,那么 execute() 方法返回 True。

这个解决方案比计时器解决方案灵活,因为如果用户不做任何事情,它会全速运行,同时不会影响计算机的响应性。但是,不要做得太过分。


结束语

本文讨论了一些设计模式,它们通过在后台使用 Ajax 技术提高 GWT 应用程序的速度。提供了一些解决方案来克服常见的 JavaScript 限制(比如缺少线程),降低向服务器请求数据时的时间延迟(通过预抓取、缓存和在客户机本地预检验)。GWT、Ajax 和一些模式有助于提高应用程序的速度,增强它的易用性,为用户提供响应性更好的网站。



下载

描述名字大小下载方法
本文的完整源代码full_source_code.zip24KBHTTP

关于下载方法的信息


参考资料

学习

获得产品和技术

讨论

关于作者

http://www.ibm.com/developerworks/i/p-fkereki.jpg

Federico Kereki 是一名来自乌拉圭的系统工程师,拥有超过 20 年的开发、咨询和大学教学的经验。目前,他正在与各种各样的缩略词打交道:SOA、GWT、Ajax、PHP,当然还有 FLOSS!

关于报告滥用的帮助

报告滥用

谢谢! 此内容已经标识给管理员注意。


关于报告滥用的帮助

报告滥用

报告滥用提交失败。 请稍后重试。


developerWorks:登录


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 使用条款

 


当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在 developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。

请选择您的昵称:

当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

(长度在 3 至 31 个字符之间)


单击提交则表示您同意developerWorks 的条款和条件。 使用条款.

 


为本文评分

评论

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=426996
ArticleTitle=模式 + GWT + Ajax = 易用性!
publish-date=09092009
author1-email=fkereki@gmail.com
author1-email-cc=

标签

Help
使用 搜索 文本框在 My developerWorks 中查找包含该标签的所有内容。

使用 滑动条 调节标签的数量。

热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。

我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。

使用搜索文本框在 My developerWorks 中查找包含该标签的所有内容。热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。