根据 Jargon, Acronyms, and Buzzwords (JAB) 索引的排名,本文标题中的词得分都很高,但是现在要把它们结合在一起。本文研究几个与 GWT 和 Ajax 相结合的编程模式,它们可以产生更好的 Web 用户体验和更快的响应速度。如果您不熟悉 JAB 索引,请不要担心:这是我刚刚发明的!
让我们从右到左依次讨论标题中的四个词,首先是易用性(usability)。对于网站来说,“容易使用” 意味着具有清晰的屏幕布局和工作流逻辑,不需要特殊培训,等等。在这里,我主要关注响应时间。仅仅有速度还不能提供易用性(尽管性能差的网站的易用性肯定不好),但是我考虑的工具可以在不损害站点其他方面的情况下提高性能。
Ajax 让客户机能够在后台从服务器获取数据,这会向用户提供更流畅的体验;设计良好的 Ajax Web 应用程序可以提供与标准(安装的)程序相似的外观和感觉。在 Ajax 出现之前,对于所有数据请求,用户都要等待服务器响应。有了 Ajax,用户可以继续浏览页面,客户机会在后台悄悄地获取数据。(实际上,不一定非使用 Ajax;适当地使用 iframe,也可以产生相同的效果,但是方法比较复杂)。Ajax 能够让用户避免停顿,这是提高应用程序易用性的重要一步,因此标题中的等式包含 Ajax。
GWT 是一个完全开放源码的 Java™ 开发框架,它让我们能够完全使用 Java 语言开发 Ajax 应用程序。请注意:对于服务器端使用 Java 代码原本就很常见(比如 applet),这里的关键是现在可以把 Java 代码编译为 JavaScript 代码,然后用户的浏览器执行这些 JavaScript 代码。
GWT 会透明地应用 Ajax。客户机应用程序几乎可以像使用客户端 servlet 一样使用服务器端 servlet,这意味着客户机和服务器可以共享类和代码(但有一些限制),可以实现更 “丰富” 的客户机。在 GWT 出现之前,客户机-服务器交互需要复杂的编程,而且不能对客户机使用 Java 代码。GWT 突破了这个障碍,简化了高易用性站点的开发。
什么是模式 ?在软件工程中,模式代表对于常见问题可以广泛应用的概括性解决方案。它不是直接解决方案,而是一个 “路径”,实现的细节要由程序员提供。模式提供一种经过测试、已经证明有效的方案,有助于更快地开发软件。
模式最初是作为一个建筑学概念出现的,常常与建筑进行类比。举一个经常被引用的例子:窗子 是一个让阳光能够照进房间的解决方案,但是它没有指定窗子的具体形状或风格。对于一个问题,可能有多个模式(天窗或开放式天井也可以解决采光问题),要由设计师决定应用哪个模式。本文讨论几个性能问题,提供适合 GWT 的解决方案,您可以在自己的网站上使用它们。
我使用一个简单的数据库(见清单 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 表单用于在数据库中添加新城市
图 3. 如果用户输入重复的城市名,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 特性。因为客户端代码只能操作自己的有限的对象,所以需要两个特殊方法:一个构造函数,可以接收客户端对象并使用它创建服务器端对象;一个方法,可以根据服务器端对象生成客户端对象。
ClientCityData 和 ServerCityData 类演示这个模式。客户端代码需要实现 IsSerializable 接口,以便能够在客户机和服务器之间来回发送对象。ServerCityData 类只能在服务器上使用,它包含上面提到的两个特殊方法。
到目前为止,GWT 帮助我提高了性能,但是它在一个方面会损害性能:缓存。当浏览器请求页面时,它会先搜索自己的缓存。如果在缓存中找到了所需的结果,它就不调用服务器,直接提供数据。(当然,在数据放进缓存之前必须满足许多条件,但是这与目前的问题无关)。问题在于,当 GWT 调用 servlet 时,它通过不可缓存的 Ajax 过程实现 Remote Procedure Call (RPC),所以即使反复请求相同的数据,浏览器也不会使用缓存,每次请求都会有延迟,见图 4。
图 4. Cities Browser 可以分页浏览一个地区的城市
如果页面需要以前得到过的(不变的)信息,那么可以通过设置本地缓存来提高性能,见清单 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 方法),首先检查是否已经获取了这个国家的地区。
最后一个示例(见 CitiesGrid 和 CitiesGridWithCache)有点复杂。地区可以有许多城市,所以信息必须分页显示。我使用一个散列映射作为类属性,但是必须构造一个包含国家、地区和页面起点的键,见 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. 在处理过程期间显示城市(一些城市还没有装载)
为了让这个解决方案工作顺畅,要研究每一步可以执行的最大工作量和各步之间的时间间隔。如果采用大量短步骤,那么计算机的响应性更好,但是也意味着要等待更长时间才能得到所有数据。但是,如果步骤长,可能导致 “busy script” 消息,这也不好。必须通过试验找到最佳折衷点。
延期命令(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.zip | 24KB | HTTP |
学习
- Jesse James Garrett 在 2005 年 2 月的文章 “Ajax: A New Approach to Web Applications” 中首次提出了 Ajax 这个词。
-
Google Code GWT 网站 是必须要访问的网站。还有一份出色的技术解释 how GWT uses Ajax for RPC。
-
网上有许多关于易用性的信息源,比如 Usability.gov 网站和易用性专家 Jakob Nielsen 的 Alertbox columns。
-
Portland Pattern Repository 网站覆盖与模式相关的所有方面,内容非常新。由 “Gang of Four”(Gamma、Helm、Johnson 和 Vlissides)于 1994 年编写的 Design Patterns: Elements of Reusable Object-Oriented Software 是基本参考资料和必备读物。
-
在 Open Ajax Alliance 网站上有对两连接限制问题的 讨论。
-
ISO 维护 ISO 3166 国家和地区编码 (3166-2) 系列。FIPS 提供 地区编码 的列表,编码由两字母的国家编码和两字母的地区编码组成 — 例如,美国伊利诺斯州的编码是 US18;注意,18 是编码而不是常用的缩写 IL。
-
developerWorks Web development 专区 提供与 Web 2.0 开发相关的工具和信息。
- developerWorks 技术活动和网络广播:随时关注 developerWorks 技术活动和网络广播。
-
在 技术书店 浏览关于这些主题和其他技术主题的图书。
获得产品和技术
-
如果需要随机(或接近随机)的数据,可以使用几个免费的 Structured Query Language (SQL) 生成器,比如 GenerateData、DBMonster、Open Datagenerator、dgMaster 和 Spawner Data Generator,还可以使用 DB Data Generator 和 EMS Data Generator 等商业解决方案。但是注意,这些产品提供的功能并不相同。
-
下载 GWT 并试验本文提供的代码。
-
下载 Eclipse Ganymede,这是用于 GWT 的标准集成开发环境 (IDE)。
-
下载 MySQL
— 比较新的 Generally Available (GA) 5.1 版本或经过长时间测试的 5.0 版本。
-
查阅 MaxMind 的 免费的城市表。
讨论
