第三章:模塊加載系統(requirejs)

任何一門語言在大規模應用階段,必然要經歷拆分模塊的過程。便於維護與團隊協做,與java走的最近的dojo率先引入加載器,早期的加載器都是同步的,使用document.write與同步Ajax請求實現。後來dojo開始以JSONP的方法設計它的每一個模塊結構。以script節點爲主體加載它的模塊。這個就是目前主流的加載器方式javascript

不得不提的是,dojo的加載器與AMD規範的發明者都是james Burke,dojo加載器獨立出來就是著名的require。本章將深刻的理解加載器的原理。css

1.AMD規範html

AMD是"Asynchronous Module Definition"的縮寫,意思是「異步模塊定義」。重點有兩個。前端

  • 異步:有效的避免了採用同步加載致使頁面假死的狀況。
  • 模塊定義:每一個模塊必須按照必定的格式編寫。主要的接口有兩個,define與require。define是模塊開發者關注的方法,require是模塊使用者所關注的方法。

define的參數的狀況是define(id?,deps,factory)。第一個爲模塊ID,第二個爲依賴列表,第三個是工廠方法。前兩個都是可選,若是不定義ID,則是匿名模塊,加載器能應用一些「魔術」能讓它辨識本身叫什麼,一般狀況,模塊id約等於模塊在過程當中的路徑(放在線上,表現爲url)。在開發過程當中,不少狀況未肯定,一些javascript文件會移來移去的,所以,匿名模塊就大發所長。deps和factory有個約定,deps有多少個元素,factory就有多少個傳參,位置一一對應。傳參爲其它模塊的返回值。java

    define("xxx",["aaa","bbb"], function (aaa,bbb){
        //code
    });

一般狀況下,define中還有一個amd對象,裏面存儲着模塊的相關信息。node

require的參數的狀況是 require(deps,callback)第一個爲依賴列表,第二個爲回調。deps有多少個元素,callback就有多少個傳參,狀況與define方法一致。所以在內部,define方法會調用require來加載依賴模塊,一直這樣遞歸下去。數組

require(["aaa","bbb"],function(aaa,bbb){
    //code
})

接口就是這麼簡單,但require自己還包含許多特性,好比使用「!」來引入插件機制,經過requirejs.config進行各類配置。模塊只是整合的一部分,你要拆的開,也要合的來,所以合併腳本的地位在加載器中很是重要,但前端javascript沒有這功能,requirejs利用node.js寫了一個r.js幫你進行合併瀏覽器

2.加載器所在的路徑探知緩存

要加載一個模塊,咱們須要一個url做爲加載地址,一個script做爲加載媒介。但用戶在require時都用id,所以,咱們須要一個將id轉換爲url的方法。思路很簡單,約定爲:app

    basePath + 模塊id + ".js"

