利用 GWT 开发高性能 Ajax 应用

阅读数:8904 2008 年 4 月 15 日

话题:JavaWeb框架JavaScript语言 & 开发

近日,InfoQ 发表了Ryan Dewsbury 所著的Google Web Toolkit Application》书中的"Integrating with a GWT-RPC Servlet"一章。

对性能的提升是 Ajax 受欢迎的主要原因。我们通常以为那些所谓的眩目变换对于用户来说是 Ajax 最吸引人的地方,可能用户也确实由于这个原因而对 Ajax 独有情钟。如果你回头去看那些传统的 web 应用,会发现它们几乎静态到令人反感,所以说用户仅仅出于这些眩目变换而选择 Ajax 不无道理。然而,如果说眩目的变换真得大大改善了用户体验的话,那么动态的 gif 图片应该受到更广泛的应用才是。谢天谢地,Web 应用早已走过这种幼稚的时代。Ajax 不会再重复动态 gif 图片的老路,它不会再把重点放在这类眩目的变换上了。因此,无论人们是否感受或是意识到,Ajax 真正改善用户体验的地方还是在对性能的提升上。

这篇文章的重点并非要说明 Ajax 天生在哪些方面比传统 Web 应用优秀。关于这个问题,只要将 Google 地图与其他 Web 地图或者将 Gmail 与 Hotmail 进行对比,自然就可以得出结论。当然,应用 Ajax 的确能显著改善性能和用户体验。但在此,我要向大家展示的是如何将 Ajax 应用的性能提高到一个新的层次——从而使您的应用脱颖而出。

选择 GWT 的理由

Google Web Toolkit (GWT) 将 Ajax 的开发推进了一大步,然而面对当下种类繁多的 Ajax 解决方案,此类新技术的推广难免遇到种种挑战。但无可否认,在 Ajax 开发方面,GWT 给开发者提供了其他解决方案无可比拟的便利。如果你还没有受到任何开发框架束缚的话,实在没有什么理由不选择 GWT,因为 GWT 能够无偿的使应用的整体性能得到大幅度提升。

我所说的“无偿”是指在开发中可以抛开性能问题不考虑,而将主要精力集中在业务逻辑方面,因为 GWT 本身已能使性能得到优化。GWT 带有一个能将 Java 代码编译成 JavaScript 的编译器。如果熟悉编译语言(C、Java 等等)的话,你一定了解平台独立性是此类语言追求的一个目标,因此其编译器能够针对特定平台对代码进行优化,这样程序员就可以将重点放在代码的结构组织和可读性上。GWT 编译器也做了类似的事情,它将 Java 代码编译成一些高度优化的 JavaScript 文件,每个文件对应于一种特定的浏览器,其中的优化步骤还应用了普通编译器中的优化方法,去掉了没有被调用的函数和内联代码。此种方式得到的代码相对直接编写的 JavaScript 代码要小的多而且做到了浏览器独立,因此执行效率较高。实质上 GWT 已将 JavaScript 看作 web 中的汇编代码来处理。在浏览器加载 JavaScript 代码的时候,仅仅加载针对该浏览器所需的代码而已。使用 GWT 的应用比任何直接用 JavaScript 实现的应用要来得更精炼更快。对即将发布的 GWT 1.5 版本,GWT 开发团队坚信其编译器生成的代码会比其他任何手工编写的代码都要快。以上这些应该足以说服大家选用 GWT 作为 Ajax 的解决方案,如果还不够,还有许多其他充分的理由,比如你可以在开发 GWT 程序时应用某些 Java 开发工具(能用 Eclipse 来调试 Ajax 程序在我看来确实是一个非常有分量的砝码)。

锦上添花

还远不止这些呢!Ajax 已经比传统 web 应用要出色得多,而 GWT 又远超一般的 Ajax 技术。只简单地做些技术决定就能让你将大部分精力放在业务功能上,达到事半功倍的效果,开发出完美的应用。当然,GWT 并非凭空就能做到这些,下面我将讲述几种进一步提升 GWT 性能方法。

1、始终做好缓存

当你将 GWT 的 Java 代码编译成 JavaScript 后,对应于每个浏览器版本都会有生成一个相应的文件,该文件采用唯一标识的文件名。这些就是你的应用程序的代码文件,直接把它们放到一个 web 服务器上就能发布你的应用了。由于文件名是通过对你的代码进行 Hash 函数计算而得,所以文件名本身就已包含了版本信息。如果你修改了代码后重新编译,生成的文件会有新的文件名。这意味着要么文件已经被下载到了本地浏览器,要么从来没有被请求过,因此就没有必要用检查文件修改日期(HTTP 的 If-Modified-Since 头)的方法来决定是否需要版本更新。这样可以减少很多不必要的 HTTP 请求过程,虽然这些请求过程单独可能很微不足道,但是当用户量达到一定程度,它们就会变成不得不考虑的因素。这类请求对客户端来说也是一种拖累,因为对同一个应用,每个浏览器最多只能有两个活动的请求。很多对 Ajax 下载时间的优化都是从减少向服务器发送的请求量入手的。

