
1.4 第一个Ajax应用实例
1.4.1 传统的聊天室
B/S结构的聊天室要实现的功能有两个:第一个功能是对用户的管理,包括用户登录和用户注册等;第二个功能是管理用户的聊天信息,系统需要保持用户最近的聊天信息。
通常情况下,系统会将用户信息、聊天信息都保存在数据库里。本应用为了简化,用户信息以Properties文件进行保存,用户聊天信息保存在内存中(使用一个List保存)。
该B/S聊天室遵循MVC的开发模式:客户端向控制器发送请求,控制器负责拦截用户请求,调用Model处理用户请求,控制器根据Model的处理结果,决定向用户呈现怎样的界面。B/S聊天室的业务逻辑非常简单,包含如下功能。
(1)用户注册:向保存用户名、密码的文件中增加一条记录。
(2)用户登录:判断用户输入的用户名、密码是否正确,若正确则会跳转到聊天页面,否则不跳转。
(3)用户聊天:发送消息让所有用户看到。
聊天室的组件关系如图1-6所示。

图1-6 聊天室的组件关系
1.实现业务逻辑组件
系统没有采用数据库存放用户信息,而是使用Properties文件存放用户名和密码。所有的用户登录验证、新用户注册都需要通过Properties文件校验。业务逻辑组件提供如下方法用于加载属性文件。

上面的程序代码用于实现读取userFile.properties文件中的用户名和密码信息。这个方法是个工具方法,用于加载所有用户名和密码。userList保存了当前系统中所有用户名和密码,它是ChatService对象的实例属性,是一个Properties对象,其中属性名是用户名,属性值是密码。
如果系统的注册用户非常多,则属性文字非常大,userList也将非常大,这可能导致系统的性能下降,因此采用数据库保存信息更加合适。本例只为演示用,因此没有采用数据库。
此外,还有对应的方法用于将userList保存到Properties文件中,每次用户注册成功后都应该将新注册的用户保存到Properties文件中。保存userList的方法如下:

上面的粗体字代码用于将userList对象中的用户名、密码信息保存到userFile.properties文件中。
上面的两个方法都是系统进行持久化的方法,只不过此处的持久化无须访问数据库,而只是使用Properties文件来保存持久化信息。业务逻辑对象必须向控制器提供的方法如下:
(1)boolean validLogin(String user,String pass):用于判断用户名和密码是否可以成功登录。
(2)boolean addUser(String name,String pass) :用于注册用户时向Properties文件中增加记录。
(3)String getMsg():用于获取系统所保存的所有用户的聊天信息。
(4)void addMsg(String user,String msg):用于增加聊天信息。聊天信息是瞬态信息,系统没有对聊天信息完成持久化,但每个用户的发言应该被增加到聊天信息。
本聊天系统的业务逻辑组件直接依赖上面的工具方法进行持久化,所以无须依赖持久化组件。业务逻辑组件ChatService的代码如下:



2.实现控制器
系统的控制器由Servlet充当,Servlet负责拦截用户请求,然后调用ChatService对象处理用户请求,根据处理结果,将请求forward到合适的页面显示。本系统包含三个用例:用户注册、用户登录和用户聊天。系统为每个请求配置一个控制器。控制器的运行结构大致相似,下面以注册所用的控制器为例进行讲解。



如上面程序所示,该RegServlet调用ChatService对象的addUser()方法来注册新用户,也就是控制器调用业务逻辑组件方法来处理用户请求。
其余两个控制器ChatServlet和LoginServlet与此类似,ChatServlet调用addMsg()和getMsg()方法来添加聊天信息和显示聊天信息。两个控制器调用getMsg()方法获取聊天记录后,将聊天记录放置到HttpServletRequest的msg属性中。JSP页面则直接通过如下的表达式语言来输出聊天信息:

聊天界面由一个文本域和一个文本框组成,文本框负责收集用户输入的聊天信息,文本域负责显示当前所有用户的聊天信息。聊天页面的代码片段如下:

上面代码中文本区用于显示系统的聊天信息,下面表单中文本框和按钮用于输入聊天信息和发送聊天信息。除此之外,该页面也使用了JavaScript来提供客户端输入校验,详细代码可查看源码。
前面的程序已经实现了一个简单的B/S聊天室,但这个B/S聊天室存在一些小问题。传统B/S结构的应用都是基于请求/响应的应用。客户端向服务器发送请求,而服务器则生成对客户端的响应。在这种结构模式里,服务器不会主动向客户端发送响应。如果客户端不发送任何请求,则即使系统的聊天信息发生改变,用户也依然看不到其他用户的聊天信息。
当用户发送请求时,请求被控制器截获,控制器处理完用户请求后,将请求转发到JSP页面,由该JSP页面呈现处理结果。关键问题就在这里:每次用户发送请求后只能等待服务器响应,如果服务器响应很慢,客户端浏览器就将一直等待,什么事情也做不了。如果客户端想再次发送请求,则完全不可能,因为服务器没有生成响应,即客户端的浏览器是一片空白。
当用户发送聊天信息时,客户端浏览器需要不断地下载聊天页面,即每次发送聊天信息后,都需要重新下载页面。
服务器每次响应都会生成一个完整的页面。在实际应用中,完整页面包含的内容相当多,少则几百行,多则几千行、上万行。有时除了少量的数字和文字需要改变,页面的其他修饰、效果、图片等都无须更新,但客户端必须重新下载这些已经下载过的资源。相同资源的大量重复下载,严重占用了客户的网络带宽,也使得客户端的速度变慢。总体来说,传统的B/S聊天室有如下问题。
(1)JSP页面无法异步发送请求,用户请求与服务器响应严格交替:用户请求→服务器响应。如果用户没有发送请求,服务器就不会响应;如果服务器响应没有完成,用户就无法再次发送请求。
(2)服务器响应后总是生成完整JSP页面,导致大量下载重复资源。
1.4.2 使用Ajax实现聊天室功能
针对传统的B/S聊天室所存在的问题,Ajax技术进行了相应的改进。Ajax技术并不是要取代B/S结构的应用,而是更好地完善了传统的Web应用。
对于JSP存在的问题,Ajax都有非常好的解决方案:Ajax使用XMLHttpRequest异步发送请求,Ajax的服务器响应仅是必须更新的数据,而不再是整个页面。JavaScript负责将必须更新的数据加载到视图页面中。
使用Ajax可提高页面的复用:通过使用Ajax技术,请求和页面分离开,一个视图页面可以发送多个请求,因而用户可以长时间使用同一个页面,故可以更好地复用一个已下载的页面。
1.异步发送请求
异步发送请求是Ajax最核心的内容,Ajax中的A就代表Asynchronous(异步的),Ajax使用XMLHttpRequest对象异步发送请求。在某种程度上,Ajax是以XMLHttpRequest对象为核心,结合JavaScript、DOM、CSS后组成的新技术。
为了使用XMLHttpRequest对象,必须先创建XMLHttpRequest对象,创建该对象的代码如下:


