作者: 刘长炯 BeanSoft@126.com

在用 AJAX 开发的过程中, 不可避免的会遇到中文问题. 很多原来可以通过表单进行 POST 提交的字符, 到了用 AJAX 实现的时候, 就会出现烦人的乱码和丢特殊字符的现象. 另外服务器端返回值如何解析, 也是一个很烦人的问题. 本文将就个人的一点实践经验作出总结, 并给出一个尽量简单可行, 复用性高的方案. 目的不是替代你喜欢的 AJAX 框架, 而是希望帮助您理解和处理可能遇到的问题.

开始之前: 首先一个问题就是通常 XMLHttpRequest 默认的编码都是UTF-8的, 所以我们建议所有页面, 客户端和服务器端都使用 UTF-8 作为编码.

1. base64 encode 和 decode
    这个方案依赖于 JavaScript 实现的 base64 编码/解码方法, 在客户端发送参数的时候用 base64 进行编码, 服务器端通过 base64 进行解码后还原出原来的字符, 这个解决方案可以满足需要, 但是有个问题就是一是增加了客户端代码量, 还有个大问题就是编码后的内容比原始内容会大很多, 另外如果找到的 base64 JS 算法不够标准的话, 服务器端就无法还原原来的值了. 现在网上有很多种 base64 的 JS 实现代码, 例如如下的一个算法实现:

<HTML>
<HEAD>
Base64
<script language=javascript>
var base64EncodeChars = “ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/”;
var base64DecodeChars = new Array(
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
-1, 0, 1, 2, 3,  4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1);
function base64encode(str) {
var out, i, len;
var c1, c2, c3;
len = str.length;
i = 0;
out = “”;
while (i < len) {
c1 = str.charCodeAt(i++) & 0xff;
if(i == len)
{
out += base64EncodeChars.charAt(c1 » 2);
out += base64EncodeChars.charAt((c1 & 0x3) « 4);
out += “==”;
break;
}
c2 = str.charCodeAt(i++);
if(i == len)
{
out += base64EncodeChars.charAt(c1 » 2);
out += base64EncodeChars.charAt(((c1 & 0x3)« 4) | ((c2 & 0xF0) » 4));
out += base64EncodeChars.charAt((c2 & 0xF) « 2);
out += “=”;
break;
}
c3 = str.charCodeAt(i++);
out += base64EncodeChars.charAt(c1 » 2);
out += base64EncodeChars.charAt(((c1 & 0x3)« 4) | ((c2 & 0xF0) » 4));
out += base64EncodeChars.charAt(((c2 & 0xF) « 2) | ((c3 & 0xC0) »6));
out += base64EncodeChars.charAt(c3 & 0x3F);
}
return out;
}
function base64decode(s
tr) {

var c1, c2, c3, c4;
var i, len, out;
len = str.length;
i = 0;
out = “”;
while (i < len) {
/* c1 */
do {
c1 = base64DecodeChars[str.charCodeAt(i++) & 0xff];
} while(i < len && c1 == -1);
if(c1 == -1)
break;
/* c2 */
do {
c2 = base64DecodeChars[str.charCodeAt(i++) & 0xff];
} while(i < len && c2 == -1);
if(c2 == -1)
break;
out += String.fromCharCode((c1 « 2) | ((c2 & 0x30) » 4));
/* c3 */
do {
c3 = str.charCodeAt(i++) & 0xff;
if(c3 == 61)
return out;
c3 = base64DecodeChars[c3];
} while(i < len && c3 == -1);
if(c3 == -1)
break;
out += String.fromCharCode(((c2 & 0XF) « 4) | ((c3 & 0x3C) » 2));
/* c4 */
do {
c4 = str.charCodeAt(i++) & 0xff;
if(c4 == 61)
return out;
c4 = base64DecodeChars[c4];
} while(i < len && c4 == -1);
if(c4 == -1)
break;
out += String.fromCharCode(((c3 & 0x03) « 6) | c4);
}
return out;
}
function utf16to8(str) {
var out, i, len, c;
out = “”;
len = str.length;
for(i = 0; i < len; i++) {
c = str.charCodeAt(i);
if ((c >= 0x0001) && (c <= 0x007F)) {
out += str.charAt(i);
} else if (c > 0x07FF) {
out += String.fromCharCode(0xE0 | ((c » 12) & 0x0F));
out += String.fromCharCode(0x80 | ((c » 6) & 0x3F));
out += String.fromCharCode(0x80 | ((c » 0) & 0x3F));
} else {
out += String.fromCharCode(0xC0 | ((c » 6) & 0x1F));
out += String.fromCharCode(0x80 | ((c » 0) & 0x3F));
}
}
return out;
}
function utf8to16(str) {
var out, i, len, c;
var char2, char3;
out = “”;
len = str.length;
i = 0;
while (i < len) {
c = str.charCodeAt(i++);
switch(c » 4)
{
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
// 0xxxxxxx
out += str.charAt(i-1);
break;
case 12: case 13:
// 110x xxxx 10xx xxxx
char2 = str.charCodeAt(i++);
out += String.fromCharCode(((c & 0x1F) « 6) | (char2 & 0x3F));
break;
case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx
char2 = str.charCodeAt(i++);
char3 = str.charCodeAt(i++);
out += String.fromCharCode(((c & 0x0F) « 12) |
((char2 & 0x3F) « 6) |
((char3 & 0x3F) « 0));
break;
}
}
return out;
}

