利用GWT开发高性能Ajax应用
近日,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文件中添加此过滤器的声明,例如:
CacheFilter com.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