上面程序中的代码可以在Internet Explorer、Firefox、Opera(除IE之外的其他浏览器都会遵守DOM2规范)等浏览器中创建XMLHttpRequest对象。因为XMLHttpRequest在不同的浏览器中实现方式不同,因而在不同的浏览器中创建XMLHttpRequest对象的方式也略有差异。
一旦XMLHttpRequest对象创建成功,系统就可以使用XMLHttpRequest发送请求。XMLHttpRequest请求与传统的请求不同,传统的发送请求需要提交表单,或者请求新的网络页面,这都将导致浏览器重新发送请求、重新加载新页面。而XMLHttpRequest发送请求则通过JavaScript代码完成,这就避免了页面的刷新,这也是异步发送请求的核心。
XMLHttpRequest对象包含send()方法用于发送请求。在发送请求之前,应先与请求的URL取得连接,XMLHttpRequest通过open()方法打开与请求URL的连接。下面是使用XMLHttpRequest发送请求的JavaScript代码。


上面的程序用open()方法打开与请求资源的连接,因为本系统采用POST方法发送请求参数,因此在请求里增加了Content-Type请求头,并将该请求头的值设为application/x-www-form-urlencoded,这是为了保证对请求参数采用合适的格式发送。程序中的粗体字代码是发送POST请求的完整过程。
一般而言,使用XMLHttpRequest发送请求的步骤如下:
(1)使用open()方法连接服务器URL。
(2)调用setRequestHeader()方法为请求设置合适的请求头。根据不同的请求,可能需要设置不同的请求头。
(3)指定回调函数。所谓回调函数就是用于检测XMLHttpRequest状态的函数(类似于事件监听器),当XMLHttpRequest的状态发生改变时,该回调函数将被触发而自动执行。
(4)调用send()方法发送请求。
通过上面的程序可以发现,在采用Ajax发送请求时,发送请求比传统Web应用略复杂。传统Web应用发送请求有以下两种形式:
(1)在浏览器的地址栏输入请求地址后按Enter键发送GET请求。
(2)提交表单发送的方式比较简单,基本无须编写任何程序代码,在改为使用Ajax请求后,需要先创建XMLHttpRequest对象,再使用该对象来发送异步请求。
2.使用Servlet生成响应
控制器Servlet将请求获取,调用Service()方法完成信息处理,然后将结果发送到客户端页面,下面是这种用法下的控制器代码。


上面的Servlet与前一个Servlet(1.4.1节)基本相似,只是在Servlet处理用户请求结束后并未直接生成响应,而是输出聊天信息到聊天页面。
3.解析服务器响应
服务器响应生成简单文本,而XMLHttpRequest包含属性responseText,该属性可获取服务器响应生成的文本。在解析服务器响应之前,必须先判断服务器响应是否完成,以及响应是否正确,如生成状态码为404等的错误响应也是没有意义的。为此,XMLHttpRequest提供了以下两个属性。
(1)readyState:判断服务器响应的状态,其中4表明响应完成。
(2)status:判断服务器响应对应的状态码,其中200表明响应正常,404表明资源丢失,500表明内部错误等。关于XMLHttpRequest的详细介绍请参考第2章。
判断完响应状态后,可以使用responseText属性获取服务器响应文本,并将该文本输出到页面显示。下面是解析、处理服务器响应的JavaScript代码。


上面的程序中代码先判断XMLHttpRequest的响应状态,当readyState属性为4时表明响应完成;再判断status是否为200,是则表明服务器生成了正确的响应。
此时,浏览器的页面通过JavaScript与服务器进行的通信基本完成。客户端通过sendRequest()函数向服务器发送请求,服务器通过chatServlet处理用户请求,在服务器响应完成后且服务器生成了正确的响应后,客户端通过DOM操作将服务器响应加载在视图页面上。
如果视图页面使用的是JSP页面,也可以通过Servlet将请求转发到JSP页面生成响应。
整个聊天室HTML页面的代码如下:




通过上面的页面,基于Ajax的聊天室已基本完成。Ajax聊天室的客户端请求在后台异步发送,客户端读取服务器响应也通过JavaScript完成。整个过程不会阻塞用户的聊天,即使服务器的响应变慢,客户端依然可发送请求或者查看原有的聊天记录,无须等待下载页面。图1-7所示为该聊天室页面的运行效果。

聊天页面配置运行

图1-7 聊天室页面的运行效果