function doit() {
var f = document.f
f.output.value = base64encode(utf16to8(f.source.value))
f.decode.value = utf8to16(base64decode(f.output.value))
}
</script>
</HEAD>
<BODY>
<H1>Base64</H1>
<FORM NAME="f">
原码& lt;BR>
<TEXTAREA NAME=”source” ROWS=4 COLS=60 WRAP=”soft”></TEXTAREA>


Base64 encode

<TEXTAREA NAME=”output” ROWS=4 COLS=60 WRAP=”soft”></TEXTAREA>


Base64 decode

<TEXTAREA NAME=”decode” ROWS=4 COLS=60 WRAP=”soft”></TEXTAREA>


<INPUT TYPE=BUTTON VALUE=”转换” ONCLICK=”doit()”>
</FORM>
</BODY>

在每个表单值被提交之前调用 base64encode, 然后在服务器端调用 base64 解码器即可.
在 JSP 中可以通过这样做来实现:
        sun.misc.BASE64Decoder base64decoder = new sun.misc.BASE64Decoder();
        byte[] data = base64decoder.decodeBuffer(request.getParameter(“input”));
        String result = new String(data, “UTF-8”);// 注意建议这里指定字符集来还原到原来的字符串

在这种情况下服务器端返回的字符也可以通过先 base64 编码的方式传递到客户端, 客户端之后调用 JS 形式的解码器即可还原到原来的字符串. 服务器端可以使用 sun.misc.BASE64Encoder (不要用 java.netURLEncoder).


2.使用 JS 自带的 escape() & encodeURI() & encodeURIComponent()

