本课主要教大家如何书写一个完整的ajax模块,讲解的代码主要跟ajax有关,而jQuery的ajax模块添加了Deferred异步编程的机制,因此对ajax的理解难度增大,还是忽略掉。但是我要讲解的代码跟jQuery的ajax模块思路是一样的,只是没有加入Deferred异步编程的思想,这样更有利于大家理解ajax的原理。
$.ajax = function(opts){ //大家如果用过jQuery的ajax,应该记得$.ajax({url:...,data:....,type:'POST',success:function(){}}),就可以进行一次ajax请求,这里的ajax方法也是一样,接收一个json对象。
if(!opts || !opts.url){
$.error("传入的参数必须为json对象,并且此对象要有url属性");
}
opts = setOptions(opts); //处理用户传入的参数,比如:把type属性值大写化,把data的数据json对象转换成字符串格式等。
var dummyXHR = new $.XMLHttpRequest(opts); //创建一个xhr
"complete success error".replace(/\S+/g, function(match){ //match = complete,success,error
if(typeof opts[match] =="function") { //如果传入的json对象中有此回调方法
dummyXHR.bind(name, opts[name]); //就给此xhr绑定此回调方法
delete opts[name];
}
})
if(opts.contentType){ //如果有设置请求内容类型的字段,就设置
dummyXHR.setRequestHeader("Content-Type", opts.contentType);
}
for(var i in opts.headers){ //如果传入了请求头的字段集合,就设置
dummyXHR.setRequestHeader(i, opts.headers[i]);
}
if(opts.async && opts.timeout){ //如果是异步请求,并且有超时字段
dummyXHR.timeoutID = setTimeout(function(){
dummyXHR.abort();
}, opts.timeout); // 超时后,将执行回调方法,把请求中断
}
dummyXHR.request(); //发送请求
return dummyXHR;
}
$.XMLHttpRequest = function(opts){
this.readyState = 0;
this.options = opts;
this._events = {};
this.requestHeaders:{}
}
$.XMLHttpRequest.prototype = {
constructor: $.XMLHttpRequest,
bind: function(type,callback){
var listeners = this._events[type]; //如果此类型的事件已经绑定过事件回调函数,那么就直接添加到数组中就行了
if(listeners){
listeners.push(callback);
}else{
this._events[type] = [callback];
}
return this;
},
setRequestHeader:function(name,value){
this.requestHeaders[name] = value;
return this;
},
request:function(){
var opts = this.options;
var xhr = this.xhr = new $.xhr(); //这里上一课已经讲了它的兼容性写法,因此这里不再书写
if(opts.async){ //如果是异步请求,需要添加事件监听函数
if(xhr.onerror === null){ //如果浏览器支持最新的xhr的接口,不支持的话,这里是undefined。
var self = this;
xhr.onload = xhr.onerror = function(e){
this.readyState = 4; //强制把状态变成4,兼容IE9+,IE浏览器可能会出现3,或4的情况,因此这里强制设置,兼容处理。
self.respond(); //请求完成后,执行回调方法
}
}else{
xhr.onreadystatechange = function(){
self.respond();
}
}
}
if(opts.crossDomain && !("withCredentials" in xhr)){
$.error("本浏览器不支持跨域");
}
if(opts.username){ //调用xhr对象的open方法,打开连接,这里如果是get请求,在setOptions方法中,已经把data中的数据添加到url后面了。
xhr.open(opts.type,opts.url,opts.async,opts.username,opts.password);
}else{
xhr.open(opts.type,opts.url,opts.async);
}
for(var i in this.requestHeaders){
xhr.setRequestHeader(i,this.requestHeaders[i]); //设置真正的xhr对象的请求头
}
xhr.send(opts.data || null); //如果是post请求,这里就会有data数据,如果是get请求,这里就没有data属性,返回undefined,因此send(null)。
},
respond : function(forceAbort){
var xhr = this.xhr;
if(!xhr) return; //onreadystatechange会执行多次,因此通过这个变量来判断是否已经执行过了。
try{
var completed = xhr.readyState ===4; //状态为4时,就代表请求完成
if(completed || forceAbort){ //如果超时,就会强制取消请求
xhr.onerror = xhr.onload = xhr.onreadystatechange = null;
if(forceAbort){
xhr.abort();
}else{
var status = xhr.status;
this.responseText = xhr.responseText;
try{
var xml = xhr.responseXML; //以防返回的xml是一个不正规的xml,浏览器解析时会生成一个DOMException对象,访问时,会抛错。
}catch(e){}
if(xml && xml.documentElement){ //如果是xml文档
this.responseXML = xml;
}
try{
var statusText = xhr.statusText; //跨域情况下,火狐访问它会抛错
}catch(e){
statusText = "火狐访问错误";
}
this.dispatch(status,statusText);
}
}
}catch(e){ //如果网络出现问题,访问xhr的属性,在火狐下会抛错。
this.dispatch(500,e+"");
}
},
abort:function(){
this.respond(true);
return this;
},
dispatch:function(status,statusText){
this.readyState = 4;
var eventType = "error";
if(status >=200 && status < 300 || status ===304 ||status ===1223||status ===0){ //status=204,代表请求成功,但是没有内容返回。
eventType = "success";
if(status == 204 ||status ===1223||status ===0 ){
statusText = "noContent";
}else if(status == 304){
statusText = "noModified"
}
else{
var dataType = this.xhr.getResponseHeader("Content-Type") || "text"; //得到数据的类型
try{
this.response = $.ajaxConverters[dataType].call(this,this.responseText,this.responseXML); //处理不同数据
}catch(e){
eventType = "error"; //如果数据解析出错
statusText = "parsererror:"+e;
}
}
}
this.status = status;
this.statusText = statusText;
if(this.timeoutID){ //清除定时器
clearTimeout(this.timeoutID);
delete this.timeoutID;
}
if(eventType === "success"){
this.fire(eventType, this, statusText , this.response); //如果请求成功,就触发成功的回调函数
}else{
this.fire(eventType, this, statusText);
}
this.fire("complete", this, statusText); //不管成功或者失败,只要请求完成后,都会调用complete回调方法。
delete this.xhr;
},
fire:function(type){
var listeners = this._events[type] || [];
if(listeners.length){ //如果有此类型的回调方法
var args = [].slice.call(arguments);
for(var i=0,callback;callback = listeners[i++];){
callback.apply(window,args);
}
}
}
}
$.ajaxConverters = {
text:function(text){
return text || "";
},
xml:function(text,xml){
return xml != undefined ? xml : $.parseXML(text);
},
html:function(text){
return $.parseHTML(text);
},
json:function(text){
return $.parseJSON(text);
},
script:function(text){
return $.parseJS(text);
}
}
jsonp原理,请前端开发人员必须去看,很容易理解,但是非常重要。面试必问,而且还有一个问题,也是面试官非常喜欢问的,就是解析一个url的方法。一般进入方法里面,需要一个正则来匹配这个url是否是一个正确的url,写出这个正则,就基本上可以得80分了。
当我们要给url后面添加查询字符串时,我们可以用url + (url.test(/\?/) ? "&" : "?") + name + "=" +value; //这里没有考虑有hash的情况,如果url有?,就代表它本身有查询字符串,那么只要在后面添加&name=value就行了。如果没有,就需要在url添加?name=value。
最后,我们来讲一下,上一节课留下的问题,如何模拟老版本浏览器进行FormData的ajax请求。
请看源代码:
function request = function(opts){
var form = opts.form; //form指向的是页面上的form元素
var ID = "iframe-upload";
var iframe = createIframe(ID); //创建一个新的id=ID,name =ID的iframe,并添加到页面中。但是这个iframe在页面中是隐藏的,不会显示在页面上
var backups = { //先把form元素的这些属性值保存起来,因为提交form表时,需要重写这些属性
target:form.target ||"",
action:form.action||"",
enctype:form.enctype,
method:form.method
};
var fields = opts.data ? addDataToForm(form, opts.data) : []; //如果同时还需要提交其他数据,那么需要把这些数据放到form元素中。
form.target = ID; //以防提交时,刷新当前页面,现在只会刷新隐藏的iframe。
form.action = opts.url;
form.method = "POST"; //必须指定method与enctype,不然在Firefox下会报错。同时,如果form中包含文件域()时,如果缺少method="POST",以及enctype = "multipart/form-data",文件将不会被发送给url。
form.enctype = "multipart/form-data"; //form元素的enctype属性值,1:application/x-www-form-urlencoded 在发送前,编码所有字符(post请求默认就是此值)。2:text/plain 不对特殊字符编码。3:multipart/form-data 不对字符编码,在使用包含文件上传控件的表单时,必须使用该值。
$.bind(iframe,"load",function(e){ //绑定iframe的load事件,当form表提交后,会触发iframe的刷新,这时就会触发iframe的load事件
respond(e,iframe);
});
form.submit(); //提交form表
for(var i in backups){
form[i] = backups[i]; //恢复form元素的那些属性值
}
fields.forEach(function(input){
form.removeChild(input); //移除之前添加的隐藏的input元素
})
}
function createIframe(ID){
var iframe = $.parseHTML("").firstChild;
return (document.body || document.documentElement).insertBefore(iframe); //把新创建的iframe添加到页面的最后面(第二个参数不写或写成null),并返回这个iframe。
}
function addDataToForm(form,data){
var el,ret=[];
for(var d in data){
el = document.createElement("input");
el.type = "hidden"; //隐藏的input
el.name = d;
el.value = data[d];
form.appendChild(el); //添加到form元素中
ret.push(el);
}
return ret;
}
function respond(e,iframe){
var node = iframe;
var responseText;
if(e && e.type == "load"){
var doc = node.contentWindow.document; //取得iframe中的document对象,这里的document对象就是url返回的数据
responseText = doc;
if(doc.body){ //如果返回的数据存在body,说明返回的不是xml。
responseText = doc.body.innerHTML;
}
dispatch(200,"success",responseText); //请求成功,执行回调函数
$.unbind(node,"load",function(e){ //绑定iframe的load事件,当form表提交后,会触发iframe的刷新,这时就会触发iframe的load事件
respond(e,iframe);
});
setTimeout(function(){
node.parentNode.removeChild(node); //移除页面上的iframe。
});
}
}
这一课难度还是蛮大的,ajax这一章节也已经讲完,下一课,将讲解动画引擎。
加油!