• 第三十六课:如何书写一个完整的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中包含文件域(<input type=file>)时,如果缺少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("<iframe "+"id='" + ID + "'"+ " name='"+ ID + "'" + " style='position:absolute;left:-9999px;top:-9999px;' />").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这一章节也已经讲完,下一课,将讲解动画引擎。

    加油!

  • 相关阅读:
    【译】StackExchange.Redis 中文文档(十)性能分析
    【译】StackExchange.Redis 中文文档(九)服务器相关命令
    【译】StackExchange.Redis 中文文档(八)流
    【译】StackExchange.Redis 中文文档(七)推送/订阅消息顺序
    【译】StackExchange.Redis 中文文档(六)事件
    【译】StackExchange.Redis 中文文档(五)事务
    查看供应商2086报表
    创建内部供应商
    创建客户前台配置
    创建客户后台配置-spro
  • 原文地址:https://www.cnblogs.com/chaojidan/p/4203175.html
Copyright © 2020-2023  润新知