第三十五课:Ajax详解
一个完整的Ajax请求:
var xhr = new (self.XMLHttpRequest || ActiveXObject)("Microsoft.XMLHTTP"); //new XMLHttpRequest()传入参数不影响。
xhr.onreadystatechange = function(){ //请求状态改变就会执行这个函数,状态有0,1,2,3,4五个状态。
if(this.readyState === 4 && this.status === 200){
alert(this.responseText);
}
}
xhr.open("post","url",true); //第三个参数如果为true,就代表异步请求,如果为false就代表是同步。
xhr.setRequestHeader("",""); //设置请求头
xhr.send("key=val&key2=val2"); //发送数据
大家可能知道,在new ActiveXObject()时,针对IE不同版本的浏览器,传入的参数也不一样。根据IE官方建议,我们只要检测这四个参数:"Msxml2.XMLHTTP.6.0","Msxml2.XMLHTTP.3.0","Msxml2.XMLHTTP",Microsoft.XMLHTTP。其中"Microsoft.XMLHTTP"是针对IE5以及以前版本的。
为了达到兼容性,我们需要这样做:
var xhr = (function(){
var fns = [
function(){return new XMLHttpRequest();},
function(){return new ActiveXObject("Msxml2.XMLHTTP");},
function(){return new ActiveXObject("Msxml2.XMLHTTP.6.0");},
function(){return new ActiveXObject("Msxml2.XMLHTTP.3.0");},
function(){return new ActiveXObject("Microsoft.XMLHTTP");} //这里兼容IE5,现在的程序都不需要兼容IE5了,因此可以去掉。
], xhrTemp;
for(var i =0,len = fns.length ; i< len;i++){
try{
fns[i](); //调用每一个创建xhr对象的函数,如果成功,就把生成xhr对象的函数返回给xhr变量。
xhrTemp = fns[i];
break;
}catch(){}
}
return xhrTemp;
})();
还有一部分人可能知道,其实IE6,7已经支持XMLHttpRequest对象了,但是为什么还需要用ActiveXObject对象呢,因为IE6,7的XMLHttpRequest对象有兼容性问题,比如:IE7下的xhr对象不支持本地file协议,会出现拒绝访问,需要倒退到ActiveXObject对象来操作本地file协议的功能。而且它们的xhr对象不是一个javascript对象,也就是说在IE6,7下,你通过var xhr = new XMLHttpRequest();生成的xhr对象不是js对象,而是ActiveXObject对象。
总结以上,我们可以在我们框架中,写出以下的最终版本:
window.$ = {};
var s = ["XMLHttpRequest", "ActiveXObject('Msxml2.XMLHTTP.6.0')", "ActiveXObject('Msxml2.XMLHTTP.3.0')", "ActiveXObject('Msxml2.XMLHTTP')"];
if(!"1"[0]){ //IE6,7下,"1"[0]返回undefined,其他浏览器返回"1"。
s[0] = location.protocol === "file:" ? "!" : s[0]; //如果是本地协议的操作,那么就覆盖XMLHttpRequest,解决IE6,7兼容性问题。
}
for(var i =0, xhr; xhr = s[i++];){
try{
if(eval("new " +xhr)){ //判断是否能够new出一个xhr对象,这里的eval可以把字符串解析成js代码执行
$.xhr = new Function("return new " + xhr); //新建一个Function实例对象,传入的参数是一个字符串。如果xhr="XMLHttpRequest",这段代码实际上会变成这样:function(){ return new XMLHttpRequest();}
break;
}
}
catch(){}
}
xhr对象的事件回调函数的绑定与状态分析
针对xhr对象的事件回调函数,我们一般使用onreadysatechange。但是XMLHttpRequest2(XMLHttpRequest新版本)添加了多个事件回调函数。如下:
loadstart:在请求开始时触发
progress:在请求发送数据以及接收数据期间,在服务器指定的时间,间隔触发
abort:在请求被取消时触发,例如:在调用了xhr.abort()方法时。
error:在请求失败时触发
load:在请求成功时触发
timeout:超时触发
loadend:在请求完成时触发,无论是成功还是失败
对于xhr请求的状态,我们一般通过判断xhr.status的值。
2xx状态与304我们都可以看成是成功状态。但是有一些兼容性问题需要处理:IE(非原生的xhr对象,也就是使用ActiveXObject实现的xhr)中,会将204设置为1223,Opera会在取得204后将status设置为0。因此,成功状态的写法如下:
if((xhr.status >=200 && xhr.status < 300) || xhr.status ==304 || xhr.status ==1223 || xhr.status ==0)
还有一个特例:在Firefox下,本地使用XMLHttpRequest,成功返回时,status等于0。由于很少在本地发请求,因此很多框架都没有对这个进行处理。
xhr发送请求和数据
open方法有5个参数,open(method,url,async,username,password)
method代表请求的HTTP方法,值包括:GET,POST,PUT,DELETE.HEAD,OPTION等。有些浏览器还支持你自定义method。
url是请求的地址,浏览器对这个url会进行同源安全策略,也就是会要求这个URL与包含脚本的文件具有相同的域名,端口,协议。在GET请求下,我们需要把传给url的数据,放到url?的后面,只能是字符串。
async指示请求使用的是异步还是同步,如果这个参数是false,请求是同步的,那么只要在send方法后,取xhr的响应数据就行了。如果这个参数是true或省略,请求是异步的,那么就需要在open方法之前在xhr对象上添加一个onreadystatechange事件回调函数。
username,password参数是可选的,为url所需的授权提供认证资格。
发送数据时,我们使用的是send方法,get方法一般传入null(在url?后面已经传入数据了),而post请求传入你要发给此url的数据。
早期的xhr对象,send方法中只能传入字符串,现代的xhr可以传FormData,document,ArrayBuffer,Blob。
FormData是一个什么样的对象呢,大家都知道,我们点击按钮提交表单时,浏览器会自动将表单的所有disabled为false的input,textarea,select,button元素的name与value抽取出来,变成一个querystring字符串。但是,我们在做ajax请求时,浏览器不会自动将表单的所有这些元素进行这样的处理,因此,我们需要用代码手动来实现这样的功能。jQuery中的serialize方法就是实现这样功能的一个方法。而现代浏览器的FormData对象也可以实现这样的功能。举个例子:
var formdata = new FormData();
formdata.append("name", "chaojidan");
formdata.append("age", 26);
xhr.send(formdata);
FormData也可以这样用:
var formobj = document.getElementById("form"); //得到页面上form元素的对象。
var formdata = formobj.getFormData(); //得到此form表单中所有disabled为false的input,textarea,select,button元素的name与value。
xhr.send(formdata);
FormData还可以这样用:
var formobj = document.getElementById("form"); //得到页面上form元素的对象。
var formdata =new FormData(formobj); //得到此form表单中所有disabled为false的input,textarea,select,button元素的name与value。
xhr.send(formdata);
如果send的是一个document,我们需要把这个document赋值为一个xml对象,然后send(document)。
至于ArrayBuffer和Blob对象,大家可以去看js高级程序编程,里面说的很详细。
xhr接收数据
早期的xhr对象拥有两种接收数据的属性,xhr.responseText对应解码后的字符串(默认解码为utf-8),xhr.responseXML对应一个XML文档。IE还支持xhr.responseBody对应未解码的二进制数据。现在流行json数据的传输,因此服务器会通过responseText传json格式的字符串给前端,而我们前端需要对这个json格式的字符串进行解析,解析成js对象。至于如何解析,大家可以去看ajax hacks这本书。
至于后端返回什么类型的数据,我们可以通过xhr.getResponseHeader("Content-Type");
XMLHttpRequest2新增了responseType和response属性。
responseType,在发送请求前,根据你的数据需要,可以将xhr.responseType设置为text,arraybuffer,blob或document。忽略此设置,默认将响应设为text。
在请求完成后,xhr.response的属性值为DOMString或ArrayBuffer或Blob或Document,具体取决于responseType的设置。
xhr还有一个overrideMimeType方法,比如:xhr.overrideMimeType("text/plain; charset = x-user-defined"),此段代码的意思是,重写了默认的MIME类型,强制浏览器将该响应当成纯文本文件来对待,并且使用一个用户自定义的字符集(这样就是告诉浏览器,不要去解析数据,直接返回未处理过的字节码)。
XMLHttpRequest实现文件上传
假设页面上有一个id为upload的input(type = file),还有一个id为progress用于显示进度的SPAN元素。
window.addEventListener("load", function(){
var el = document.querySelector("#upload");
var progress = document.getElementById("#progress");
el.addEventListener("change",function(){
var file = this.files[0]; //点击input,会弹出一个选择文件的框,用户选择好文件后,点击确定按钮,这时就会触发input元素的change事件,并且input元素有一个files属性,此属性值就是用户要上传的文件数组。input.files[1]代表第二个文件。
if(file){
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress",function(){ //当用ajax请求发送文件时,会触发xhr.upload的progress事件
var done = e.position || e.loaded, total = e.totalSize || e.total; //兼容性写法
progress.innerHTML = (Math.floor(done/total *1000)/10) + "%"; //用百分比的形式显示,文件已经发送了百分之多少?
});
xhr.addEventListener("load",function(){ //当ajax请求完成后,会触发load事件。
progress.innerHTML = "上传成功";
});
xhr.open("put", "/upload", true); //打开这个请求,异步请求
xhr.setRequestHeader("X-File-Name", encodeURIComponent(file.fileName || file.name)); //把文件的名字放到请求头的X-File-Name字段中,需要编码
xhr.setRequestHeader("Content-Type", "application/octet-stream"); //设置ajax请求的数据类型
xhr.send(file); //发送文件。
}
});
})
如果文件太大,我们可以用文件对象的slice方法对文件进行切割,然后分块上传。比如:上面例子中的file,我们可以调用var file1 = file.slice(0,1024),上传file1,然后再var file2 = file.slice(1024,file.size),上传file2。file.size可以其实就是得到file的文件大小。
那么,如何在老版本IE下上传文件,由于我们无法序列化二进制文件(上传文件其实就是二进制数据的上传),只好通过传统的form提交方法实现,为了防止提交后,刷新本页面,我们需要创建一个临时的iframe,具体实现方案,我们将在下一课代码讲解。
加油!