标题里的技术都是web开发里最常见的技术,但是我想这些常用的技术有很多细节是很多朋友不太清楚的,理解这些细节是我们深入掌握这些技术的一把钥匙,今天我就讲讲我使用这些技术时体会到的这些细节。
同步提交是指通过对页面的form表单执行submit操作,将用户在页面上录入的数据提交到服务器,服务器处理完数据后将结果信息返回到页面上。
当我们使用同步提交的时候有一个不可或缺的元素,就是form标签,form标签代表了一个边界,在form范围内的input,select等元素,在form执行submit事件后,它们所记录的数据就会被浏览器使用http协议以post或者get的方式传输到对应的服务器上。
那么我们如何执行form的submit操作了?浏览器为我们提供了四种方式:
方式一:
<input type=”submit” value=”Submit”/>
方式二:
<button type=”submit”>Submit</button>
方式三:
<input type=”image” src=”submit.gif”/>
方式四:
<form id=”frm” name=”frm”></form>
document.getElementById(“frm”).submit()
这里最特别的是方式三,在一个form标签里包含一个type为image的元素,其效果和一个submit按钮居然一样,这个估计很多人没有发现。
不过从我多年的使用感受来说,前三种方式的本质是一样,应该归为一类,而第四种方式灵活性最高,也是最常用的,在实际开发里我们最好只使用第四种方式,摒弃前三种方式,为了让我的建议由说服力,我下面就讲讲前三种方式的坏处。
坏处一:form表单会自动提交。
form的自动提交一般都是我们在页面录入信息时候,不小心按了回车键,具体点是当form表单下的输入元素(例如:文本框、密码框、下拉框、单选框等等,不过textarea文本域除外)获取焦点,这个时候你按了回车键就会导致form表单自动提交。为了防止这种情况的发生, 我们会这么改写form,如下所示:
<form name="form1" method="post" onsubmit="return false;">
onsubmit="return false;"就可以让我们阻止form表单自动提交行为,虽然我们有了解决这种问题的手段,但是我们并不知道什么原因促使了这个问题的发生,其实问题的根本原因就是我们开发的页面里有前三种方式中的一种方式,这几个特别的用于提交form表单的元素是问题的罪魁祸首。
坏处二:当用户在页面填写完数据后,用户接着要执行submit操作,当今的网站几乎都不会让用户就这么直接把数据提交到后台,出于各种要命原因的考虑,我们必须在浏览器提交数据前对数据进行校验或者对某些数据进行格式的转化。
由于上面的原因,我们就不得不在有submit能力的标签上添加校验方法,这些方法往往是和点击事件绑定在一起,这似乎没啥问题,但有时我们却发现页面在测试的时候居然会不通过,不通过的原因是我们发现服务端接收到了两次同样的http请求,我发现很多做了多年开发的朋友对这个问题的原因也是不甚明了,可能这个问题的解决方法就和上面的坏处一提供的解决方案一样,太完美了,完美的阻止了我们探求真相的原因,这个问题的解决方案就是:将按钮的类型变成button,同时不要使用form表单里附带了submit功能的按钮,而是方式四来提交form表单。
使用具有submit功能元素提交form表单会有会有重复提交的原因是这些元素本身有一个默认的点击事件,这个点击事件是用来执行submit操作,如果我们给submit按钮添加校验方式时候也是使用点击事件,如果我们代码里不小心写了个form.submit,那么结果就会让form表单执行两遍。当然在点击事件里有办法阻止默认事件就是:evt.preventDefault(),这样按钮就不会执行默认的submit事件,关于这一点在我们使用a标签时候也会经常发生,所以我们会常常看到a标签里有这种写法href="javascript:void(0)",其目的就是阻止a标签执行默认的href属性的点击事件,同样对于a标签evt.preventDefault()也是有效的,它也能阻止a标签的默认事件。
方式四是使用javascript语言来控制form表单的提交,所以方式四和javascript代码结合最好,而当今网站的网页都会使用大量的javascript代码来增强页面的智能性,毫不犹豫的使用方案四做form的submit操作,现在应该要当做一种惯例来执行。
不过有很多场景只能使用form表单和服务端交互,这种独占的场景都是在和ajax技术进行比较后得出了,如果要解释清楚原因,得先把ajax给聊清楚,所以这里我们暂时先卖个关子,等到ajax技术讲解完毕后,我们回过头来在聊聊只能使用form表单的场景。
Ajax这个名字是个缩写,它的全称应该叫做异步的javascript+xml的技术,它的出现对web前端开发具有划时代的意义,正式因为ajax,提升了浏览器在web应用中的作用,才需要更加专业的web前端工程师专门从事web前端的开发。
Ajax技术的核心是XMLHttpRequest对象, XMLHttpRequest对象是一个javascript内置对象,不过在ie6以下的版本(包括ie6)需要使用ActiveXObject方式创建它。关于XMLHttpRequest的用法我想只要做过ajax应用的人都十分熟悉和清楚,因此我这里不想对XMLHttpRequest做过多描述,不过还是要贴一份代码,这样便于我接下来的讲述。
代码如下:
<script type="text/javascript">
var xmlhttp;
function loadXMLDoc(url)
{
xmlhttp=null;
if (window.XMLHttpRequest)
{// code for all new browsers
xmlhttp=new XMLHttpRequest();
}
else if (window.ActiveXObject)
{// code for IE5 and IE6
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
if (xmlhttp!=null)
{
xmlhttp.onreadystatechange=state_Change;
xmlhttp.open("GET",url,true);
xmlhttp.send(null);
}
else
{
alert("Your browser does not support XMLHTTP.");
}
} function state_Change()
{
if (xmlhttp.readyState==4)
{// 4 = "loaded"
if (xmlhttp.status==200)
{// 200 = OK
// ...our code here...
}
else
{
alert("Problem retrieving XML data");
}
}
}
</script>
上面的例子是使用ajax执行get的请求,这个执行操作的代码如下:
xmlhttp.onreadystatechange=state_Change;
xmlhttp.open("GET",url,true);
xmlhttp.send(null);
这三行代码还是很有学问的,有些细节可能很多朋友理解的并不是太清楚,下面我就讲讲这些细节。
细节一:open方法,open方法有三个参数,第一个参数是http请求的方式,XMLHttpRequest常用的请求方式有两种:get和post,第二个参数是请求的url,如果你使用XMLHttpRequest对象执行get操作,那么你就得把参数组织在url里,第三个参数的含义是【是否异步发送请求】,第三个参数的解释我用括号括起来,想表达的就是第三个参数其实很多朋友并没有正确理解它,该参数可以传两个值:true和false,true代表异步发送请求,false代表同步发送请求,那么这里就有一个问题什么叫做异步发送请求,什么叫做同步发送请求了?其实XMLHttpRequest里异步和同步的含义是指ajax执行时候是否阻塞后续javascript代码的执行,如果我们选择同步执行方式,那么ajax执行http请求后,其他javascript代码必须等待http执行完毕即请求得到了响应并且响应结果被处理完毕后,其他的javascript代码才能执行,如果我们选择异步执行方式,那么ajax执行http请求后不会阻塞其他javascript代码执行即ajax执行http请求后浏览器会单独起一个线程等到这个http的响应,当前线程马上就可以执行其他javascript代码,等独立线程获得了http响应再去插队UI线程执行ajax的回调函数。这也是我前面文章讲到使用ajax方式解决阻塞脚本问题的原理。
细节二:关于send方法,send方法的参数只有一个即send(body),在使用get方式提交请求,send方法传入null值,如果我们使用post方式提交我们会将参数放置在send方法的参数里,对于这一点区别很多朋友都不是很清楚,因为我们做web开发时候不管是get方式还是post方式都会传递参数,为什么到了ajax会单独用个send方法传递参数呢?解释这个原因就得从http协议说起,我们知道http的get请求的参数都是放置到url后面,而post则不会,其实这种不同不仅仅源自于我们看到的现象,到了http报文的组织也不一样,get请求参数是和url集成的,而http报文记录url的位置是http头,而执行post请求的时候,http报文组织时候会将url和请求参数分离,url还是放置在http头部,请求参数则是作为报文主体的一部分发送至服务器,而send方法的一个作用就是可以往http报文的body即主体里填写请求参数,因此get的请求就无法使用send传递参数,因为get不需要http的主体里传输参数,只有post方式可以使用send方法传递参数。由此可见XMLHttpRequest对象给开发者提供了更多精确控制http请求的能力,这一点是传统form表单提交方式所不能给予的。
细节三:本细节也是关于send方法的问题,不过这个细节和post请求方式联系更加紧密,所以这里我先给出一种post请求的写法:
xmlhttp.open("POST",url,true);
//post请求要自己设置请求头
xmlhttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
//发送数据,开始与服务器进行交互
//post发送请求
xmlhttp.send("name=sharpxiajun&city=shanghai ");
一般我们使用post请求提交时候,要设置http的头Content-Type为application/x-www-form-urlencoded,而send的请求参数的组织方式就是按照get请求封装到url后面参数的形式来的。为什么ajax要这么做了?
我记得几年前我曾请教过一位朋友关于这个问题的答案,他告诉我ajax的post请求是模拟form表单执行post请求,而form表单使用post提交的时候会将http请求头里的Content-Type置为application/x-www-form-urlencoded,当时他还写了一个例子来说明这个问题。虽然问题得到了解答,但是却让我感觉到XMLHttpRequest更加神秘,为什么ajax做post请求要模拟同步的form表单的post请求了?它自己做个属于自己的post请求方式不行吗?而且更可恶的是一直到前不久我都觉得ajax没有提供非模拟form表单提交的方式。
这种迷惑的根源在哪里了?首先我们思考下在没有ajax的情况下,我们如何执行post操作,答案只有一个就是使用form表单,除此之外别无他法,相比之下get就不同的,get方式的请求是将url和参数融为一体,例如下面写法:
www.cnblogs.com? name=sharpxiajun&city=shanghai
而javascript的内置对象有很多操作url的方式,最典型的一种就是:
location.href = url
如果我们在url后面加入参数,参数任然可以被服务端所获取,但是这个过程就没有使用form表单了。
在同步提交的时代下浏览器要执行post请求只能通过form,但是这个form对post的处理还不仅仅这些,每当我们使用post提交form时候会将Content-Type置为application/x-www-form-urlencoded,这个参数的作用仅仅是让接收服务器知道这个请求使用了post方式提交数据的吗?
我们知道http使用get方式传参,参数会有长度的限制,这主要是url最大长度制约了参数的长度,但是使用post则不同,post将参数放置在http请求体里,原则上是没有任何长度限制。想想我们经常使用浏览器下载图片,上传图片,甚至还能使用浏览器上传下载超大的视频文件,这些都是归功于请求数据放置在http文件体里,因此使用post的http请求我们可以传输任何数据,数据格式不仅仅局限于文本,那么如果有一天我们要使用form表单传输一个文件,文件在网络里会被传化为二进制,等数据到了服务端后还需要转码,还原文件,这些操作都只能在post请求方式下进行,所以post传参比get复杂的多,为了让服务端知道这个复杂的情形,我们就必须要告诉服务器会有这种情况发生,所以form做post请求就会出现指定Content-Type的问题,服务器接收到这个Content-Type就明白了,请求数据的情况会很复杂,要注意了。
到了ajax,我们也需要执行post请求,但是ajax的post请求方式是模拟form表单的post方式,这么做的目的是为了照顾服务器的感情,本来页面的post提交情况对于服务端就很复杂了,要是ajax另起炉灶使用新方式提交post数据,那么不是所有的web服务器都要版本升级,这样的结果就是低版本的web容器就不能享受ajax的便利了,这么做的坏处也就不言而喻了,所以我们希望web服务器能一视同仁的对待所有post请求。
不过,XMLHttpRequest操作ajax请求真的只有模拟form表单提交方式这一种吗?答案还真不是,其实使用ajax做post请求我们可以完全没必要使用模拟form表单提交方式,我前不久开发时候就碰到了这个问题,我开发一个新系统时候为了保证系统的安全性,提出一个要求:所有页面提交到服务端的请求如果包含请求参数都得把Content-Type置为application/json,如果不是application/json请求,就会被拒,听人说这是系统规范,考,哪来这么操蛋的规范啊,如是我开发时候就改了Content-Type,结果是服务端居然接收不到请求参数了,但是通过firebug抓包,浏览器请求成功发送了,这下子头大了,问题出在哪里了?什么地方导致请求数据丢失了?
最终我发现了问题原因,是send方法,使用send传参时候我们都会这么写xmlhttp.send("name=sharpxiajun&city=shanghai ");名值对用=分割,不同名值对用&号分割,这个参数形式叫做序列化,在jQuery里还专门有做序列化的方法,在使用form表单同步提交方式下,我们不用考虑参数序列化的事情,因为submit后浏览器会帮我们做好这件事情,不过同步提交下也有痕迹告诉浏览器做了序列化的操作,例如上面我讲到的使用非form表单提交get请求的方式,但是使用ajax就得我们自己干这些事情了,使用jQuery多的人常常忽视了序列化的背景知识,因为jQuery让我们习惯了使用json格式提交数据,所以说要学好javascript基本功还是很重要的。序列化的数据到了服务端,web服务器就会帮我们把名值对解析好,但是当我们改变了Content-Type值后,web服务器就不会去做这件事情即参数没有被解析,所以在java里的servlet我们就不能按传统的方式获取参数了,但是参数其实并没有消失,而是解析方式不对,所以最后我们这么改写ajax:
xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8;");
var _param = {};
xhr.send(JSON.stringify(_param));
send里传递的不再是序列化数据,而是使用json格式的字符串,这样web服务器就不是通过序列化解析数据,而是通过json解析了。
由此可见XMLHttpRequest给我们操作http请求带来了更大的灵活性。
细节四:本细节任然是send方法的问题,send参数在官方文档里可以接受两种类型的参数一种是xml,一种是字符串,前面例子里我们都是使用的字符串,而没有使用xml格式,本来我想试验一下xml格式,可惜,我一直没做这个事情,所以本篇文章拖了许久没发表了,今天想想算了,不试验了,让看了本文的大牛们去试试。
前不久看了一篇文章,里面写到send的参数可以是form表单的DOM即可以这么传参数xhr.send(document.getElementById(“frm”)),这个做法我也没试验过,但是这个描述会影响到我下文的描述,如果我form表单里包含文件上传的input,这么做的post请求会将文件数据提交到后台服务器吗?
现在我们抛开send非字符串的参数类型,假设我们的XMLHttpRequest只能传递字符串,那么问题来了我们就没法使用ajax提交文件数据到服务器了,换句话说XMLHttpRequest是无法上传文件的。我很早之前写了一个ajax文件上传的框架,使用hack的技术模拟异步的文件上传操作,它的原理是在主页面构造一个隐藏的form表单和隐藏的iframe,我将页面可见form里的元素拷贝到隐藏的form里,然后submit这个form,隐藏的form请求响应由隐藏的iframe接收,然后通过操作iframe的响应数据来完成对ajax异步文件上传模拟。
hack技术写起来很爽很有成就感,但是如果有天然的ajax异步提交方式才是最适合生产开发的,本来细节四是为了指出ajax不能做文件上传操作,但是查阅资料后发现可能是我没有找到正确途径,这个需要我更进一步研究,因为自己还没有实践,但是为了保持内容的严谨,我还是要提提自己的质疑,如果结果是XMLHttpRequest的确不能这么做,那么我也给出了模拟异步文件请求的方式,一举两得,其实有时没有答案的思考也能给人以长进。
细节五:ajax的x本意是xml,随着ajax技术的发展,我们在ajax里使用xml其实并不太多了,取代xml的技术是更好用的json,为什么有这个转变了?
上文我讲到send方法还有个参数类型就是xml,但是实际上我们都快遗忘它有这个参数,因为浏览器里将一段和xml相似的字符串转化为真正的xml并不太容易,而且不同浏览器构造xml还存在差异,这点可能有些朋友看到了会不太理解,因为很多人都知道html是xml的子集,我这里居然会大放厥词说javascript和xml居然融合的不好,但事实就是如此,javascript里生成xml是个冷僻技术了,虽然javascript规范里的确规定了生成xml方式,但是实际开发里真正使用的少之又少,这里要注明下,本段生成xml指的是使用javascript构造xml,不是使用javascript解析xml。
当服务器返回的数据是xml时候,javascript操作xml就变的简单些了,我们可以直接将xml传化为dom操作,这个操作和操作html类似。不过操作xml即操作dom没有json好,下面我们看看下面的例子:
<user>
<name>sharpxiajun</name>
<city>shanghai</city>
</user>
如果我们用dom操作这个xml想取到name的值,我们就得遍历,上面的例子很简单,如果再复杂点即name下还有子元素,那么这种遍历就更深,程序编写也就越复杂,不过我们有个替代方法,看下面的xml:
<user name="sharpxiajun" city="shanghai"></user>
这个时候我们使用XXX.getAttribute(“name”)就能获取name的值,而且这个xml比上面变简单了,同时数据也变小了。但是这也说明了另外一个问题,就是xml太灵活了,同一个数据可以选择多种多样的方式表示,这样的结果是xml表现数据很不直观,同时传输的数据量也很大,而且传输的数据里有很多内容是为了用于表达数据含义的,而不是真正有用的数据。相比之下,json格式的数据就比较单纯,例如下面的json:
{"name":"sharpxiajun","city":"shanghai"}
这段json我们能很清晰的看懂,而且json很容易传化为javascript对象,操作十分方便,所以json最终取代了xml。
谈到json,我要插一句,记得有个网友曾经私下问我一个问题,说他一次做代码评审时候一个年轻的程序员跟他提了个问题,因为问我的朋友在执行ajax操作时候喜欢使用json传递数据(这位朋友一般用jQuery),所以每次构造参数他都喜欢这么写:
var param = {name:"sharpxiajun",city:"shanghai"}
年轻的程序员认为他的写法不规范,因为json的规范里不管是名值对的名还是值都要加引号(幸亏这个程序员说是引号,没有排除单引号),那么他认为上面的写法会有安全隐患,隐患的原因就是不规范,但是问我的朋友说他开发做了好几年,几乎都是这么写的,没发现有问题,当时他问我,我也被问倒了。
但是我现在知道答案了,json是什么?json的定义是javascript对象的表示方法,它是一种数据格式,json不是变量。所以json其实不是javascript对象,它是按照javascript对象字面量的模式定义的数据格式,这个数据格式可以被服务端语言解析为map对象。其实问我的朋友这么写一点问题都没有,这个朋友的写法是javascript对象的字面量定义方式,javascript对象的名值对的名可以不用引号,如果你传入参数是javascript对象,程序一点隐患都没有,不过网络传输时候,javascript对象要变成json格式,浏览器会让名值对的名加上引号的。
细节六:ajax的核心对象XMLHttpRequest可以操作http的请求头.
例如:
xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8;");
这给ajax带来了很大的灵活性,记得有个初学者曾问我过一个问题,XMLHttpRequest可以操作http请求头,那同步请求怎么定义请求头呢?我当时也被这个问题卡壳了,同步请求我们可以操作请求头吗?我当时回答是不行,其实同步提交请求可以改变http的请求头,那就是html的meta标签,如下图所示:
相比之下XMLHttpRequest对请求头的操作更加灵活。
哦,十一点半了,写不完了,只得分篇写了,下一篇文章我会继续讲解ajax技术,然后把同步提交和ajax技术作对比,接下来讲comet(服务器推送技术),最后来个大总结。也许分篇写,我还能记起更多能容哦。