为了避免浏览器对版本的请求,你可以通过配置 web 服务器来向客户端发送 Expires HTTP 头。这个 Expires HTTP 头包含页面过期的时间,这样就可以避免浏览器在页面过期时间之前发送版本检查的请求。在 Apache 中设置这些非常容易,只需要将以下内容加入到.htaccess 文件即可:



ExpiresDefault "now plus 1 year"

Apache 会给所有符合 *.cache.* 模式的文件加上 expires 头,设置其失效日期为一年后,此模式将匹配所有 GWT 应用文件。如果你使用的是 Tomcat,也可直接通过 servlet 过滤器来添加头部。增加一个 servlet 过滤器非常简单,只需要在 WEB_INF/web.xml 文件中添加此过滤器的声明,例如:

CacheFiltercom.rdews.cms.filters.CacheFilter



CacheFilter/gwt/*

这样 tomcat 就知道在哪里找到此过滤器、知道哪些文件可以通过该过滤器。本例中,/gwt/* 模式表示 gwt 文件夹下的所有文件。这个过滤器的实现类将通过 doFilter 方法来添加 Expires 头。对 GWT 应用来说,我们需要在每个不符合 *.nocache.* 模式的文件里添加此 Expires 头。nocache 文件是不需要缓存的,因为其中含有版本选择的逻辑。以下是这个过滤器的实现代码:

public class CacheFilter implements Filter {

private FilterConfig filterConfig;

public void doFilter( ServletRequest request, ServletResponse response,



FilterChain filterChain) throws IOException, ServletException {

HttpServletRequest httpRequest = (HttpServletRequest)request;

String requestURI = httpRequest.getRequestURI();



if( !requestURI.contains(".nocache.") ){

long today = new Date().getTime();

HttpServletResponse httpResponse = (HttpServletResponse)response;

httpResponse.setDateHeader("Expires", today+31536000000L);

}

filterChain.doFilter(request, response);

}

public void init(FilterConfig filterConfig) throws ServletException {



this.filterConfig = filterConfig;

}

public void destroy() {



this.filterConfig = null;

}

}

2. 程序压缩

通过去掉未被调用的方法和艰涩的代码、使用简短的变量名和方法名等方式,GWT 编译器在减少代码量方面表现得非常出色,但是最后得到的代码文本仍然是未经压缩的。因此可以通过 gzip 压缩需要部署的应用程序的方法进一步减小代码文件的大小。gzip 可以将应用程序压缩到原来的 70%左右,从而提高应用的下载速度。

幸运的是,文件压缩也可以简单地通过配置服务器来实现,唯一要做的只是在 Apache 服务器的.htaccess 文件中加上以下语句:

SetOutputFilter DEFLATE

Apache 首先会自动与浏览器进行沟通,根据浏览器的支持情况从而决定是否发送压缩版本,不过目前所有主流浏览器都支持 gzip 压缩。

如果使用的是 Tomcat,那么可以直接利用 server.xml 文件中 Connector 元素,只要加上以下的属性就可以进行程序文件的压缩了:

compression="on"

3. 图片打包

Ajax 应用借助于浏览器和 HTTP 协议强大的分布力量,然而浏览器和 HTTP 协议本身对分布 Ajax 应用没有特别的优化。Ajax 应用是需要部署的,在这一点上它跟桌面应用程序有些相象,而传统的 web 程序使用的是共享资源分布模型(shared resource distribution model),在程序运行过程中浏览器和服务器间会不断进行交互,从而对页面所需要的资源进行管理。这种方式使资源能够在页面间共享和缓存,从而保证打开新页面所需的下载量达到最小化。在 Ajax 应用中,资源一般不会分布在页面间,因此不需要单独对其进行下载缓存。不过,对于 Ajax 应用,在下载应用程序资源时采用传统的分布式模型也并非不可行,许多 Ajax 应用也正是这么做的。

然而,你可以选择将程序中用到的所有图片合并到一个文件中,以减少 HTTP 请求的次数。这样可以突破同一时间只能发送两个请求的限制,一次性地下载所有图片。

GWT 从 1.4 版本开始支持 ImageBundle 接口。在这个接口中可以为每一个图片建立一个方法,编译器会将所有的图片组合到一个文件中,并将图片数据的 Hash 做为新文件的文件名(象程序代码一样永久缓存这个文件)。一次性打包合并的图片数量是没有限制的,所有这些图片只需要一次请求就可以全部下载。

在已经完成的几个 GWT 项目中我一直沿用将基本图片打包的做法,以下是示例代码:

public interface Images extends ImageBundle {

/**



* @gwt.resource membersm.png

*/

AbstractImagePrototype member();

/**

* @gwt.resource away.png

*/

AbstractImagePrototype away();

/**



* @gwt.resource starsm.gif

*/

AbstractImagePrototype star();

/**



* @gwt.resource turn.png

*/

AbstractImagePrototype turn();

/**



* @gwt.resource user_add.png

*/

AbstractImagePrototype addFavorite();

}

