說(shuō)到AJAX,每個(gè)人都不會(huì)陌生,畢竟這兩年它太流行了。然而,真正哪些地方需要AJAX,并不是每個(gè)人都能夠把握得很好。使用AJAX可以開(kāi)發(fā)豪華的基于瀏覽器的富客戶端界面,然而其開(kāi)發(fā)量的龐大和調(diào)試的艱難,讓每一個(gè)程序員如同生活在地獄中一般。
我認(rèn)為,真正需要AJAX的不外乎兩種情況:
1、用戶不希望他關(guān)注的信息離開(kāi)他的視線的時(shí)候。比如填寫(xiě)某些表單的時(shí)候,有時(shí)候辛辛苦苦填寫(xiě)的東西,一點(diǎn)提交按鈕,全沒(méi)了,如果某個(gè)字段驗(yàn)證失敗,則所有的東西都要從頭再填,著實(shí)讓人郁悶。雖然設(shè)計(jì)較好的網(wǎng)站可以保留用戶填寫(xiě)的信息,但是在提交后頁(yè)面刷新的一瞬間,用戶仍然有一種不安感。
2、用戶不希望一點(diǎn)小的更新就刷新整個(gè)龐大的頁(yè)面的時(shí)候。比如用戶回復(fù)一篇很長(zhǎng)且充滿圖片的文章,雖然回復(fù)的內(nèi)容只有幾個(gè)字,但是卻要等待頁(yè)面漫長(zhǎng)的刷新,也會(huì)造成不好的用戶體驗(yàn)。
如果單單只是為了實(shí)現(xiàn)絢麗的效果而使用AJAX,我個(gè)人認(rèn)為不可取。
根據(jù)以上的總結(jié),我這里想實(shí)現(xiàn)這樣一個(gè)注冊(cè)用戶的功能,讓用戶在提交信息的時(shí)候不需要刷新頁(yè)面,所有的字段驗(yàn)證全部發(fā)回服務(wù)器端進(jìn)行,驗(yàn)證的錯(cuò)誤信息再顯示到表單頁(yè)面,整個(gè)過(guò)程不刷新頁(yè)面,知道注冊(cè)成功后跳轉(zhuǎn)到首頁(yè),如下圖:
1、供用戶填寫(xiě)的表單

2、用戶填寫(xiě)信息后,表單變?yōu)椴豢删庉?,并提示?shù)據(jù)正在提交

3、如果驗(yàn)證失敗,顯示錯(cuò)誤信息,同時(shí)表單變?yōu)榭捎米層脩粜薷?br />
4、注冊(cè)成功后,提示注冊(cè)成功,然后跳轉(zhuǎn)到首頁(yè)

