JSP 中 AJAX 的表单提交中文问题的简单解决方案 - GBK 版本(引用)

JSP 中 AJAX 的表单提交中文问题的简单解决方案 - GBK 版本(引用)

作者: beansoft@126.com 2007.01.03

下载本文源码: ajaxform_gbk.zip 6KB

转载请注明原创作者, 尊重他人劳动成果.

更新:
2007-01-03
修复了 AJAXFormer 中第二个参数 resultDiv 处理不当的问题;
增加了从服务器端返回脚本并加以执行的功能;
增加了表单提交后网络出错的错误显示功能.
这些新功能都已经放在示例页面中了.

测试通过: Resin 3.0.18, Tomcat 5.0.30, 5.5.20; 浏览器: IE 6/Firefox 2.0.

上一篇文章 JSP 中 AJAX 的表单提交中文问题的简单解决方案 主要是针对 UTF-8 版本的进行处理的, 鉴于中国大陆地区大部分还是用 GBK 编码写 JSP, 因此本文就针对 GBK 的实践结果进行介绍.

有朋友提到 当AJAX遭遇GBK的尴尬 里说当 AJAX 使用 GBK 编码后, 表单提交将出现乱码. 如前文所述, 只要全部采用 UTF-8 编码, 是没有任何问题的. 那么都用 GBK 呢?

首先要讲的是我们的文章还是一样的原则: 尽可能少的改动原来的代码来解决中文乱码问题. 所以本文的示例没有用过滤器等方法.

那么使用 GBK 编码到底有没有乱码问题呢?

第一个关键点就是 AJAX 的表单提交代码必须正确的按照 HTTP 规范实现, 即要保持原来的 GET/POST 方式不变, 也要保持里面的内容和浏览器提交的内容一摸一样. 以下内容摘自我编写的内部培训教材:

----------------- 引用开始 -----------------

首先必须要介绍一下 HTTP 协议和 GET, POST 的工作方式.

当用户在Web浏览器地址栏中输入一个带有http://前缀的URL并按下Enter后,或者在Web页面中某个以http://开头的超链接上单击鼠标,HTTP事务处理的第一个阶段--建立连接阶段就开始了.HTTP的默认端口是80.
随着连接的建立,HTTP就进入了客户向服务器发送请求的阶段.客户向服务器发送的请求是一个有特定格式的ASCII消息,其语法规则为:

6KB
< Method > < URL > < HTTP Version > <\n>
{ <Header>:<Value> <\n>}*
<\n>
{ Entity Body }

请求消息的顶端是请求行,用于指定方法,URL和HTTP协议的版本,请求行的最后是回车换行.方法有GET,POST,HEAD,PUT,DELETE等.
在请求行之后是若干个报头(Header)行.每个报头行都是由一个报头和一个取值构成的二元对,报头和取值之间以":"分隔;报头行的最后是回车换行. 常见的报头有Accept(指定MIME媒体类型),Accept_Charset(响应消息的编码方式),Accept_Encoding(响应消息的字符集),User_Agent(用户的浏览器信息)等.
在请求消息的报头行之后是一个回车换行,表明请求消息的报头部分结束.在这个\n之后是请求消息的消息实体(Entity Body).
Web服务器在收到客户请求并作出处理之后,要向客户发送应答消息.与请求消息一样,应答消息的语法规则为:

< HTTP Version> <Status Code> [<Message>]<\n>
{ <Header>:<Value> <\n> } *
<\n>
{ Entity Body }

应答消息的第一行为状态行,其中包括了HTTP版本号,状态码和对状态码进行简短解释的消息;状态行的最后是回车换行.状态码由3位数字组成,有5类:

  • 1XX 保留
  • 2XX 表示成功
  • 3XX 表示URL已经被移走
  • 4XX 表示客户错误
  • 5XX 表示服务器错误

