第三十六课:如何书写一个完整的ajax模块

本课主要教大家如何书写一个完整的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("