看似簡(jiǎn)單的功能,我卻足足花了兩天時(shí)間才搞定,所以形容為在地獄里漫步。下面,大家會(huì)看到我的設(shè)計(jì)思路和遇到的各種問(wèn)題。
要做AJAX開(kāi)發(fā),首先當(dāng)然少不了挑選一個(gè)AJAX框架。我最喜歡的是Prototype,因?yàn)槲矣憛拸?fù)雜的功能。在SpringSide中集成有Prototype,我們只需要在jsp文件中加入如下代碼,就可以使用了:
<
script?
src
="scripts/prototype.js"
></
script
>
事實(shí)上,我只使用了Prototype的一個(gè)函數(shù),它就是Ajax.Request(),它簡(jiǎn)化了我們繁瑣的實(shí)例化XHR、監(jiān)控請(qǐng)求狀態(tài)的過(guò)程,語(yǔ)法如下:
var
?ajax?
=
?
new
?Ajax.Request(url,?
{method:?
"
get
"
?,?onComplete:onResponse}
?);
method為方法類型,如get,post等;onComplete為回調(diào)函數(shù),通常在這個(gè)函數(shù)中完成對(duì)相應(yīng)數(shù)據(jù)的解析和顯示。
看似水落石出,只要在用戶點(diǎn)擊注冊(cè)按鈕的時(shí)候調(diào)用這個(gè)函數(shù)就可以做到異步提交數(shù)據(jù)了。
問(wèn)題一、如何將表單中的數(shù)據(jù)發(fā)回服務(wù)器?
我們都知道,當(dāng)我們提交整個(gè)網(wǎng)頁(yè)的時(shí)候,其表單中的數(shù)據(jù)也一并POST過(guò)去了,基本上無(wú)需我們操心;而AJAX不然,AJAX向服務(wù)器提交請(qǐng)求的時(shí)候,除了url,其它屁信息都沒(méi)有。沒(méi)有辦法,我們只有自己取出表單中的數(shù)據(jù),把它添加到url參數(shù)中,然后傳遞給服務(wù)器。因此,當(dāng)提交按鈕被點(diǎn)擊時(shí),我的處理函數(shù)是這樣的:
function
?onSubmit()
{

????
var
?url?
=
?
"
RegUser.do?method=submit
"
;
????
//
將表單數(shù)據(jù)添加到url中以便于使用GET傳遞到服務(wù)器
????
var
?inputs?
=
?userForm.all.tags(
"
input
"
);

????
for
(
var
?i
=
0
;?i?
<
?inputs.length;?i
++
)
{
????????url?
=
?url?
+
?
"
&
"
?
+
?inputs[i].name?
+
?
"
=
"
?
+
?inputs[i].value;
????????
//
設(shè)置表單為不可用狀態(tài)
????????inputs[i].disabled?
=
?
"
true
"
;
????}
????
//
提示用戶正在提交數(shù)據(jù)
????$(
"
doing
"
).style.pixelTop?
=
?document.body.scrollTop?
+
?
230
;
????$(
"
doing
"
).style.left?
=
?
250
;
????$(
"
doing
"
).style.display
=
"
block
"
;
????
????
//
使用AJAX將數(shù)據(jù)傳遞到服務(wù)器,并接受服務(wù)器的回應(yīng)
????
var
?ajax?
=
?
new
?Ajax.Request(url,?
{method:?
"
get
"
?,?onComplete:onResponse}
?);
}
?
乍一看來(lái),解決這個(gè)問(wèn)題似乎并不復(fù)雜,但不知大家想過(guò)沒(méi)有,如果用戶輸入非法字符怎么處理。在url中,有幾個(gè)字符是會(huì)被引起錯(cuò)誤的,比如"@"和"#","@"會(huì)讓服務(wù)器只把"@"后面的字符串當(dāng)成有效url地址,"#"代表一個(gè)網(wǎng)頁(yè)中的錨點(diǎn)。也有可能還有更多的非法字符,我們暫時(shí)還沒(méi)有發(fā)現(xiàn)。我曾經(jīng)想過(guò)使用JavaScript的escape()來(lái)將表單中的字符編碼,但是又會(huì)引起中文無(wú)法傳遞到服務(wù)器。唉,看來(lái)除非在客戶端使用JavaScript代碼來(lái)過(guò)濾掉這些字符,是在也想不出其它的辦法。
問(wèn)題二、服務(wù)器返回什么數(shù)據(jù)給AJAX對(duì)象?
我們通過(guò)AJAX把數(shù)據(jù)異步傳遞到服務(wù)器,等服務(wù)器驗(yàn)證完畢后,服務(wù)器給我們回復(fù)什么格式的數(shù)據(jù)呢?是XML?普通文本?JSON?還是其它。XML我首先排除,因?yàn)榻馕鏊墓ぷ髁刻罅恕.?dāng)前,JSON最是流行。但是我更加懶惰,我直接返回有效的JavaScript代碼,這樣,我在AJAX的onComplete時(shí),只需要一行代碼,如下:
function
??onResponse(request)??
{
????eval(request.responseText);
}
問(wèn)題三、AJAX讓Validator框架走開(kāi)?
在Struts中,有一個(gè)驗(yàn)證框架Validator,它可以很方便的完成對(duì)ActionForm的驗(yàn)證。但是一旦我們使用AJAX,Validator就派不上任何用場(chǎng),因?yàn)橹挥性趈sp文件中使用Struts的<html:form>系列標(biāo)簽,才能讓Struts表我們的表單數(shù)據(jù)自動(dòng)封裝到ActionForm中,但是前文已經(jīng)提過(guò),我們的表單數(shù)據(jù)是通過(guò)url參數(shù)傳遞的,所以除非自己擴(kuò)展Struts,否則我們跟Validator無(wú)緣。
于是,所有的驗(yàn)證代碼我們必須得在服務(wù)器端自己編寫(xiě),即要考慮周全,又要防止出錯(cuò)。幸好SpringSide提供的HibernateEntityDao<T>讓我們?cè)隍?yàn)證用戶名和昵稱是否重復(fù)時(shí)省了一大把勁。我的服務(wù)器端代碼如下:
//?????????獲取用戶提交的數(shù)據(jù)并驗(yàn)證?????
????public?ActionForward?submit(ActionMapping?mapping,?ActionForm?form,
????????????HttpServletRequest?request,?HttpServletResponse?response)

