dojo/request模塊整體架構解析

 總體說明

  做前端當然少不了ajax的使用,使用dojo的童鞋都知道dojo是基於模塊化管理的前端框架,其中對ajax的處理位於dojo/request模塊。一般情況下我們使用ajax請求只需要引入dojo/request模塊,然後按照文檔的說明制定參數即可。實際上dojo在這一模塊的處理中抽象了很多概念:

  • 平臺偵探器:dojo/request/default
  • 請求分發器:dojo/request/registry
  • 全局通知器:dojo/request/notify
  • 數據傳輸器:dojo/request/xhr dojo/request/script dojo/request/iframe dojo/request/node
  • 數據轉化器:dojo/request/handlers

  處理器的總體關係如下圖所示:

  

  正是這些概念使得dojo在ajax請求上能夠提供強大的擴展性和簡捷的接口。

 

  Provider

  請求傳輸器被稱爲Provider,dojo框架自身提供了以下4個provider

  • dojo/request/xhr 提供跨瀏覽器的XMLHttpRequest,在瀏覽器端它被作爲默認的provider
  • dojo/request/node 用於node平臺的異步請求,在node下唄當做默認的provider。dojo是可以運行在node平臺下的,當然需要做一些配置,這是另一篇文章的主要內容
  • dojo/request/iframe 不刷新瀏覽器傳輸form表單,在文件上傳時經常用到
  • dojo/request/script 常以jsonp方式來進行跨域請求

  所有dojo自帶的Provider返回一個promise對象,其中有一個不在標準規範內的屬性:response。該屬性是一個標準promise對象,該對象將一個代表服務器端響應結果的對象作爲fulfill狀態的值。這個對象有以下幾個屬性:

  關於這幾個Provider的詳細講解請繼續關注下一篇文章

  

  default

  一般情況下我們發送ajax請求時只需引入dojo/request即可,實際上這是在default中根據不同的運行平臺自動給我們提供了一個默認的provider。


define([
    'exports',
    'require',
    '../has'
], function(exports, require, has){
    //讀取dojoConfig中的配置信息
    var defId = has('config-requestProvider'),
        platformId;
    
    //根據不同平臺選擇不同的provider
    if(has('host-browser') || has('host-webworker')){
        platformId = './xhr';
    }else if(has('host-node')){
        platformId = './node';
    /* TODO:
    }else if(has('host-rhino')){
        platformId = './rhino';
   */
    }

    if(!defId){
        defId = platformId;
    }

    exports.getPlatformDefaultId = function(){
        return platformId;
    };
    //作爲插件使用,是跟參數選擇provider
    exports.load = function(id, parentRequire, loaded, config){
        require([id == 'platform' ? platformId : defId], function(provider){
            loaded(provider);
        });
    };
});

  代碼中關於exports跟require模塊的說明請看我的上一篇博客:require、module、exports dojo中的三個特殊模塊標識

  上述內容關於load的函數的出現,意味着該模塊可以作爲「插件」使用。dojo插件主要用於加載一些非AMD的資源,比如css、html。dojo中常用的插件有5個:

  • dojo/domReady 
  • dojo/text 用於加載靜態資源文件
  • dojo/i18n 加載國際化語言文件
  • dojo/has 用於特性檢測
  • dojo/require

  當在define或require中一個模塊引用包含一個!,dojo的加載器會自動將這個模塊引用字符串在!處分開,左邊部分作爲一個模塊引用對待,右邊部分,等待左邊模塊加載完畢後交由模塊的load方法處理;


exports.load = function(id, parentRequire, loaded, config){
        require([id == 'platform' ? platformId : defId], function(provider){
            loaded(provider);
        });
    };

  關於load函數的幾個參數:

  • id:代表!右側部分
  • parentRequire:上下文智能的require請求器
  • loaded:id模塊加載完畢後的回調
  • config:猜測是dojo/_base/config

  後三個參數是dojo自己來處理,一般情況下我們不需要關心。

  關於插件還要在說幾句:
  dojo中不會像緩存module一樣緩存插件所加載的資源比如:我們可以多次引用同一個module,但是這個module只會加載一次,這是AMD規範所強制規定的。但是我如果多次dojo/text!./template.html這個template.html會被加載多次。

  

  notify

  notify是全局的ajax事件通知器,負責全局範圍內的ajax事件監聽,有類似於jquery中ajaxStart、ajaxComplete的事件。