例如:415,表示不支持改媒体类型;503,表示服务器不能访问.最常见的是200,表示成功.常见的报头有:Last_Modified(最后修改时间),Content_Type(消息内容的MIME类型),Content_Length(内容长度)等.
在报头行之后也是一个回车换行,用以表示应答消息的报头部分的结束,以及应答消息实体的开始.
下面是一个应答消息的例子:

HTTP/1.0 200 OK
Date: Moday,07-Apr-97 21:13:02 GMT
Server:NCSA/1.1
MIME_Version:1.0
Content_Type:text/html
Last_Modified:Thu Dec 5 09:28:01 1996
Coentent_Length:3107

<HTML><HEAD><TITLE>...</HTML>

那么 GET 和 POST 有什么区别? 区别就是一个在 URL 请求里面附带了表单参数和值, 一个是在 HTTP 请求的消息实体中. 用下面的例子可以很容易的看到同样的数据通过GET和POST来发送的区别, 发送的数据是 username=张三 :

 GET 方式, 浏览器键入 http://localhost?username=张三

GET /?username=%E5%BC%A0%E4%B8%89 HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-powerpoint, application/vnd.ms-excel, application/msword, */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.1.4322)
Host: localhost
Connection: Keep-Alive

POST 方式:

POST / HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-powerpoint, application/vnd.ms-excel, application/msword, */*
Accept-Language: zh-cn
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.1.4322)
Host: localhost
Content-Length: 28
Connection: Keep-Alive

username=%E5%BC%A0%E4%B8%89

比较一下上面的两段文字, 您会发现 GET 方式把表单内容放在前面的请求头中, 而 POST 则把这些内容放在请求的主体中了, 同时 POST 中把请求的 Content-Type 头设置为 application/x-www-form-urlencoded. 而发送的正文都是一样的, 可以这样来构造一个表单提交正文:

encodeURIComponent(arg1)=encodeURIComponent(value1)&encodeURIComponent(arg2)=encodeURIComponent(value2)&.....

注: encodeURIComponent 返回一个包含了 charstring 内容的新的 String 对象(Unicode 格式), 所有空格、标点、重音符号以及其他非 ASCII 字符都用 %xx 编码代替,其中 xx 等于表示该字符的十六进制数。 例如,空格返回的是 "%20" 。 字符的值大于 255 的用 %uxxxx 格式存储。参见 JavaScript 的 encodeURIComponent() 方法.

下面就讨论一下如何在 JavaScript 中执行一个 GET 或者 POST 请求. 如果您用过 Java, 那么您可能熟悉下列的用 java.net.URLConnection 类进行 POST 操作的代码(参考 Java Tip 34: POSTing via Java ):
 

URL url;
URLConnection urlConn;
DataOutputStream printout;
// URL of CGI-Bin or jsp, asp script.
url = new URL ("somepage");
// URL connection channel.
urlConn = url.openConnection();
// ......
// No caching, we want the real thing.
urlConn.setUseCaches (false);
// Specify the content type.
urlConn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
// Send POST output.
printout = new DataOutputStream (urlConn.getOutputStream ());
String content = "name=" + URLEncoder.encode ("Buford Early") + "&email=" + URLEncoder.encode ("buford@known-space.com");
printout.writeBytes (content);
printout.flush ();
printout.close ();

以上的代码向 somepage 发送了一次 POST 请求, 数据为 name = Buford Early, email = buford@known-space.com.
用JavaScript 来执行 POST/GET 请求是同样的原理, 下面的代码展示了分别用 XMLHttpRequest 对象向 somepage 用 GET 和 POST 两种方式发送和上例相同的数据的具体过程:
GET 方式

var postContent =
"name=" + encodeURIComponent("Buford Early") + "&email=" + encodeURIComponent("buford@known-space.com");
xmlhttp.open("GET", "somepage" + "?" + postContent, true);
xmlhttp.send(null);

POST 方式

var postContent =
"name=" + encodeURIComponent("Buford Early") + "&email=" + encodeURIComponent("buford@known-space.com");
xmlhttp.open("POST", "somepage", true);
xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlhttp.send(postContent);

至此希望你已经能够理解如何用 JavaScript 中的 XMLHttpRequest 对象来执行 GET/POST 操作, 剩下的工作就是您如何来构造这些提交的参数了, 最后我给出一个将现有的 form 提交代码修改为异步的 AJAX 提交的代码(注意目前作者还不知道如何让 file 上传表单域也能异步上传文件). 首先请看两个 JavaScript 函数:

// form - the form to submit
// resultDivId - the division of which to display result text in, in null, then
// create an element and add it to the end of the body
function ajaxSubmitForm(form, resultDivId) {
var elements = form.elements;// Enumeration the form elements
var element;
var i;

var postContent = "";// Form contents need to submit

for(i=0;i<elements.length;++i) {
var element=elements[i];

if(element.type=="text" || element.type=="textarea" || element.type=="hidden") {
postContent += encodeURIComponent(element.name) + "=" + encodeURIComponent(element.value) + "&";
}
else if(element.type=="select-one"||element.type=="select-multiple") {
var options=element.options,j,item;
for(j=0;j<options.length;++j){
item=options[j];
if(item.selected) {
postContent += encodeURIComponent(element.name) + "=" + encodeURIComponent(item.value) + "&";
}
}
} else if(element.type=="checkbox"||element.type=="radio") {
if(element.checked) {
postContent += encodeURIComponent(element.name) + "=" + encodeURIComponent(element.value) + "&";
}
} else if(element.type=="file") {
if(element.value != "") {
postContent += encodeURIComponent(element.name) + "=" + encodeURIComponent(element.value) + "&";
}
} else {
postContent += encodeURIComponent(element.name) + "=" + encodeURIComponent(element.value) + "&";
}
}

alert(postContent);

ajaxSubmit(form.action, form.method, postContent);
}

// url - the url to do submit
// method - "get" or "post"
// postContent - the string with values to be submited
// resultDivId - the division of which to display result text in, in null, then
// create an element and add it to the end of the body
function ajaxSubmit(url, method, postContent, resultDivId)
{
var loadingDiv = document.getElementById('loading');
// call in new thread to allow ui to update
window.setTimeout(function () {
loadingDiv.innerText = "Loading....";
loadingDiv.style.display = "";
}, 1);

// code for Mozilla, etc.
if (window.XMLHttpRequest)
{
xmlhttp=new XMLHttpRequest();
}
// code for IE
else if (window.ActiveXObject)
{
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}

if(xmlhttp) {
xmlhttp.onreadystatechange = function() {
// if xmlhttp shows "loaded"
if (xmlhttp.readyState==4)
{
if(resultDivId) {
document.getElementByID(resultDivId).innerHTML = xmlhttp.responseText;
} else {
var result = document.createElement("DIV");
result.style.border="1px solid #363636";
result.innerHTML = xmlhttp.responseText;
document.body.appendChild(result);
}

loadingDiv.innerHTML =
"Submit finnished!";
}

};

if(method.toLowerCase() == "get") {
xmlhttp.open("GET", url + "?" + postContent, true);
xmlhttp.send(null);
} else if(method.toLowerCase() == "post") {
xmlhttp.open("POST", url, true);
xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlhttp.send(postContent);
}
} else {
loadingDiv.innerHTML =
"Can't create XMLHttpRequest object, please check your web browser.";
}

}

函数 ajaxSubmitForm 将表单要提交的内容进行封装, 然后调用 ajaxSubmit 函数来执行真正的异步提交, 表单提交后所返回的结果则显示在给定的 DIV 容器中或者没有指定参数时用 DOM 对象动态生成一个 DIV 容器来显示结果并添加到页面末尾. 这样, 对原来的表单只需要改动一个地方就可以将原来的表单提交改为异步模式, 即在 form 标签里加入: onSubmit="ajaxSubmitForm(this);return false;" 即可, return false 确保表单不会被浏览器同步提交. 完整的例子请看这里.


----------------- 引用结束 -----------------

OK, 希望至此为止您已经理解了如何用 AJAX 来正确的执行 GET/POST. 如果这个问题您解决了, 可是说后台的乱码问题就和你直接通过表单提交几乎没有区别了. 这个方法的具体封装已经在附件的 ajax_common.js 中了.

至此也该贴出来我们的 GBK 编码的客户端页面的内容了:


<html>

<head>
<meta http-equiv="Content-Type" content="text/html; charset=gbk">
<title>AJAX Form Submit Test</title>
<script src='ajax_common.js'></script>

</head>

<body>
本页面的编码是中文.<br/>
&lt;meta http-equiv="Content-Type" content="text/html; charset=gbk"&gt;<br/>
<b>测试过的服务器:</b><br/>
Resin 3.0.18<br/>
Tomcat 5.5.20<br/>
Tomcat 5.0.30<br/>
<h3>AJAX Form Submit Test</h3>
Fill the form and then click submit<br>
提交方式: POST<br>
<form method="POST" id="form1" name="form1"
action="form_action.jsp"
onSubmit="former.ajaxSubmitForm();return false;">
<p><input type="hidden" name="hidden1" value="hiddenValue">
text:<input type="text" name="textf&1" size="20" value="text文本&amp;1">
checkbox:<input type="checkbox" name="checkbox1" value="ON" checked>
radio:<input type="radio" value="V1" checked name="radio1">
select:<select size="1" name="select1">
<option selected value="option1">D1</option>
</select>
<br>
<br>
<input type="submit" name="B1" value="submit">
<input type="reset" name="B2" value="reset">
</p>
</form>

提交方式: GET<br>
<form method="GET" id="form2" name="form2"
action="form_action.jsp"
onSubmit="former2.ajaxSubmitForm();return false;">
<p><input type="hidden" name="hidden1" value="hiddenValue">
text:<input type="text" name="text文本&amp;2" size="20" value="text文本&amp;2">
checkbox:<input type="checkbox" name="checkbox1" value="ON" checked>
radio:<input type="radio" value="V1" checked name="radio1">
select:<select size="1" name="select1">
<option selected value="option1">D1</option>
</select>
<br>
<br>
<input type="submit" name="B1" value="submit">
<input type="reset" name="B2" value="reset">
</p>
</form>

<div id="loading" style="display:none; position:absolute;
border:1px solid orange; height:20px; width:600; left: 93px; top: 112px;
background-color: #FFFFCC; cursor:pointer;" title="Click to hide" onClick="this.style.display='none';"></div>

<div id="resultDiv" style="border:1px solid orange; background-color: #FFFFCC; cursor:pointer;" title="Click to hide" onClick="this.style.display='none';">
Form 1 的提交结果将会显示在这里.
</div>


<script type="text/javascript">
var former = new AjaxFormer($('form1'), 'resultDiv');
var former2 = new AjaxFormer($('form2'));
</script>
</body>

</html>

可以看到我们的确使用的是 GBK 编码, 浏览器打开的时候自动选择的编码也是简体中文.

那么第二个关键点就是服务器端的表单数据读取了.这个问题跟具体的服务器有很大关系. 对于 Resin 服务器来说, 问题很少, 基本上不论是 POST 和 GET, 出乱码的概率都比较小. 但是 Tomcat 就不敢恭维了, 这大概也是开源产品和商业产品的区别, 缺乏前后一致性和兼容性, 因为开源的不需要提供技术支持. Tomcat 的 GET/POST 的编码处理方式不同的版本都不一样, 就像 Eclipse/Netbeans 新版本从来不需要兼容老版本的插件 API 一样, Hibernate/Struts/Spring 也是一样, 所以学 Java 的很累. 当然, 这就是免费/开源的代价. 跑题了. 因此我们的服务器端代码大部分都是对 Tomcat 的乱码问题的解决(POST的没有问题, 主要是 GET 方法的).

<%@ page contentType="text/html; charset=gbk" pageEncoding="gbk"%>
<html>
<%
//Send some headers to keep the user's browser from caching the response.
response.addHeader("Expires", "Mon, 26 Jul 1997 05:00:00 GMT" );
response.addHeader("Last-Modified", new java.util.Date().toGMTString());
response.addHeader("Cache-Control", "no-cache, must-revalidate" );
response.addHeader("Pragma", "no-cache" );
// This will emulate a network delay, for 2 sec.
//Thread.currentThread().sleep(2000);

request.setCharacterEncoding("utf-8");
%>

<%!

/**
* 转换字符串的内码.
*
* @param input
* 输入的字符串
* @param sourceEncoding
* 源字符集名称
* @param targetEncoding
* 目标字符集名称
* @return 转换结果, 如果有错误发生, 则返回原来的值
*/
public static String changeEncoding(String input, String sourceEncoding,
String targetEncoding) {
if (input == null || input.equals("")) {
return input;
}

try {
byte[] bytes = input.getBytes(sourceEncoding);
return new String(bytes, targetEncoding);
} catch (Exception ex) {
}
return input;
}