需要注意的是每个方法都有一个公共注解来指明图片的文件名,方法的返回类型都是 AbstractImagePrototype。 AbstractImagePrototype 类的 createImage 方法将返回一个可以在程序接口中使用的图片 widget。以下的代码揭示了如何 使用该图片包:

Images images = (Images) GWT.create(Images.class);

mainPanel.add( images.turn().createImage() );

这一切看上去很简单,不过正是这些看似简单的东西开启了 GWT 性能提升之门。

4. 使用 StyleInjector

我们又该如何处理 CSS 文件以及 CSS 图片等应用程序资源呢?在传统的 web 分布模型中,这些都作为外部资源而被独立下载和缓存。在 Ajax 应用中,这样做意味着额外的 HTTP 请求和缓慢的程序加载。目前,GWT 对此尚未提供任何优化,但在 GWT 的官方孵化项目中有一些很有意思的 GWT 代码,这些代码很可能会包含在 GWT 的未来版本中,其中尤其值得关注的是 ImmutableResourceBundle 和 StyleInjector 两个类。

ImmutableResourceBundle 的功能和 ImageBundle 很相似,但是它可以用于包括 CSS 和 CSS 图片在内的任何类型的资源。这个类的目的在于为程序资源提供一个抽象,使得处理它们的方式对浏览器来说达到最优化。下面这个类即是一个可用于加载 CSS 文件及其相关资源的例子:

public interface Resources extends ImmutableResourceBundle {

/**



* @gwt.resource main.css

*/

public TextResource mainCss();

/**



* @gwt.resource back.gif

*/

public DataResource background();

/**



* @gwt.resource titlebar.gif

*/

public DataResource titleBar();

/**

* @gwt.resource dialog-header.png

*/

public DataResource dialogHeader();

}

这个类会为每个资源指定一个文件和方法,这一点和 ImageBundle 非常类似,但它的返回类型是 DataResource 或 TextResource。对于 TextResource 类,我们可以通过其 getText 方法得到指定文件中的内容,而对于 DataResource 类,我们可以用 getUrl 方法来得到资源的引用(例如对图片或者 IFRAME 的引用)。不同的浏览器对这些数据的加载方式各不相同,但我们无须担心这些。大多数情况下,数据会通过使用 URL 前缀以内联 URL 的方式出现。这个类的用途很广泛,但是最直接的应用可能还是将 CSS 与其他程序文件一块打包使用。

可以注意到,在这个接口中引用了一个 CSS 文件及其一些图片。在这种情况下,该接口被拿来将 CSS 及其图片与程序文件进行打包,从而减少 HTTP 请求的次数和缩短应用启动时间。在 CSS 文件中一般会指定一些背景图片,但会使用占位符(placeholder)来取代真实的图片 URL。这些占位符被用来引用打包的文件中其他一些元素,尤其是图片。例如,main.css 文件有这样一个名为 gwt-DialogBox 的 CSS 规则:

.gwt-DialogBox{ background-image:url('%background%') repeat-x; }

如果要在程序中应用此 CSS 文件和图片,你需要用到孵化项目中的 StyleInjector 类。StyleInjector 会将 CSS 文件中的占位符匹配到打包文件中的特定资源,然后再将 CSS 文件注入到浏览器中供应用程序使用。这听起是挺复杂,但实际使用还是比较方便的,重点是它能改善应用的性能。下面这段代码是使用 StyleInjector 将 CSS 从资源包注入到应用程序中的一个例子:

Resources resources = (Resources)GWT.create(Resources.class);

StyleInjector.injectStylesheet( resources.mainCss().getText(), resources );

需要注意的是以上这些目前还是孵化项目的一部分,在正式发布前随时都有可能做调整。

结论

总之,Ajax 应用相对于传统 web 应用在使用性上有质的飞跃,同时 GWT 所提供的工具能使你的 Ajax 性能无偿地得到大幅度提升。关于这一点,你可以将GWT mail sample的启动速度跟其他 Ajax 应用范例做个比较。如果再在传统 web 应用和 Ajax 应用间在部署差异加以关注的话,我们还可以进一步提高应用的性能。对于下一代的 Ajax 应用,我充满了期待。

作者介绍

Ryan Dewsbury 精通 C++ 和 Java 语言,1998 年从业以来分别从事过程序员、架构师和咨询顾问等多种角色的工作。起初几年 Ryan 在协助一个半导体制造公司构建系统框架。最近他主要应用前沿软件为一些创业型互联网公司改善用户体验。另外,这些年 Ryan 还从事过一些独立软件项目的开发工作,包括 2004 年的 Easy Message,以及最近的两个基于 GWT 的 web 游戏网站 Gpokr (gpokr.com) 和 KDice (kdice.com)。

注:文中的代码或者文字并不涉及安全或异常处理。

本文内容摘自《Google Web Toolkit Applications》一书,作者Ryan Dewsbury,Prentice Hall Professional 2007 年 11 月出版,版权所有,Pearson Education,Inc 2008,ISBN:0321501969 。更多信息请浏览www.informit.com/title/0321501969

查看英文原文:High Performance Ajax with GWT