define(['../Evented', '../_base/lang', './util'], function(Evented, lang, util){
    // module:
    //        dojo/request/notify
    // summary:
    //        Global notification API for dojo/request. Notifications will
    //        only be emitted if this module is required.
    //
    //        | require('dojo/request', 'dojo/request/notify',
    //        |     function(request, notify){
    //        |         notify('load', function(response){
    //        |             if(response.url === 'someUrl.html'){
    //        |                 console.log('Loaded!');
    //        |             }
    //        |         });
    //        |         request.get('someUrl.html');
    //        |     }
    //        | );

    var pubCount = 0,
        slice = [].slice;
    //實例化dojo/Evented對象,負責分發事件
    var hub = lang.mixin(new Evented, {
        onsend: function(data){
            if(!pubCount){
                this.emit('start');
            }
            pubCount++;
        },
        _onload: function(data){
            this.emit('done', data);
        },
        _onerror: function(data){
            this.emit('done', data);
        },
        _ondone: function(data){
            if(--pubCount <= 0){
                pubCount = 0;
                this.emit('stop');
            }
        },
        emit: function(type, event){
            var result = Evented.prototype.emit.apply(this, arguments);

            // After all event handlers have run, run _on* handler
            //運行完標準事件處理函數後,再來運行本身的私有函數。
            //load和error事件處理完後觸發done事件
            //done事件處理完畢後,再來運行本身的_ondone函數,然後觸發stop事件
            if(this['_on' + type]){
                this['_on' + type].apply(this, slice.call(arguments, 1));
            }
            return result;
        }
    });

    function notify(type, listener){
        // summary:
        //        Register a listener to be notified when an event
        //        in dojo/request happens.
        // type: String?
        //        The event to listen for. Events emitted: "start", "send",
        //        "load", "error", "done", "stop".
        // listener: Function?
        //        A callback to be run when an event happens.
        // returns:
        //        A signal object that can be used to cancel the listener.
        //        If remove() is called on this signal object, it will
        //        stop the listener from being executed.
        return hub.on(type, listener);
    }
    notify.emit = function(type, event, cancel){
        return hub.emit(type, event, cancel);
    };

    // Attach notify to dojo/request/util to avoid
    // try{ require('./notify'); }catch(e){}
    return util.notify = notify;
});

  最後的一句:util.notify= notify; util將notify與provider關聯起來。

 

  registry

  該模塊可以在不同的情況下使用不同的provider;匹配的條件可以是正則表達式、字符串或者函數。通過registry可以根據不同的條件註冊不同的provider。


require(["dojo/request/registry", "dojo/Deferred"], function(request, Deferred){
  request.register("crossdomain/ie", xdrProvider);

  var xdrProvider = function(url, options){
    var def = new Deferred();
    xdr = new XDomainRequest();
    if (xdr) {
      xdr.onerror = function(){
        def.reject('error');
      };
      xdr.ontimeout = function(){
        def.reject('timeout');
      };
      xdr.onprogress = function(){
        def.progress('progress');
      };
      xdr.onload = function(res){
        def.resolve(res);
      };
      xdr.timeout = 6000;
      xdr.open(options.method, url);
      xdr.send(serilize(options.data));
    } else {
        def.reject("Failed to create");
    }
    
    return def;
  }
  
  request.get("crossdomain/ie/getData", {
    method: "get",
    data:{id:'ie9'}
  }).then(function(text){
    // Do something with the response
  });

});

  以下便是registry的源碼:


define([
    'require',
    '../_base/array',
    './default!platform',//想想notify中的load函數
    './util'
], function(require, array, fallbackProvider, util){
    var providers = [];

    function request(url, options){
        var matchers = providers.slice(0),//作用類似clone
            i = 0,
            matcher;

        while(matcher=matchers[i++]){
            if(matcher(url, options)){//匹配provider
                return matcher.request.call(null, url, options);
            }
        }
        //fallbackProvider由default根據不同平臺注入默認的provider
        return fallbackProvider.apply(null, arguments);
    }

    function createMatcher(match, provider){
        var matcher;

        if(provider){
            if(match.test){
                // RegExp
                matcher = function(url){
                    return match.test(url);
                };
            }else if(match.apply && match.call){
                matcher = function(){
                    return match.apply(null, arguments);
                };
            }else{
                matcher = function(url){
                    return url === match;
                };
            }

            matcher.request = provider;
        }else{
            // If only one argument was passed, assume it is a provider function
            // to apply unconditionally to all URLs
            matcher = function(){
                return true;
            };

            matcher.request = match;
        }

        return matcher;
    }

    request.register = function(url, provider, first){
        var matcher = createMatcher(url, provider);
        providers[(first ? 'unshift' : 'push')](matcher);

        return {
            remove: function(){
                var idx;
                if(~(idx = array.indexOf(providers, matcher))){
                    providers.splice(idx, 1);
                }
            }
        };
    };
    //這裏意味着registry也可以使用插件的寫法,作用是替換一個默認的provider
    request.load = function(id, parentRequire, loaded, config){
        if(id){
            // if there's an id, load and set the fallback provider
            require([id], function(fallback){
                fallbackProvider = fallback;//js中的詞法作用域,load中永遠能夠訪問到fallbackProvider變量。
                loaded(request);
            });
        }else{
            loaded(request);
        }
    };

    util.addCommonMethods(request);

    return request;
});


  handlers

  XMLHttpRequest對象請求成功後返回的數據格式只有text跟xml兩種,handlers根據request中指定的handleAs參數將請求成功後的數據轉化爲指定類型。與jquery中的類型轉化器作用類似。

  dojo中提供了以下三種數據轉化器:

  

  此外,handlers有跟registry類似的register方法,可以讓我們自定義數據轉化器。


require(["dojo/request/handlers", "dojo/request", "dojo/dom", "dojo/dom-construct", "dojo/json",
    "dojo/on", "dojo/domReady!"],
function(handlers, request, dom, domConst, JSON, on){
  handlers.register("custom", function(response){
    var data = JSON.parse(response.text);
    data.hello += "!";
    return data;
  });

  on(dom.byId("startButton"), "click", function(){
    domConst.place("<p>Requesting...</p>", "output");
    request("./helloworld.json", {
      handleAs: "custom"
    }).then(function(data){
      domConst.place("<p>data: <code>" + JSON.stringify(data) + "</code>", "output");
    });
  });
});

  如果您看完本篇文章感覺不錯,請點擊一下下方的推薦來支持一下博主,謝謝!