探求网页同步提交、ajax和comet不为人知的秘密(中篇)
深入研究某项技术,了解使用这些技术的细节,其实最终目的都是为了完成一个选择问题:当我们要使用这些技术解决某个具体的问题时候我们到底该如何去选择。如果碰到有两种技术可以让我们达到同样的目的,我们就会不自然的去比较它们之间的差异,通过对这些差异的梳理,我们就能得出在使用它们时候我们到底该如何取舍。
上篇里我讲到XMLHttpRequest可以更加精确的控制http请求的报文头,如是乎我就去寻找在同步提交里我是否能像XMLHttpRequest那样的去控制http的头部信息呢,最终我发现同步提交下我们可以在html的head里设置meta标签,使用http-equiv来设置http的头部信息,不过meta的头部信息其实是服务器给浏览器的响应头部信息,而不是请求头部信息,下面是我们最常用,最能影响页面展示的meta写法,如下所示:
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> <meta http-equiv="expires" content="31 Dec 2099">
这个头部是告诉浏览器该页面是一个html文档,字符集是UTF-8,还告诉浏览器你要将这个页面缓存在浏览器里,如果用户不清理浏览器缓存,那么该页面在2099年12月31号前都可以从缓存读取,不需要在从服务器下载整个html文档了。
Expire是服务端指定浏览器的缓存行为,在web前端优化里它所代表的缓存能力是非常重要,ajax也能使用浏览器缓存,不过这块知识需要和我后面要讲的内容相关,所以这里先暂时搁置一下。
Content-type在上篇文章里多次出现,我们开发页面时候,html的head里该属性可谓是一个熟悉的陌生人,在很多用来开发页面的IDE里,我们选择模板创建一个页面,这个标签都是默认带有的,都不用我们自己添加它,但是我在多年开发经验我还真的碰到过content-type使用不当所带来的技术问题。
很早前有位朋友请教我一个这样的问题,他们公司有个web应用,这个web应用没有太多的页面,大部分是后台程序,该系统的目的是对他们公司用户提供一些特别的服务,用户如果要使用该服务只需要在自己网站页面里跨域请求他们系统提供的网页,为了保证系统的安全,这位朋友的系统会对用户发送的请求做很多安全性的检查,这个检查比较耗时,但是又非常重要,因此他们为了给终端使用者以友好的用户体验,就在安全检查正在处理时提供一个等待页面,也许是因为这个朋友开发的系统服务端过重而且页面太少太简单,不是特别擅长页面开发的他们开发时候就没有单独写个等待页面,而是将等待页面放在后台的servlet里直接构造(其实就是在后台直接拼写页面的字符串了),但是他们构造时候忘了在这个简单页面里加上content-type属性,可恶的是这位朋友的系统测试时候没有发现任何问题,可怕的是上了生产环境后某些调用他们系统的用户在ie浏览器下页面直接显示了是源代码而不是页面,在非ie浏览器下却是正常。不是所有人有问题,有问题也是部分浏览器,在自己的测试环境又模拟不出问题,这怎么搞,那位朋友的项目组花了两天都没弄明白怎么回事,而这时用户的投诉是越来越多,如是他问我碰到过这种情况吗?当时我用ie和非ie抓包工具对比了在不同浏览器里http请求的请求头和响应头,发现了区别:有的浏览器content-type是text/html,有的是text/plain,ie浏览器下都是text/plain,非ie两种皆有,不过ie下如果是text/plain,那么页面就会显示源码,如是我把我看到的现象告诉了这位朋友,这位朋友查阅下代码发现他们构造等待页面时候没有指定content-type头,如是他们强制指定该头为text/html,问题就解决。虽然问题解决了,但是我们还是没有找到什么地方出问题了,为什么测试环境下不能重现问题的发生,这个现象对于做过互联网开发的朋友可能就很好理解了,互联网的应用部署的网络环境是非常复杂的,用户从浏览器发送一个http请求到服务端,这个过程并不是像我们平时开发那样直接到像tomcat,Jboss这些web容器,而是会经过好多路径,最常见的有cdn,负载均衡设备,静态资源服务器例如apache等等,请求通过的这些环节都有权限和能力更改http报文,如果你们的网站规模越大,那么网络环境会更加复杂,几乎很难找到一个知道这所有细节的人,所以在互联网公司都会有一个预发布环境,这个环境会尽最大努力模拟真实环境,虽然这个环境也是为了测试,但很多公司的预发布环境是可以在外网访问,如果预发布环境设计的好,就能很有效的保证系统投产后的安全性,不过最有效能避免上面问题发生的手段还是自己技术、经验要过硬。
Content-type很有用,多理解下它可以为我们解决更多疑惑,详细讲解这个属性不是本文的主题,下面我给感兴趣的朋友两个链接,有兴趣可以瞧瞧:
http://baike.baidu.com/view/1547292.htm?fr=aladdin
http://tool.oschina.net/commons
大多数时候,在同步请求里我们发送http请求给服务端是不需要了解太多content-type的作用,其实很多http头也不用过多关心,但是有个东西特别是像我们这种使用汉字而非英语的国家,对字符集的编码还是要倍加注意的。
虽然html头部设置charset的编码是UTF-8,这是响应头的字符集,但是如果我们以这个响应的页面接着做http请求,那么这个编码级会影响到提交请求的字符编码即我们如果不做特别指定那么之后在这个页面所有请求都是按照该编码级进行的。那么问题来了,如果我们想从一个UTF-8页面请求一个后台只能接收GBK编码数据的接口时候,那么我们怎么更改这个请求的编码了,问题更进一步点,我们该如何在同步请求里控制请求的http头,至少是很重要的content-type属性呢?
同步提交核心是form,这和ajax的核心是XMLhttpRequest一样,在form标签下有个属性accept-charset,该属性规定服务器处理表单数据所接受的字符集。accept-charset 属性允许您指定一系列字符集,服务器必须支持这些字符集,从而得以正确解释表单中的数据。
该属性的值是用引号包含字符集名称列表。如果可接受字符集与用户所使用的字符即不相匹配的话,浏览器可以选择忽略表单或是将该表单区别对待。此属性的默认值是 "unknown",表示表单的字符集与包含表单的文档的字符集相同。不过这个属性在ie下效果并不好,甚至有时会无效,所以如果排除开发者将编码写错的原因,而是请求方和响应方约定做这样的转化,对于字符集的转化在服务端做是最好的。
Form表单还有一个重要的属性对http请求有很大的影响那就是enctype,它包含三个取值,如下所示:
值 |
描述 |
application/x-www-form-urlencoded |
在发送前编码所有字符(默认) |
multipart/form-data |
不对字符编码。 在使用包含文件上传控件的表单时,必须使用该值。 |
text/plain |
空格转换为 "+" 加号,但不对特殊字符编码。 |
第一行的值在我本文上篇里我曾提到过多次,现在我们知道它在浏览器里的作用了,form表单enctype默认的属性值,它让浏览器在请求发送前编码请求的数据,为什么浏览器要编码请求数据了?根本原因是因为电脑是美国人发明的,美国人使用英文字母给电脑输入数据,而像我们中国人则是使用方块字汉字,而汉字对于电脑而言是一种特殊字符,不能使用几个字母组合简单表示的,放眼全球,这种问题不仅仅发生在汉字上,韩文,日本等等非字母的语言都会存在同样问题,但是网络传输的数据都是二进制,为了让请求数据在服务端能被正确还原服务端就必须有一个能容纳世界上所有语言的字符集例如UTF-8或则特有的字符集GBK规范进行转义,这就是application/x-www-form-urlencoded的作用,他告诉浏览器你要对请求数据转码。Form表单还能传输文件,文件是需要特别的解析器来解析,例如图片要图片解析器,word文档要office解析,但是这些文件本身存储就是二进制,而且是特有的二进制,因此网络传输时候最好能保持原样,如是乎有了multipart/form-data属性,有时我们不想转码了那么就可以使用text/plain,这些参数最后都会体现在http请求头部的content-type属性里。
由此可见form表单并不那么简单,它也帮我们开发屏蔽了许多技术细节,所以当我们不使用form表单提交同步请求时候就有可能掉进这些细节陷阱里,最常见的一个问题就是使用非form提交的get请求,该get请求会传参至服务端,而这个参数里包含中文例如:websit=博客园,如果我们直接这么写,浏览器不会给我报错,但是到了服务端后我们会发现参数变成乱码,url造成的乱码是让很多初学者头疼的问题,不过javascript早就预料到了,如是它提供了三对可以对字符串编码的函数,分别是: escape,encodeURI,encodeURIComponent,相应3个解码函数:unescape,decodeURI,decodeURIComponent 。这个问题的根本就是我们提交请求失去了form的保护,我们必须手动指定form里enctype所提供的功能。关于上面三对函数,本文不做过多讲述,想了解的朋友可以问问度娘了。
上面内容是我讲同步请求时候讲掉的内容,这里补上了,由上面的内容我们可以看出同步提交对http头部的控制能力是很差的。
XMLHttprequest对http头有着强大的控制能力,它可以通过setRequestHeader(名,值)来设置http的请求头,方法getResponseHeader(名)获取指定的头,方法getReponseHeaders()获取所有响应头信息,虽然http请求头对于用户是不可见的(用户只关心http请求体的内容,因为请求才是用户需要的),但是对于开发者而言http请求头非常重要,因为http请求头是整个http的领导者,我们可以通过控制http请求头信息达到很多传统http请求所不能达到的目的。
在使用XMLHttpRequest时候如果我们不对请求头做任何修改,默认情况下浏览器会发送以下http头部信息给服务器,它们分别是:
Accept:浏览器能够处理的内容类型; Accpet-Charset:浏览器能够显示字符集; Accept-Encoding:浏览器能够处理的压缩编码; Accept-Language:浏览器当前设置的语言; Connection:浏览器和服务器之间连接类型; Cookie:当前页面设置的任何Cookie; Host:发出请求页面所在的域; Referer:发出请求的页面的url, User-Agent:浏览器的用户代理字符串。
上面九个头部信息是所有浏览器都会传送的,不过浏览器类型不同,头部信息可能还会有点差异。
虽然setRequestHeader方法可以修改http头部信息,但是如果我们修改上面9个属性中的某些属性会发现这种修改有时不顶用,这是某些浏览器出于安全考虑不让开发者轻易更改,好在setRequestHeader方法还能自定义请求头,如果我们想做一些与请求头相关的特别处理,最好是自己定义请求头。此外setRequestHeader方法使用要在open方法后,send方法前,否则会达不到效果。
接下来是ajax技术里另外一个重点了:回调函数的使用。这里有一句话:ajax的回调函数是用来处理http请求响应的,也就是说服务端给了浏览器请求结果才会调用ajax的回调函数,这个说法对吗?我觉得很多人都会认为这句话是正确的,这里我先不告诉大家答案,先讲讲关于ajax回调函数的使用,大家请看下面的代码:
xmlhttp.onreadystatechange=state_Change; function state_Change() { if (xmlhttp.readyState==4) {// 4 = "loaded" if (xmlhttp.status==200) {// 200 = OK // ...our code here... } else { alert("Problem retrieving XML data"); } } }
XMLHttpRequest的onreadystatechange方法用来接收回调函数,回调函数里我们要判断readyState的取值为4,status的取值为200时候,我们就认为得到了成功的响应,否则就是没有得到成功的响应。我曾经做过一个尝试,就是去掉xmlhttp.readyState==4代码,我发现else里的alert会执行多次,点完了这些对话框后请求任然可以正确处理,这说明了一个问题:onreadystatechange方法是和readystate值有关的,每当readystate值发生改变的时候onreadystatechange就会被执行,其实从onreadystatechange的名字就能知道这点。
XMLHttpRequest里readystate的取值如下所示:
取值0:未初始化即还没调用open方法;
取值1:启动即已经调用了open方法,但还没有调用send方法;
取值2:发送即调用send方法,但还没有收到响应;
取值3:接收即已经接收到部分响应数据;
取值4:完成即接收到了全部响应数据。
由此可见ajax的回调函数并不是只有当获得响应时候才会触发,一个ajax请求用于记录回调函数的onreadystatechange方法会被调用5遍,所以我们必须使用readystate==4来表明响应成功。此外从readystate状态值我们也可以发现onreadystatechange属性的赋值要放在open方法之前,否则你的写法是存在隐患的。
了解了这个后,我们看看jQuery的ajax方法的使用,jQuery的ajax方法是对浏览器底层ajax操作的封装,该方法屏蔽了浏览器实现之间的差异,同时还提供了一些方法,这些方法是对底层ajax操作的上层封装,他能让那些没有深入理解原始ajax原理的人以方便,这里我们着重看下jQuery里ajax里回调函数的使用,下面是我摘抄jQuery文档里的内容:
如果要处理$.ajax()得到的数据,则需要使用回调函数。beforeSend、error、dataFilter、success、complete。 beforeSend 在发送请求之前调用,并且传入一个XMLHttpRequest作为参数。 error 在请求出错时调用。传入XMLHttpRequest对象,描述错误类型的字符串以及一个异常对象(如果有的话) dataFilter 在请求成功之后调用。传入返回的数据以及"dataType"参数的值。并且必须返回新的数据(可能是处理过的)传递给success回调函数。 success 当请求之后调用。传入返回后的数据,以及包含成功代码的字符串。 complete 当请求完成之后调用这个函数,无论成功或失败。传入XMLHttpRequest对象,以及一个包含成功或错误代码的字符串。
上面的success方法就是readystate值为4,status的值为200的情形了(实际情况下status值会更多点,后面再聊这个问题),complete是当请求完成后调用的函数,这个方法其实和status值无关了,只和readystate值为4有关,beforeSend则是readystate状态为1时候使用的,error方法就比较复杂点了,它是一个综合错误的考虑,反正就是不成功就成error了,dataFilter是根据用户定义的响应数据的类型(dataType)对返回数据做相应的转化,一般我们很少使用该函数,都是依赖jQuery内部完成,不过这个回调是在success方法之前执行。
dataFilter回调函数可以对响应数据进行处理,这里就引出了一个新问题,我们通过XMLHttpRequest获取的响应数据有哪些类型,XMLHttpRequest有两个属性用来存储响应数据,一个是responseText:文本类型的响应数据,其实就是字符串,responseXML:XML文档类型的响应数据,该属性只有在http的响应头是text/xml或者application/xml时候浏览器会帮我们转化,如果不是上述类型,该属性则为null。
我们使用jQuery的ajax方法时候,通过设定dataType属性,我们会获得更加丰富的响应数据,下面是我摘抄的jQuery文档的内容,如下所示:
dataType:预期服务器返回的数据类型。如果不指定,jQuery 将自动根据 HTTP 包 MIME 信息来智能判断,比如XML MIME类型就被识别为XML。在1.4中,JSON就会生成一个JavaScript对象,而script则会执行这个脚本。随后服务器端返回的数据会根据这个值解析后,传递给回调函数。可用值: "xml": 返回 XML 文档,可用 jQuery 处理。 "html": 返回纯文本 HTML 信息;包含的script标签会在插入dom时执行。 "script": 返回纯文本 JavaScript 代码。不会自动缓存结果。除非设置了"cache"参数。'''注意:'''在远程请求时(不在同一个域下),所有POST请求都将转为GET请求。(因为将使用DOM的script标签来加载) "json": 返回 JSON 数据 。 "jsonp": JSONP 格式。使用 JSONP 形式调用函数时,如 "myurl?callback=?" jQuery 将自动替换 ? 为正确的函数名,以执行回调函数。 "text": 返回纯文本字符串
上面的大部分操作是jQuery帮我们完成的,如果我们定义了dataFilter回调函数,该函数是在类型转化前执行的,就是说dataFilter操作的数据是ajax返回的原始数据。
当readystate值为3时候,我们会做到一些意想不到的功能,readystate为3的状态很特别,这个时候服务器已经给出了响应,但是响应数据并没有全部发送给浏览器,不过此时你操作responseText会发现里面是有数据的,但是这个数据不全,我曾见过使用XMLHttpRequest这个属性有人做出了两个效果,具体如下所述:
效果一:虽然我们开发时候都是尽全力让请求和响应时间变得更短,但是某些请求处理是快不起来的例如:上传文件或者浏览器接收超大的数据,如果场景是后者即用户请求数据量很小,但是接收数据很大,接收时间很长的时候,我们一般都会做一个等待请求处理的效果,如果这个效果能有精确的进度条,那么给用户的体验是非常好的,http响应报文头里有个属性就是content-length,这个属性告诉浏览器整个响应的大小,一般浏览器收到响应,就算这个响应还只有部分数据,这个响应头也会先发送,之所以提前,是让服务器知道数据接收到多少就表明接收完毕,所以在readystate为3时候我们可以通过计算收到响应的大小和content-length做比较就能知道响应接收到了那个阶段了,这样进度条的进度就会很精确。
效果二:这个效果是我很多年前无意中看见一个国外网站发现的,可惜我现在不记得网址了,网页的作者也是研究ajax的特点的,他通过ajax请求了一篇文章,然后这个文章会在页面一行行的显示出来,因为这个效果使用setTimeout函数在浏览器也能做,所以这位作者就控制服务端数据返回时间,浏览器收到部分数据后就马上在页面上显示出来。
不过上述操作有个特点,就是开发者都会定义一个回调函数,使用setTimeout函数轮询这个回调函数,setTimeout和回调函数使用是在onreadystatechange回调函数里面,而且接收数据的字符串要用公共变量存储,所以这个效果做起来还是有点难度的,原因就是readystate状态为3时候回调函数只能调用一次。
在jQuery的ajax方法里有一个属性ifModified,文档的解释是:
(默认: false) 仅在服务器数据改变时获取新数据。使用 HTTP 包 Last-Modified 头信息判断。在jQuery 1.4中,他也会检查服务器指定的'etag'来确定数据没有被修改过。
这个参数说明ajax技术可以对http的缓存进行操作,所以有些javascript的书里讲解ajax时候表示请求成功会使用两个响应码,一个就是常见的200,另一个是304,304的响应码表明该请求的响应要从浏览器缓存里取,不需要服务器直接返回了。
为了说清楚ajax操作缓存,那我们首先要知道浏览器的缓存机制,在web前端优化使用浏览器缓存是一个很重要的法则,那么如何让浏览器能缓存我们的响应呢?一般有两种方式:
方式一:通过expires、cache-control来控制,前者是指定缓存到期的具体日期,后者是指定缓存多久后过期例如10年后过期,我们第一次请求某个资源成功响应码为200,这个时候响应头里会返回一个last-modified回来,如果该资源我们设定了缓存,那么第二次请求在请求头里会发送if-Modified-Since属性,该属性的值就是第一次返回的last-modified,服务器接收后发现资源没有改变,服务器就会返回304响应码回来,304响应码就是告诉浏览器响应结果从浏览器缓存里取。
方式二:使用etag,etag是另一种缓存失效机制,使用etag的服务器会给指定资源计算一个hash值(一般用md5算的),第一次请求时候服务器会将这个值返回给浏览器,第二次请求浏览器会将这个值发送给服务器,服务器通过计算资源的hash值和传来的值作比较后,如果值相等那么就表明资源没有更改,这时服务器也会给一个304的响应码给浏览器,让浏览器在自己缓存里去找对应的响应。
不管是那种方式,如果请求已经被缓存,服务器都会返回304响应码,使用浏览器缓存不代表浏览器不发送请求,是否取浏览器缓存是服务器来指挥的,只不过304请求不会返回响应体,只有响应头,而且请求方式是get,这个请求非常小,速度非常快。
同样当ajax收到了304响应码时候,ajax就会从浏览器缓存里取响应结果,这个操作XMLHttpRequest会自己帮我们做掉。
不过关于304的情况有一点一定要注意,能被浏览器缓存的请求只有get请求即原始请求一定要是get请求,post请求时没法被浏览器缓存的。
XMLHttpRequest非常强大,我这里的强大是想说它除了可以做异步请求还能做同步请求,前面我讲了onreadystatechange,讲了readystate,其实这两个东西应该可以说专属于异步请求,为什么呢?在以前的文章里我讲到nodejs的作者选择javascript语言原因是因为javascript的回调机制,因为有回调就可以让单线程的javascript做出异步的效果,既然是同步提交,定义回调函数还有意义吗?所以如果我们使用XMLHttpquest做同步请求,我们的代码可以这么写:
xmlhttp.open("GET",url,false); xmlhttp.send(null); if (xmlhttp.status==200) {// 200 = OK // ...our code here... } else { alert("Problem retrieving XML data"); }
代码会按顺序执行的,如果请求很慢,照样阻塞页面执行。
看到这里估计有人会有疑问的,ajax的同步提交有价值吗?这还不如用form来做同步提交,这个要具体问题具体看了,我在前面讲到传统的同步提交对http的精确控制能力很差,如果某些同步请求我们需要更多的http控制那么使用ajax比较好,此外传统的同步请求响应要么是覆盖了原页面,要么就是用个新窗体接收响应,但是同步的ajax对响应的处理灵活度更大,相比之下ajax同步请求优势更大些。
在jQuery的ajax里有一个属性就是timeout,可以为ajax请求设置超时时间,在我前面讲解ajax技术时候我们会发现XMLHttpRequest没有一个属性可以设置这个超时时间属性,其实在ie8包括ie8以上的版本里,XMLHttpRequest拥有timeout属性,其他浏览器没有,但是XMLHttpRequest拥有一个abort方法,这个方法可以取消异步请求,记住是异步请求,同步操作这个方法无效,无效原因是同步提交下我们是没机会执行该方法的。有了这个方法我们就可以模拟ajax请求超时。
哦,写了8000多字了,看来本篇也无法写完我这个主题,由此可见页面的提交数据方式的学问是很大的。