????????????throws?Exception?
{
????????boolean?success?=?true;
????????String?result?=?"";
????????User?user?=?new?User();

????????//?驗(yàn)證用戶名????????
????????String?name?=?new?String(request.getParameter("name").getBytes(
????????????????"ISO-8859-1"),?"GB2312");
????????user.setName(name);

????????if?(name?==?null?||?name.equals(""))?
{
????????????success?=?false;
????????????result?+=?"name_err.innerHTML='用戶名不能為空';";

????????}?else?if?(!name.matches("^[A-Za-z0-9_]*$"))?
{
????????????success?=?false;
????????????result?+=?"name_err.innerHTML='用戶名只能包含字母、數(shù)字和下劃線';";

????????}?else?if?(name.length()?>?20)?
{
????????????success?=?false;
????????????result?+=?"name_err.innerHTML='用戶名不能超過(guò)20個(gè)字符';";
????????}
????????//?判斷用戶名是否重復(fù)

????????else?if?(userManager.isNotUnique(user,?"name"))?
{
????????????success?=?false;
????????????result?+=?"name_err.innerHTML='該用戶名已經(jīng)被注冊(cè)';";

????????}else
{
????????????result?+=?"name_err.innerHTML='';";
????????}

????????//?驗(yàn)證昵稱
????????String?monicker?=?new?String(request.getParameter("monicker").getBytes(
????????????????"ISO-8859-1"),?"GB2312");
????????user.setMonicker(monicker);

????????if?(monicker?==?null?||?monicker.equals(""))?
{
????????????success?=?false;
????????????result?+=?"monicker_err.innerHTML='昵稱不能為空';";
????????}?else?if?(monicker.matches("^.*[~!@#$%^&*()-=+<>?/'\";:]+.*$"))?{
????????????success?=?false;
????????????result?+=?"monicker_err.innerHTML='昵稱不能包含特殊字符';";

????????}?else?if?(monicker.length()?>?15)?
{
????????????success?=?false;
????????????result?+=?"monicker_err.innerHTML='昵稱不能超過(guò)15個(gè)字符';";
????????}//?判斷昵稱是否重復(fù)

????????else?if?(userManager.isNotUnique(user,?"monicker"))?
{
????????????success?=?false;
????????????result?+=?"monicker_err.innerHTML='該用昵稱已經(jīng)被使用';";

????????}else
{
????????????result?+=?"monicker_err.innerHTML='';";
????????}

????????//?驗(yàn)證兩次輸入的密碼是否匹配
????????String?password?=?new?String(request.getParameter("password").getBytes(
????????????????"ISO-8859-1"),?"GB2312");
????????user.setPassword(password);
????????String?password_again?=?new?String(request.getParameter(
????????????????"password_again").getBytes("ISO-8859-1"),?"GB2312");

????????if?(password?==?null?||?password.equals(""))?
{
????????????success?=?false;
????????????result?+=?"password_err.innerHTML='密碼不能為空';";

????????}?else?if?(password_again?==?null?||?password_again.equals(""))?
{
????????????success?=?false;
????????????result?+=?"password_err.innerHTML='確認(rèn)密碼不能為空';";

????????}?else?if?(!password.equals(password_again))?
{
????????????success?=?false;
????????????result?+=?"password_err.innerHTML='兩次輸入的密碼不匹配';";

????????}else
{
????????????result?+=?"password_err.innerHTML='';";
????????}

????????//?驗(yàn)證密碼問(wèn)題和問(wèn)題答案,規(guī)則和昵稱相同
????????String?question?=?new?String(request.getParameter("question").getBytes(
????????????????"ISO-8859-1"),?"GB2312");
????????user.setQuestion(question);

????????if?(question?==?null?||?question.equals(""))?
{
????????????success?=?false;
????????????result?+=?"question_err.innerHTML='問(wèn)題不能為空';";
????????}?else?if?(question.matches("^.*[~!@#$%^&*()-=+<>?/'\";:]+.*$"))?{
????????????success?=?false;
????????????result?+=?"question_err.innerHTML='問(wèn)題不能包含特殊字符';";

????????}?else?if?(question.length()?>?15)?
{
????????????success?=?false;
????????????result?+=?"question_err.innerHTML='問(wèn)題不能超過(guò)15個(gè)字符';";

????????}else
{
????????????result?+=?"question_err.innerHTML='';";
????????}

????????String?answer?=?new?String(request.getParameter("answer").getBytes(
????????????????"ISO-8859-1"),?"GB2312");
????????user.setQuestion(question);

????????if?(answer?==?null?||?answer.equals(""))?
{
????????????success?=?false;
????????????result?+=?"answer_err.innerHTML='答案不能為空';";
????????}?else?if?(answer.matches("^.*[~!@#$%^&*()-=+<>?/'\";:]+.*$"))?{
????????????success?=?false;
????????????result?+=?"answer_err.innerHTML='答案不能包含特殊字符';";

????????}?else?if?(answer.length()?>?15)?
{
????????????success?=?false;
????????????result?+=?"answer_err.innerHTML='答案不能超過(guò)15個(gè)字符';";

????????}else
{
????????????result?+=?"answer_err.innerHTML='';";
????????}

????????//?驗(yàn)證email
????????String?email?=?new?String(request.getParameter("email").getBytes(
????????????????"ISO-8859-1"),?"GB2312");
????????user.setEmail(email);

????????if?(email?==?null?||?email.equals(""))?
{
????????????success?=?false;
????????????result?+=?"email_err.innerHTML='Email不能為空';";

????????}?else?if?(!email.matches("^[a-zA-Z0-9]*@[a-zA-Z0-9]*$"))?
{
????????????success?=?false;
????????????result?+=?"email_err.innerHTML='不是有效的電子郵箱';";

????????}?else?if?(email.length()?>?40)?
{
????????????success?=?false;
????????????result?+=?"email_err.innerHTML='Email不能超過(guò)40個(gè)字符';";

????????}else
{
????????????result?+=?"email_err.innerHTML='';";
????????}

????????//?驗(yàn)證QQ號(hào)碼
????????String?qq?=?new?String(request.getParameter("qq")
????????????????.getBytes("ISO-8859-1"),?"GB2312");
????????user.setQq(qq);

????????if?(qq?==?null?||?qq.equals(""))?
{
????????????success?=?false;
????????????result?+=?"qq_err.innerHTML='QQ號(hào)碼不能為空';";

????????}?else?if?(!qq.matches("^\\d*$"))?
{
????????????success?=?false;
????????????result?+=?"qq_err.innerHTML='不是有效的QQ號(hào)碼';";

????????}?else?if?(qq.length()?>?12)?
{
????????????success?=?false;
????????????result?+=?"qq_err.innerHTML='QQ號(hào)碼不能超過(guò)12位';";

????????}?else?if?(qq.length()?<?5)?
{
????????????result?+=?"qq_err.innerHTML='QQ號(hào)碼不能少于5位';";

????????}else
{
????????????result?+=?"qq_err.innerHTML='';";
????????}

????????//?驗(yàn)證驗(yàn)證碼
????????String?validateImage?=?new?String(request.getParameter("validateImage")
????????????????.getBytes("ISO-8859-1"),?"GB2312");
????????if?(validateImage?==?null
????????????????||?validateImage.equals("")
????????????????||?!validateImage.equals(request.getSession().getAttribute(

????????????????????????"validateString")))?
{
????????????success?=?false;
????????????result?+=?"validateImage_err.innerHTML='驗(yàn)證碼輸入錯(cuò)誤。如看不清,點(diǎn)擊圖片更換';";

????????}else
{
????????????result?+=?"validateImage_err.innerHTML='';";
????????}
????????
????????//如果驗(yàn)證不成功,則調(diào)用JavaScript的failed()函數(shù),否則,調(diào)用sucess();

????????if(success?==?false)
{
????????????result?+=?"failed();";
????????????response.setCharacterEncoding("GB2312");
????????????response.getOutputStream().println(result);
????????????response.flushBuffer();

????????}else
{
????????????//如果驗(yàn)證成功,把數(shù)據(jù)寫(xiě)入數(shù)據(jù)庫(kù)中,要防止重復(fù)提交

????????????if(this.isTokenValid(request))
{
????????????????userManager.save(user);
????????????????this.resetToken(request);
????????????}
????????????response.setCharacterEncoding("GB2312");
????????????response.getOutputStream().println("success();");
????????????response.flushBuffer();
????????}
????????return?null;
????}客戶端的failed()和success()函數(shù)如下:

function?failed()
{
????//掩藏提示信息
????$("doing").style.display="none";
????//設(shè)置表單為可用狀態(tài)
????var?inputs?=?userForm.all.tags("input");

????for(var?i=0;?i?<?inputs.length;?i++)
{
????????inputs[i].disabled?=?"";
????}
}


function?success()
{
????$("doing").style.display="block";
????$("doing").style.color="#0000FF";
????$("doing").innerHTML?=?"用戶注冊(cè)成功,將跳轉(zhuǎn)到首頁(yè)!";
????//4秒鐘跳到首頁(yè)
????setTimeout("location.href='welcome.do';",4000);
} 問(wèn)題四、中文亂碼問(wèn)題如何解決?我想每個(gè)人在使用AJAX的時(shí)候肯定都遇到過(guò)中文亂碼的問(wèn)題,我也不例外,這個(gè)問(wèn)題困擾我的時(shí)間也不短,后來(lái)我總算時(shí)把它搞清楚了:AJAX使用的是另外一個(gè)線程,所以它的字符編碼是和頁(yè)面無(wú)關(guān)的,也就是說(shuō),它總是用GB2312編碼向服務(wù)器發(fā)送數(shù)據(jù),并且總是把接受到的數(shù)據(jù)當(dāng)GB2312來(lái)理解,這是由我們操作系統(tǒng)決定的,我們大陸的操作系統(tǒng)默認(rèn)編碼都應(yīng)該是GB2312吧。因此,在接受數(shù)據(jù)的時(shí)候,我們少不了:
String?name?=?new?String(request.getParameter("name").getBytes(
????????????????"ISO-8859-1"),?"GB2312");而發(fā)送數(shù)據(jù)的時(shí)候,也少不了:
response.setCharacterEncoding("GB2312");
????????????response.getOutputStream().println(result);
????????????response.flushBuffer();除此之外,還有瀏覽器之間對(duì)象不兼容的問(wèn)題,可見(jiàn)寫(xiě)一個(gè)AJAX應(yīng)用到處都是陷阱。
從上面大家可以看出,對(duì)于用戶注冊(cè),我全部使用的/RegUser.do來(lái)進(jìn)行處理,它繼承自SpringSide的StrutsAction,是DispatherAction的子類。它的配置如下:
struts-config.xml的action-mappings節(jié)中:
<action?path="/RegUser"?scope="request"?parameter="method">
????????????????<forward?name="agree"?path="/RegUser_Agree.jsp"/>
????????????????<forward?name="apply"?path="/RegUser_Apply.jsp"/>
????????????</action>action-servlet.xml:
<?xml?version="1.0"?encoding="UTF-8"?>
<!DOCTYPE?beans?PUBLIC?"-//SPRING//DTD?BEAN?2.0//EN"?"http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans?default-autowire="byName"?default-lazy-init="true">
????<!--?按模塊導(dǎo)入Spring?Action?Config-->
????<import?resource="modules/spring-config-admin.xml"/>

