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

2、用戶填寫信息后,表單變為不可編輯,并提示數據正在提交

3、如果驗證失敗,顯示錯誤信息,同時表單變為可用讓用戶修改

4、注冊成功后,提示注冊成功,然后跳轉到首頁

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

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

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

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

????????//?驗證用戶名????????
????????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='用戶名只能包含字母、數字和下劃線';";

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

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

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

????????//?驗證昵稱
????????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='昵稱不能超過15個字符';";
????????}//?判斷昵稱是否重復

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

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

????????//?驗證兩次輸入的密碼是否匹配
????????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='確認密碼不能為空';";

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

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

????????//?驗證密碼問題和問題答案,規則和昵稱相同
????????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='問題不能為空';";
????????}?else?if?(question.matches("^.*[~!@#$%^&*()-=+<>?/'\";:]+.*$"))?{
????????????success?=?false;
????????????result?+=?"question_err.innerHTML='問題不能包含特殊字符';";

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

????????}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='答案不能超過15個字符';";

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

????????//?驗證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不能超過40個字符';";

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

????????//?驗證QQ號碼
????????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號碼不能為空';";

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

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

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

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

????????//?驗證驗證碼
????????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='驗證碼輸入錯誤。如看不清,點擊圖片更換';";

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

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

????????}else
{
????????????//如果驗證成功,把數據寫入數據庫中,要防止重復提交

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

function?failed()
{
????//掩藏提示信息
????$("doing").style.display="none";
????//設置表單為可用狀態
????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?=?"用戶注冊成功,將跳轉到首頁!";
????//4秒鐘跳到首頁
????setTimeout("location.href='welcome.do';",4000);
} 問題四、中文亂碼問題如何解決?我想每個人在使用AJAX的時候肯定都遇到過中文亂碼的問題,我也不例外,這個問題困擾我的時間也不短,后來我總算時把它搞清楚了:AJAX使用的是另外一個線程,所以它的字符編碼是和頁面無關的,也就是說,它總是用GB2312編碼向服務器發送數據,并且總是把接受到的數據當GB2312來理解,這是由我們操作系統決定的,我們大陸的操作系統默認編碼都應該是GB2312吧。因此,在接受數據的時候,我們少不了:
String?name?=?new?String(request.getParameter("name").getBytes(
????????????????"ISO-8859-1"),?"GB2312");而發送數據的時候,也少不了:
response.setCharacterEncoding("GB2312");
????????????response.getOutputStream().println(result);
????????????response.flushBuffer();除此之外,還有瀏覽器之間對象不兼容的問題,可見寫一個AJAX應用到處都是陷阱。
從上面大家可以看出,對于用戶注冊,我全部使用的/RegUser.do來進行處理,它繼承自SpringSide的StrutsAction,是DispatherAction的子類。它的配置如下:
struts-config.xml的action-mappings節中:
<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">
????<!--?按模塊導入Spring?Action?Config-->
????<import?resource="modules/spring-config-admin.xml"/>

????<!--?簡單應用直接在此定義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>而這個com.xkland.action.RegUserAction的完整代碼如下,希望大家多提意見:
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;
????}

????//?重定向到會員注冊協議頁面??
????public?ActionForward?agree(ActionMapping?mapping,?ActionForm?form,

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

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

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

????//?構造驗證圖片??????
????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;
????}

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

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