/**
* 一个类似于 JavaScript 的 escape 函数的功能, 确保乱码可以正确传输.
*/
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)
tmp.append("0");
tmp.append(Integer.toString(j, 16));
} else {
tmp.append("%u");
tmp.append(Integer.toString(j, 16));
}
}
return tmp.toString();
}
%>

<head>
<title>Test form action page</title>
</head>
<body>
这是 GBK 编码版本的后台表单提交页面.<br/>

<%
boolean isTomcat = application.getServerInfo().toLowerCase().indexOf("tomcat") != -1;
%>

Form submit method:<%=request.getMethod()%><br/>
The form content u send is:<br/>
<%
java.util.Enumeration e = request.getParameterNames();

while (e.hasMoreElements()) {
String name = (String)e.nextElement();
String value = request.getParameter(name);

if(isTomcat && request.getMethod().equalsIgnoreCase("GET")) {
name = changeEncoding(name, "ISO8859-1", "UTF-8");
value = changeEncoding(value, "ISO8859-1", "UTF-8");
}
out.println("<b>" + name + "</b> = " + value + "<br/>");
}

// 给前台返回一个可以执行的脚本
//response.addHeader("response_script", changeEncoding("alert('提交完成');", "ISO8859-1", "UTF-8"));
response.addHeader("response_script", escape("alert('提交完成');"));
%>