escape() & encodeURI() & encodeURIComponent()这三个函数都可以用来对URI进行encode或过滤特殊字符(#/$&+=?/等)。我的经验是最好用encodeURIComponent()(需要IE 5.5以上,FireFox当然没问题),因为对UTF-8支持比较好,不会遇到中文乱码问题,否则还需要进行编码转换,很麻烦的。使用其它两个函数都会发生丢失特殊字符的问题,例如空格变+号或者空格,引号,&=?等丢失的问题, 至少使用 JSP 作为服务器端的话会发生这种情况, 有兴趣的朋友可以将本文最后的例子代码中的编码部分修改后做个测试.

下面是MSDN上对这三个函数的解释:

escape(charString)

The escape method returns a string value (in Unicode format) that contains the contents of charstring. All spaces, punctuation, accented characters, and any other non-ASCII characters are replaced with %xx encoding, where xx is equivalent to the hexadecimal number representing the character. For example, a space is returned as "%20."

Characters with a value greater than 255 are stored using the %uxxxx format.

Note   The escape method should not be used to encode Uniform Resource Identifiers (URI). Use encodeURI and encodeURIComponent methods instead.

http://msdn.microsoft.com/library/en-us/script56/html/js56jsmthescape.asp

encodeURI(URIString)

The encodeURI method returns an encoded URI. If you pass the result to decodeURI, the original string is returned. The encodeURI method does not encode the following characters: ":", "/", ";", and "?". Use encodeURIComponent to encode these characters.

http://msdn.microsoft.com/library/en-us/script56/html/js56jsmthfencodeuri.asp

encodeURIComponent(encodedURIString)

The encodeURIComponent method returns an encoded URI. If you pass the result to decodeURIComponent, the original string is returned. Because the encodeURIComponent method encodes all characters, be careful if the string represents a path such as /folder1/folder2/default.html. The slash characters will be encoded and will not be valid if sent as a request to a web server. Use the encodeURI method if the string contains more than a single URI component.

http://msdn.microsoft.com/library/en-us/script56/html/js56jsmthencodeuricomponent.asp

3. 使用一个简便的客户端数据解析方案 </p>

</span>最偷懒的办法就是返回一段 HTML 显示出来了. 至于如果是想带一些数据, 解析处理的话, 方案很多, 利用 XML 啊, JSON 啊什么的不一而足. 我这里呢就给出一个相当简便的方案: 使用 JS 内置的 eval 方法来解析. 这个方案是在帮助一个同事想最快的已最短的代码解析返回的对象的多个变量的时候提出的.

服务器端返回一个字符串:
var _dataObject = {
   username : "beansoft",
        age : 24
};

客户端在得到这个字符串后可以通过下面一段代码搞定:
var responseText = xmlhttp.responseText;
eval(responseText);
alert("_dataObject.username=" + _dataObject.username);

好了, 解析出来了!
如果要传递多个变量呢, 就用 var _dataObject1, var _dataObject2…这样就可以了, 客户端就依次是 _dataObject1.username, _dataObject2.username…

等等: 我的变量里写了特殊字符怎么办? 例如我用的字符串是 ‘abc"”, 这时候我不得不抛出杀手锏了, 这就是用 Java 实现的 escape(), unescape() 方法, 其实本例中只需要 escape() 的 Java 版本就可以了(这个方案也帮助另一个同事解决了从JSP端传递的变量含有’号结果导致客户端没法显示的问题):

    public static String escape(String src) {
        int i;
        char j;
        StringBuffer tmp = new StringBuffer();
        tmp.ensureCapacity(src.length() * 6);
        for (i = 0; i < src.length(); i++) {
            j = src.charAt(i);
            if (Character.isDigit(j) || Character.isLowerCase(j)
                    || Character.isUpperCase(j))
                tmp.append(j);
            else if (j < 256) {
                tmp.append("%");
                if (j < 16)
       &#
160;            tmp.append("0");

                tmp.append(Integer.toString(j, 16));
            } else {
                tmp.append("%u");
                tmp.append(Integer.toString(j, 16));
            }
        }
        return tmp.toString();
    }

    public static String unescape(String src) {
        StringBuffer tmp = new StringBuffer();
        tmp.ensureCapacity(src.length());
        int lastPos = 0, pos = 0;
        char ch;
        while (lastPos < src.length()) {
            pos = src.indexOf("%", lastPos);
            if (pos == lastPos) {
                if (src.charAt(pos + 1) == ‘u’) {
                    ch = (char) Integer.parseInt(src
                            .substring(pos + 2, pos + 6), 16);
                    tmp.append(ch);
                    lastPos = pos + 6;
                } else {
                    ch = (char) Integer.parseInt(src
                            .substring(pos + 1, pos + 3), 16);
                    tmp.append(ch);
                    lastPos = pos + 3;
                }
            } else {
                if (pos == -1) {
                    tmp.append(src.substring(lastPos));
                    lastPos = src.length();
                } else {
                    tmp.append(src.substring(lastPos, pos));
                    lastPos = pos;
                }
            }
        }
        return tmp.toString();
    }

这样, 在服务器端的时候可以变成:
<%
String username = "’abc"”";// 其实这个普通字符串转换成 Java 语言中的字符串也有工具可以用的, 例如本人开发的 Native2JavaString, 改日再讲.
%>
var _dataObject = {
   username : "<%=escape(username)%>",
        age : 24
};
客户端呢, 就可以简单的来JS自带的 unescape() 函数来取出原来的字符串:
var responseText = xmlhttp.responseText;
eval(responseText);
alert("_dataObject.username=" + unescape(_dataObject.username));
就是服务器端用 Java 写的 escape(), 客户端呢就用 JS 自带的 unescape().

4. 实例代码
好了, 说了这么多, 就推出个人的解决方案吧. 简单的讲就是我写了一个脚本对象 AjaxFormer, 使用的是encodeURIComponent() 来自动的将原来的 POST/GET 方式的提交代码自动的转换成 AJAX 的方式, 而且仍然保持原来的 GET/POST 模式不便.

/**
* @constructor
* This is a ajax form helper class.
*
* @param form – the document form
* @param resultDivId – the result div id
*/
function AjaxFormer (form, resultDivId);

构造器的第一个参数是个 form 对象, 第二个是个可选的结果 DIV 对象, 也就是说你可以指定服务器端返回的 HTML 代码显示的地方, 如果保持为空的话, 那么返回的 HTML 会被附加到文档的末尾. 本对象有一个名字为 ajaxSubmitForm() 的方法来自动的遍历所有表单元素, 然后将结果拼成一个字符串, 最后根据原来的表单的提交方式(get/post)来自动再客户端用 AJAX 模拟提交这个表单到原来的表单的 action 属性所指定的页面中去.

用法示例:
</span>

</span>

AJAX Form Submit Test
<script src=’ajax_common.js’></script>

</head>

</span>

AJAX Form Submit Test


Fill the form and then click submit
<form method="POST" id="form1" name="form1"
action="form_action.jsp"
onSubmit="former.ajaxSubmitForm();return false;">
   

</span>
    text:
    checkbox:
    radio:
    select:
   
    </p>
</form>


</body>

</html> </p>

</span>红色的字体就是您从一个非 AJAX 的表单提交改变成一个 AJAX 的表单提交所需要做的工作, 看上去够简单吧?

运行时的效果如图所示:
http://www.blogjava.net/images/blogjava_net/beansoft/18680/o_ajaxFormer.png

下载本文的源码: 

AJAXFormer.zip4KB

将源码解压缩到JSP服务器的任意目录下即可, 例如 $TOMCAT_HOMEwebappsROOT 下, 然后在浏览器里键入:
http://localhost:8080/ajax_form_submit.htm
.
本文参考了网上多篇文章并综合了自己的实践, 最后的源码是本人原创的作品, 如果使用转载请注明出处. 在此就不再一一指出并道谢了.

作者: beansoft@126.com 2006.12.25

转载请注明:WebLogic Android 博客 » JSP 中 AJAX 的表单提交中文问题的简单解决方案[整理]