????<!--?簡(jiǎn)單應(yīng)用直接在此定義Action
????????<bean?name="/user"?class="org.springside.helloworld.web.UserAction"/>
????????-->
????<bean?name="/welcome"?class="com.xkland.action.WelcomeAction"/>
????<bean?name="/RegUser"?class="com.xkland.action.RegUserAction"/>
</beans>而這個(gè)com.xkland.action.RegUserAction的完整代碼如下,希望大家多提意見(jiàn):
package?com.xkland.action;

import?javax.servlet.http.HttpServletRequest;
import?javax.servlet.http.HttpServletResponse;

import?org.apache.struts.action.ActionForm;
import?org.apache.struts.action.ActionMapping;
import?org.apache.struts.action.ActionForward;
import?org.springside.core.web.StrutsAction;

import?com.xkland.manager.UserManager;
import?com.xkland.util.ImageUtil;
import?java.awt.image.BufferedImage;
import?javax.imageio.ImageIO;
import?com.xkland.domain.User;


public?class?RegUserAction?extends?StrutsAction?
{
????private?ImageUtil?imageUtil;

????private?UserManager?userManager;


????public?void?setUserManager(UserManager?userManager)?
{
????????this.userManager?=?userManager;
????}


????public?void?setImageUtil(ImageUtil?imageUtil)?
{
????????this.imageUtil?=?imageUtil;
????}

????//?重定向到會(huì)員注冊(cè)協(xié)議頁(yè)面??
????public?ActionForward?agree(ActionMapping?mapping,?ActionForm?form,

????????????HttpServletRequest?request,?HttpServletResponse?response)?
{
????????return?mapping.findForward("agree");
????}

????//?重定向到填寫(xiě)表單頁(yè)面??
????public?ActionForward?apply(ActionMapping?mapping,?ActionForm?form,

????????????HttpServletRequest?request,?HttpServletResponse?response)?
{
????????//?使用Token防止重復(fù)提交
????????saveToken(request);
????????return?mapping.findForward("apply");
????}

????//?構(gòu)造驗(yàn)證圖片??????
????public?ActionForward?createValidateImage(ActionMapping?mapping,
????????????ActionForm?form,?HttpServletRequest?request,

????????????HttpServletResponse?response)?
{
????????BufferedImage?image?=?imageUtil.createValidateImage(request
????????????????.getSession());
????????response.setContentType("image/jpeg");

????????try?
{
????????????ImageIO.write(image,?"jpeg",?response.getOutputStream());
????????????response.flushBuffer();

????????}?catch?(Exception?e)?
{

????????}

????????return?null;
????}

????//獲取用戶提交的數(shù)據(jù)并驗(yàn)證
????public?ActionForward?submit(ActionMapping?mapping,?ActionForm?form,
????????????HttpServletRequest?request,?HttpServletResponse?response)

????????????throws?Exception?
{
//上面已貼代碼,此處省略
????????????}
}