</body>
</html>

booleanisTomcat=application.getServerInfo().toLowerCase().indexOf("tomcat") !=-1; 这一句主要针对 Tomcat 进行处理, 如果是 GET 方法的话, 就需要将表单参数从 ISO8859-1 转换到 UTF-8 (注意不是 GBK, 貌似 Tomcat 很喜欢 UTF-8?). 其它的地方和原来的 UTF-8 版本的没有区别. 当然如果您的站点应该用过滤器来更方便的解决这个问题.

小结:

1. 使用一致的字符集很重要, 要么全是 GBK, 要么全是 UTF-8, 如果有条件, 就全部用 UTF-8, 那样工作量是最小的;

2. 用 AJAX 提交的时候一定要按照 HTTP 的规范来, 做到和浏览器尽量兼容, 尤其是 POST 的时候不要再往 URL 地址里加参数了, 你那样是违规! 后果就是有的服务器会不搭理你传递的这些参数! 还是如我所讲, 参数提交之前要用 encodeURIComponent() 来转化, 这也是为了符合浏览器的习惯做法.

3. 后台如果读取参数有乱码, 就尽量多在 ISO8859-1, GBK, UTF-8 中间多转换几次试试, 可以试试偶写的那个 changeEncoding() 方法, 把几个转换后的表单值都列出来, 一定有一个是正确的, 总是可以解决问题的. 这个本来不应该是偶们的任务, 但是写服务器的人是老美, 尤其是 Tomcat 作者, 只熟悉 ISO8859-1.

4. 鉴于 TOMCAT 读取 POST 参数的时候很少出问题, 因此建议AJAX提交表单的时候多用 POST 方法, 尽量不用 GET.

运行截屏:

http://www.blogjava.net/images/blogjava_net/beansoft/18680/o_AJAX%20Form%20Submit%20GBK.png

其它的一些资料可以参考Blogjava上的一篇原创文章: [原创]struts,ajax乱码解决方案

欢迎发表建议和更好的观点. 谢谢! 重申本文无意代替您的 AJAX 框架, 不过在你抓狂的时候可以考虑看看他们表单提交的代码, 改改它!

本人翻译的 XMLHttpRequest 对象介绍:

The XMLHttpRequest Object Reference XMLHttpRequest 对象参考

Methods 方法

Method 方法 Description 描述
abort() Cancels the current request
取消当前请求
getAllResponseHeaders() Returns the complete set of http headers as a string
将完整的 HTTP 头部做为一个字符串返回
getResponseHeader("headername") Returns the value of the specified http header
返回给定的 HTTP 头的值
open("method","URL",async,"uname","pswd") Specifies the method, URL, and other optional attributes of a request

The method parameter can have a value of "GET", "POST", or "PUT" (use "GET" when requesting data and use "POST" when sending data (especially if the length of the data is greater than 512 bytes.

The URL parameter may be either a relative or complete URL.

The async parameter specifies whether the request should be handled asynchronously or not. true means that script processing carries on after the send() method, without waiting for a response. false means that the script waits for a response before continuing script processing
指定表单提交方法, URL, 以及请求的可选属性

method 参数可以是"GET", "POST" 或者 "PUT" 这些值中之一(使用"GET"来请求数据, 特别的, 当发送的数据长度大于512字节时使用 "POST").

URL 参数可以为相对的和完整的 URL.

async 参数指定请求是否为异步方式处理. true 意味着调用 send() 方法后脚本继续向下执行, 不需要等待响应. false 意味着脚本将等待响应之后才能继续执行

send(content) Sends the request
发送请求
setRequestHeader("label","value") Adds a label/value pair to the http header to be sent
在要发送的 HTTP 头中添加 标签/取值

Properties 属性

Property 属性 Description 描述
onreadystatechange An event handler for an event that fires at every state change
每次状态改变时除非的事件处理器
readyState Returns the state of the object:

0 = uninitialized
1 = loading
2 = loaded
3 = interactive
4 = complete
返回对象的状态

0 = 未初始化
1 = 载入中
2 = 已载入
3 = 交互
4 = 完成

responseText Returns the response as a string
将响应做为字符串返回
responseXML Returns the response as XML. This property returns an XML document object, which can be examined and parsed using W3C DOM node tree methods and properties
将响应做为XML返回. 这个属性返回一个 XML 文档对象, 可以用 W3C 的 DOM 节点树方法和属性进行检索分析
status Returns the status as a number (e.g. 404 for "Not Found" or 200 for "OK")
将状态做为数字返回(例如 404 为"Not Found" 或者 200 为 "OK")
statusText Returns the status as a string (e.g. "Not Found" or "OK")
将状态做为字符串返回(例如 "Not Found" 或者 "OK")
 
posted on 2006-12-31 14:37 BeanSoft 阅读(2650) 评论(6)  编辑 收藏 引用 所属分类: Web

posted on 2007-07-25 15:01 Tom 阅读(1127) 评论(0)  编辑  收藏 所属分类: JavaScript


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


网站导航:
 
<2007年7月>
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234

导航

统计

常用链接

留言簿(1)

随笔分类(42)

随笔档案(43)

文章分类

相册

搜索

最新评论

阅读排行榜

评论排行榜