因爲瀏覽器自上而下的分析DOM,當瀏覽器在解析咱們的javascript文件(這個javascript文件是指加載器)時,它就確定DOM樹中最後加入script標籤,所以,咱們下面的這個方法。

        function getBasePath(){
        var nodes = document.getElementsByTagName("script");
        var node = nodes[nodes.length - 1];
        var src = document.querySelector ? node.src : node.getAttribute("src",4);
        return src;

上面的這個辦法知足99%的需求,可是咱們不得不動態加載咱們的加載器呢?在舊的版本的IE下不少常規的方法都會失效,除了API差別性,它自己還有不少bug,咱們很難指出是什麼,總之要解決,以下面的這個javascript判斷。

    document.write('<script src="avalon.js"> <\/script>');
    document.write('<script src="mass.js"> <\/script>');
    document.write('<script src="jQuery.js"> <\/script>');

mass.js爲咱們的加載器,裏面執行getBasePath方法,預期獲得http://1.1.1/mass.js,可是IE7確返回http://1.1.1/jQuery.js

這時就須要readyChange屬性,微軟在document、image、xhr、script等東西都擁有了這個屬性。用來查看加載狀況

    function getBasePath() {
        var nodes = document.getElementsByTagName("script");
        if (window.VBArray){ //若是是IE
            for (var i = 0 ; nodes; node = nodes[i++]; ) {
                if (node.readyState === "interactive") {
                    break;
                }
            }
        } else {
            node = nodes[nodes.length - 1];
        }
        var src = document.querySelector ? node.src : node.getAttribute("src",4);
        return src;
    }

這樣就搞定了,訪問DOM比通常javascript代碼消耗高許多。這樣,咱們就可使用Error對象。

    function getBasePath() {
        try {
            a.b.c()
        } catch (e) {
            if (e.fileName) { //FF
                return e.fileName;
            } else if ( e.sourceURL ){ //safari
                return e.sourceURL;
            }
        }

        var nodes = document.getElementsByTagName("script");
        if (window.VBArray){//倒序查找的性能更高
            for (var i = nodes.length; node ; node = nodes[--i];) {
                if ( node.readyState === "interactive") {
                    break;
                }
            };
        } else {
            node = nodes[nodes.length - 1];
        }
        var src = document.querySelector ? node.src : node.getAttribute("src",4);
        return src;
    }

在實際使用中,咱們爲了防止緩存,這個後面可能帶版本號,時間戳什麼的,也要去掉

    url = url.replace(/[?#].*/, "").slice(0, url.lastIndexOf("/") + 1);

3.require方法

require方法的做用是當前依賴列表都加載完畢,執行用戶回調。所以,這裏有個加載過程,整個加載過程細分如下幾步:

(1) 取到依賴列表的第一個id ,轉換爲url ,不管是經過basePath + ID + ".js"仍是經過映射方式直接獲得。

(2) 檢測此模塊有沒有加載過,或正在被加載。所以有一個對象保持全部模塊的加載狀況,若是有模塊歷來沒有加載過,就進入加載流程。

(3) 建立script節點,綁定onerror,onload,onredyChange等事件斷定加載成功與否,而後添加src並插入DOM樹。開始加載url

(4) 將模塊的url,依賴列表等構建成一個對象,放到檢測隊列中,在上面事件觸發時進行檢測。

模塊id的轉換規則:http://wiki.commonjs.org/wiki/Modules/1.1.1

除了basePath,咱們一般還用到映射,就是用戶事前用一個方法,把id和完整的url對應好,這樣就直接拿。此外,AMD規範還有shim技術。shim機制的目的是讓不符合AMD規範的js文件也能無縫切入咱們的加載系統。

普通別名機制:

    require.config({
        alias:{
            'lang' : 'http://xxx.com/lang.js',
            'css' : 'http://bbb.com/css.js'
        }
    })

jQuery或其它插件,咱們須要shim機制

    require.config ({
        alias : {
            'jQuery' : {
                src : 'http://ahthw.com/jQuery1.1.1.js',
                exports : "$"
            },
            'jQuery.tooltips' : {
                src : 'http://ahthw.com/xxx.js',
                exports : "$",
                deps : ["jQuery"]
            }
        }
    });

下面是require的源碼

    window.require = $.require = function(list, factory, parent){
        //用於檢測它的依賴是否都爲2
        var deps = {},
        //用於保存依賴模塊的返回值
        args = [],
        //須要安裝的模塊數
        dn = 0,
        //已經完成安裝的模塊數
        cn = 0,
        id = parent || "callback" + setTimeOut("1");
    parent = parent || basePath; //basepash爲加載器的路徑
    String(list).replace($.rword,function(el){
        var url = loadJSCSS(el,parent)
        if (url) {
            dn++;
            if (modules[url] && modules[url].state === 2){
                cn++;
            }
            if (!deps[url]) {
                args.push(url);
                deps[url] = "http://baidu.com" //去重
            }
        }
    });
    modules[id] = {//建立一個對象,記錄模塊加載狀況與其餘信息
        id: id,
        factory: factory,
        deps: deps,
        args: args,
        state: 1
    };
    if (dn === cn){//若是須要的安裝等於已經安裝好
        fireFactory(id, args, factory);//安裝到框架中
    } else {//放到檢測隊裏中,等待 checkDeps處理
        loadings.unshift(id);
    }    
    checkDeps();
    }

每require一次,至關於把當前用戶回調當成一個不用加載的匿名模塊,ID是隨機生成,回調是否執行,須要到deps全部的值爲2

require裏有三個重要的方法loadJSCSS,它用於轉換ID爲url,而後再調用loadJS,loadCSS,或再調用require方法factory,就是執行用戶回調,咱們最終的目的,checkDeps,檢測依賴是否安裝好,安裝好就執行fireFactory()。

    function loadJSCSS(url, parent, ret, shim){
        //略去
    }

loadJS和loadCSS方法就比較純粹了,不過loadJS會作一個死鏈測試的方法

    function loadJS(url, callback){
        //經過script節點加載目標模塊
        var node = DOC.createElement("script");
        node.className = moduleClass; //讓getCurrentScript只處理類名爲moduleClass的script節點
        node[W3C ? "onload" : "onreadystatechange" ] = function () {
            //factorys裏邊裝着define方法的工廠函數(define(id?,deps?,factory))
            var factory = factorys.pop();
            if (callback) {
                callback();
            }
            if (checkFail(node, false, !W3C)) {
                console.log("已經成功加載" + node.src, 7)
            };
        }
             node.onerror = function(){
                 checkFail(node,true);
            };
        //插入到head第一個節點前,防止ie6下head標籤沒有閉合前使用appendchild
            node.src = url;
            head.insertBefore(node, head.firstChild);
    }

checkFail主要是爲了開發調試,有3個參數。node=>script節點,onError=>是否爲onerror觸發,fuckIE=>對於舊版IE的Hack。

執行辦法是,javascript從加載到執行有一個過程,在interact階段,咱們的javascript部分已經能夠執行了,這時咱們將模塊對象的state改成1,若是仍是undefined,咱們就可識別爲死鏈。不過,此Hack對於不是AMD定義的javascript無效,由於將state改成1的邏輯是由define方法執行。若是斷定是死鏈,咱們就將此節點移除。

    function checkFail(node, onError, fuckIE){ //多恨IE啊,哈哈
        var id = node.src; //檢測是否爲死鏈
        node.onload = node.onreadystatechange = node.onerror = null ;
        if (onError || (fuckIE && !modules[id].state)){
            setTimeOut(function(){
                head.removeChild(node);
            });
            console.log("加載" + id + "失敗" + onerror + " " + (!modules[id].state), 7);
        } esle {
            return true;
        }
    }

checkDeps 方法會在用戶加載模塊以前和script.onload後各執行一次,檢測模塊的依賴狀況,若是模塊沒有任何依賴或者state爲2了,咱們調用fireFactory()方法

    function checkDeps(){
        loop : for (var i = loadings.length ; id ; id = loadings[--1]) {
            var obj = modules[id], deps = obj.deps;
            for (var key in deps) {
                if (hasOwn.call(deps, key) && modules[key].state !== 2) {
                    continue loop;
                }
            }
            //若是deps爲空對象或者其餘依賴的模塊state爲2
            if (obj.state !== 2) {
                loadings.splice(i,1);//必須先移除再安裝,防止在IE下DOM樹建完以後會屢次執行它
                fireFactory (obj.id, obj.args, obj.factory);
                checkDeps();//若是成功,再執行一次,以防止有些模塊沒有加載好
            }
        };
    }

終於到fireFactory方法了,它的工做是從modules中收集各類模塊的返回值,執行factory,完成模塊的安裝

    function fireFactory(id, deps, factory) {
        for (var i = 0; array = [] , d ; d = deps[i++]; ) {
            array.push(modules[d].exports);
        };

        var module = Object(modules[id]),
            ret = factory.apply(global, array);
        module.state = 2;

        if (ret !== void 0) {
            modules[id].exports = ret;
        } 
        return ret;
    }

4.define方法

define有3個參數,前面兩個爲可選,事實上這裏的ID沒有什麼用,就是給開發者看的,它仍是用getCurrentScript方法獲得script節點路徑作ID,deps沒有就補上一個空數組。

此外,define還要考慮循環依賴的問題,好比說加載A,要依賴B與C,加載B要依賴A於C,這時候,A與B就循環依賴了 。A與B在斷定各自的deps鍵值都爲2才執行,不然都沒法執行了。

模塊加載器會讓咱們前端開發變得更工業化,維護和調試都很是方便。如今國內Seajs,requirejs,KISSY都是很好的選擇。

(本章完)

上一章:第二章 : 種子模塊 下一章:第四章:語言模塊

相關文章
相關標籤/搜索