京山游侠

专注技术,拒绝扯淡
posts - 50, comments - 868, trackbacks - 0, articles - 0
  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

说到AJAX,每个人都不会陌生,毕竟这两年它太流行了。然而,真正哪些地方需要AJAX,并不是每个人都能够把握得很好。使用AJAX可以开发豪华的基于浏览器的富客户端界面,然而其开发量的庞大和调试的艰难,让每一个程序员如同生活在地狱中一般。

我认为,真正需要AJAX的不外乎两种情况:

1、用户不希望他关注的信息离开他的视线的时候。比如填写某些表单的时候,有时候辛辛苦苦填写的东西,一点提交按钮,全没了,如果某个字段验证失败,则所有的东西都要从头再填,着实让人郁闷。虽然设计较好的网站可以保留用户填写的信息,但是在提交后页面刷新的一瞬间,用户仍然有一种不安感。
2、用户不希望一点小的更新就刷新整个庞大的页面的时候。比如用户回复一篇很长且充满图片的文章,虽然回复的内容只有几个字,但是却要等待页面漫长的刷新,也会造成不好的用户体验。

如果单单只是为了实现绚丽的效果而使用AJAX,我个人认为不可取。

根据以上的总结,我这里想实现这样一个注册用户的功能,让用户在提交信息的时候不需要刷新页面,所有的字段验证全部发回服务器端进行,验证的错误信息再显示到表单页面,整个过程不刷新页面,知道注册成功后跳转到首页,如下图:

1、供用户填写的表单
41.JPG

2、用户填写信息后,表单变为不可编辑,并提示数据正在提交
42.JPG

3、如果验证失败,显示错误信息,同时表单变为可用让用户修改
43.JPG

4、注册成功后,提示注册成功,然后跳转到首页
44.JPG

看似简单的功能,我却足足花了两天时间才搞定,所以形容为在地狱里漫步。下面,大家会看到我的设计思路和遇到的各种问题。

要做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 {
//上面已贴代码,此处省略
            }

}

评论

# re: SpringSide开发实战(六):AJAX,在地狱中漫步  回复  更多评论   

2007-03-02 08:12 by dragonsz1
居然是沙发,

一早上班就看到沫沫的好文,实在太高兴了

果然关注沫沫的BLOG没错~~~

支持,帮助很大啊~~~

# re: SpringSide开发实战(六):AJAX,在地狱中漫步  回复  更多评论   

2007-03-02 09:28 by 坏男孩
这篇比较全面标记一下

# re: SpringSide开发实战(六):AJAX,在地狱中漫步  回复  更多评论   

2007-03-02 10:00 by anonymity
赞成这个
"如果单单只是为了实现绚丽的效果而使用AJAX,我个人认为不可取。"
这样在action中校验数据不敢恭维,那怕是demo。

# re: SpringSide开发实战(六):AJAX,在地狱中漫步  回复  更多评论   

2007-03-02 11:14 by chinajj
"我曾经想过使用JavaScript的escape()来将表单中的字符编码,但是又会引起中文无法传递到服务器。唉,看来除非在客户端使用JavaScript代码来过滤掉这些字符,是在也想不出其它的办法。"
-----------------
可以在服务器端用java代码模拟javascript的unescape()函数(网上有类似的例子)来接受使用JavaScript的escape()加密过的字符串。这样不会有中文问题了吧。

# re: SpringSide开发实战(六):AJAX,在地狱中漫步[未登录]  回复  更多评论   

2007-03-02 12:22 by thinkbase.net
我们都知道,当我们提交整个网页的时候,其表单中的数据也一并POST过去了,基本上无需我们操心;而AJAX不然,AJAX向服务器提交请求的时候,除了url,其它屁信息都没有
-----------------------
这里有误导之嫌, AJAX也是一样可以 POST 的啊

# re: SpringSide开发实战(六):AJAX,在地狱中漫步  回复  更多评论   

2007-03-02 12:39 by boolean
提两点:
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 " ;
}
可以用prototype的Form对象的两个方法做
Form.disable
Form.serialize

# re: SpringSide开发实战(六):AJAX,在地狱中漫步  回复  更多评论   

2007-03-03 10:57 by 海边沫沫
@boolean
谢谢提示,看来得好好研究Prototype才行啊,因为它的确可以简化很多我们的工作。

# re: SpringSide开发实战(六):AJAX,在地狱中漫步  回复  更多评论   

2007-03-03 10:58 by 海边沫沫
@thinkbase.net
AJAX可以POST,但是提交的内容依然不包含表单数据

# re: SpringSide开发实战(六):AJAX,在地狱中漫步  回复  更多评论   

2007-03-03 11:05 by 海边沫沫
@anonymity
不错,应该进行重构,把所有的验证代码提取出来,另外组成一个Util类

# re: SpringSide开发实战(六):AJAX,在地狱中漫步  回复  更多评论   

2007-03-08 13:11 by penny
ajax 可以提交 xml 数据流,楼主想在url 后面加提交的参数据的做法有些不解。

# re: SpringSide开发实战(六):AJAX,在地狱中漫步  回复  更多评论   

2007-03-09 22:56 by billbai
var url = " RegUser.do?method=submit " ;
var myAjax = new Ajax.Request(url,{method: 'get', parameters: Form.serialize(signinForm), onComplete: showResponse,onFailure: reportError});

通过这种方式可以获得表单所有对象;

# re: SpringSide开发实战(六):AJAX,在地狱中漫步  回复  更多评论   

2007-03-11 18:12 by 京山游侠
@billbai
谢谢

# re: SpringSide开发实战(六):AJAX,在地狱中漫步  回复  更多评论   

2007-07-30 16:22 by 小白之家
还是请momo给贴出代码

# re: SpringSide开发实战(六):AJAX,在地狱中漫步  回复  更多评论   

2007-12-12 10:31 by yxw
强悍!衷心希望您把完整的代码发上了!我很想学习啊!

# re: SpringSide开发实战(六):AJAX,在地狱中漫步  回复  更多评论   

2008-08-06 16:44 by ONE
实际上prototype是可以将FORM表单中的元素序列化的

只有注册用户登录后才能发表评论。


网站导航: