JS模塊化編程之加載器原理

  世面上有好多JavaScript的加載器,好比 sea.js, require.js, yui loader, labJs...., 加載器的使用範圍是一些比較大的項目, 我的感受若是是小項目的話能夠不用,  我用過seaJSrequireJS, 在項目中用過requireJS, requireJS是符合AMD,全稱是(Asynchronous Module Definition)即異步模塊加載機制 , seaJS是符合CMD規範的加載器。javascript

  AMD__和__CMD

  AMD規範是依賴前置, CMD規範是依賴後置, AMD規範的加載器會把全部的JS中的依賴前置執行 。 CMD是懶加載, 若是JS須要這個模塊就加載, 不然就不加載, 致使的問題是符合AMD規範的加載器(requireJS), 可能第一次加載的時間會比較久, 由於他把全部依賴的JS所有一次性下載下來;php

  常識,jQuery是支持AMD規範,並不支持CMD規範,也就是說, 若是引入的是seaJS,想要使用jQuery,要用alias配置, 或者直接把 http://cdn.bootcss.com/jquery/2.1.4/jquery.js 直接引入頁面中;css

//這是jQuery源碼的最後幾行, jQuery到了1.7才支持模塊化;
// Register as a named AMD module, since jQuery can be concatenated with other
// files that may use define, but not via a proper concatenation script that
// understands anonymous AMD modules. A named AMD is safest and most robust
// way to register. Lowercase jquery is used because AMD module names are
// derived from file names, and jQuery is normally delivered in a lowercase
// file name. Do this after creating the global so that if an AMD module wants
// to call noConflict to hide this version of jQuery, it will work.
// Note that for maximum portability, libraries that are not jQuery should
// declare themselves as anonymous modules, and avoid setting a global if an
// AMD loader is present. jQuery is a special case. For more information, see
// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon
if ( typeof define === "function" && define.amd ) {
    define( "jquery", [], function() {
        return jQuery;
    });
};

   

使用方法

  好比咱們能夠這樣定義一個模塊:html

//文件所在的路徑地址爲:http://localhost:63342/module/script/dir2/1.js
define(function() {
    return "!!!!";
});

 

  也能夠這樣定義一個模塊:java

//這個文件的路徑爲http://localhost:63342/module/main.js ,並且有一個依賴, 加載器會自動去加載這個依賴, 當依賴加載完畢之後, 會把這個依賴(就是script/dir2/1.js)執行的返回值做爲這個函數的參數傳進去;
require(["script/dir2/1.js"], function(module1) {
    console.log(module1);
});
//實際上會打印出 "!!!!"

 

  通常來講,一個模塊只能寫一個define函數, define函數的傳參主要有兩種方式:node

    1:正常上能夠是一個函數;react

    2:能夠是一個數組類型依賴的列表, 和一個函數;jquery

  若是一個模塊寫了多個define會致使模塊失靈, 先定義的模塊被後定義的模塊給覆蓋了 ( 固然了, 通常咱們不那樣玩);git

  一個模塊內能夠寫多個require, 咱們能夠直接理解require爲匿名的define模塊, 一個define模塊內能夠有多個require, 並且require過的模塊會被緩存起來, 這個緩存的變量通常是在閉包內, 並且名字多數叫modules什麼的.....;github

  咱們經過加載器開發實現的模塊化開發要遵照一種規範, 規範了一個模塊爲一個JS,那麼咱們就能夠新建幾個目錄爲conroller,view, model, 也是爲了後期更好的維護解耦

  

實現一個本身的加載器

  使用的方式:

//這個模塊依賴的四個模塊,加載器會分別去加載這四個模塊;
define(["依賴0","依賴1","依賴2","依賴3"], function(依賴0,依賴1,依賴2,依賴3){

});

//返回一個空對象
define(function(){
    return {};
});

//直接把require看成是define來用就行了;
require(["依賴0","依賴1","依賴2","依賴3"], function(依賴0,依賴1,依賴2,依賴3) {
    //執行依賴0;
    依賴0(依賴1,依賴2,依賴3);
});

//這個加載器define函數和require函數的區別是,define咱們能夠傳個name做爲第一參數, 這個參數就是模塊的名字😴😠, 好吧, 無論這些了.....;

 

  如下爲加載器的結構,由於代碼量已經不多了, 因此每一函數都是必須的, 爲了避免影響全局, 把代碼放在匿名自執行函數內部:

(function() {
    定義一個局部的difine;
    var define;
    //我偷偷加了個全局變量,好調試啊;
    window.modules = {
    };
    //經過一個名字獲取絕對路徑好比傳"xx.js"會變成"http://www.mm.com/"+ baseUrl + "xx.html";
    var getUrl = function(src) {};
    //動態加載js的模塊;
    var loadScript = function(src) {};
    //獲取根路徑的方法, 通常來講咱們能夠經過config.baseUrl配置這個路徑;
    var getBasePath = function() {};
    //獲取當前正在加載的script標籤DOM節點;
    var getCurrentNode = function() {};
    //獲取當前script標籤的絕對src地址;
    var getCurrentPath = function() {};
    //加載define或者require中的依賴, 封裝了loadScript方法;
    var loadDpt = function(module) {};
    //這個是主要模塊, 完成了加載依賴, 檢測依賴等比較重要的邏輯
    var checkDps = function() {};
    定義了define這個方法
    define = function(deps, fn, name) {};
    window.define = define;
    //require是封裝了define的方法, 就是多傳了一個參數而已;
    window.require = function() {
        //若是是require的話那麼模塊的名字就是一個不重複的名字,避免和define重名;
        window.define.apply([], Array.prototype.slice.call(arguments).concat( "module|"+setTimeout(function() {},0) ));
    };
});

 

  加載器源碼實現(兼容,chrome, FF, IE6 ==>> IE11), IE11沒有了readyState屬性, 也沒有currentScript屬性,坑爹啊,  沒法獲取當前正在執行的JS路徑, 因此要用hack

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <script>
    (function() {
        var define;
        window.modules = {
        };
        var getUrl = function(src) {
            var scriptSrc = "";
            //判斷URL是不是
            // ./或者
            // /或者
            // 直接是以字符串開頭
            // 或者是以http://開頭;
            if( src.indexOf("/") === 0 || src.indexOf("./") === 0 ) {
                scriptSrc = require.config.base + src.replace(/^\//,"").replace(/^\.\//,"");
            }else if( src.indexOf("http:") === 0 ) {
                scriptSrc = src;
            }else if( src.match(/^[a-zA-Z1-9]/) ){
                scriptSrc = require.config.base + src;
            }else if(true) {
                alert("src錯誤!");
            };
            if (scriptSrc.lastIndexOf(".js") === -1) {
                scriptSrc += ".js";
            };
            return scriptSrc;
        };

        var loadScript = function(src) {
            var scriptSrc = getUrl(src);
            var sc = document.createElement("script");
            var head = document.getElementsByTagName("head")[0];
            sc.src = scriptSrc;
            sc.onload = function() {
                console.log("script tag is load, the url is : " + src);
            };
            head.appendChild( sc );
        };

        var getBasePath = function() {
            var src = getCurrentPath();
            var index = src.lastIndexOf("/");
            return  src.substring(0,index+1);
        };

        var getCurrentNode = function() {
            if(document.currentScript) return document.currentScript;
            var arrScript = document.getElementsByTagName("script");
            var len = arrScript.length;
            for(var i= 0; i<len; i++) {
                if(arrScript[i].readyState === "interactive") {
                    return arrScript[i];
                };
            };

            //IE11的特殊處理;
            var path = getCurrentPath();
            for(var i= 0; i<len; i++) {
                if(path.indexOf(arrScript[i].src)!==-1) {
                    return arrScript[i];
                };
            };
            throw new Error("getCurrentNode error");
        };

        var getCurrentPath = function() {
            var repStr = function(str) {
                return (str || "").replace(/[\&\?]{1}[\w\W]+/g,"") || "";
            };
            if(document.currentScript) return repStr(document.currentScript.src);

            //IE11沒有了readyState屬性, 也沒有currentScript屬性;
            // 參考 https://github.com/samyk/jiagra/blob/master/jiagra.js
            var stack
            try {
                a.b.c() //強制報錯,以便捕獲e.stack
            } catch (e) { //safari的錯誤對象只有line,sourceId,sourceURL
                stack = e.stack
                if (!stack && window.opera) {
                    //opera 9沒有e.stack,但有e.Backtrace,但不能直接取得,須要對e對象轉字符串進行抽取
                    stack = (String(e).match(/of linked script \S+/g) || []).join(" ")
                }
            }
            if (stack) {
                /**e.stack最後一行在全部支持的瀏覽器大體以下:
                 *chrome23:
                 * at http://113.93.50.63/data.js:4:1
                 *firefox17:
                 *@http://113.93.50.63/query.js:4
                 *opera12:http://www.oldapps.com/opera.php?system=Windows_XP
                 *@http://113.93.50.63/data.js:4
                 *IE10:
                 *  at Global code (http://113.93.50.63/data.js:4:1)
                 *  //firefox4+ 能夠用document.currentScript
                 */
                stack = stack.split(/[@ ]/g).pop() //取得最後一行,最後一個空格或@以後的部分
                stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/\s/, "") //去掉換行符
                return stack.replace(/(:\d+)?:\d+$/i, "") //去掉行號與或許存在的出錯字符起始位置
            };

            //實在不行了就走這裏;
            var node = getCurrentNode();
            //IE>=8的直接經過src能夠獲取,IE67要經過getAttriubte獲取src;
            return repStr(document.querySelector ? node.src : node.getAttribute("src", 4)) || "";

            throw new Error("getCurrentPath error!");
        };

        var loadDpt = function(module) {
            var dp = "";
            for(var p =0; p<module.dps.length; p++) {
                //獲取絕對的地址;
                var dp = getUrl(module.dps[p]);
                //若是依賴沒有加載就直接加載;
                if( !modules[dp] ) {
                    loadScript(dp);
                };
            };
        };

        //主要的模塊, 檢測全部未加載的模塊中未完成了的依賴是否加載完畢,若是加載完畢就加載模塊, 若是加載過的話,並且全部依賴的模塊加載完畢就執行該模塊
        //並且此模塊的exports爲該模塊的執行結果;
        var checkDps = function() {
            for(var key in modules ) {
                //初始化該模塊須要的參數;
                var params = [];
                var module = modules[key];
                //加載完畢就什麼都不作;
                if( module.state === "complete" ) {
                    continue;
                };
                if( module.state === "initial" ) {
                    //若是依賴沒有加載就加載依賴而且modules沒有該module就加載這個模塊;
                    loadDpt(module);
                    module.state = "loading";
                };
                if( module.state === "loading") {
                    //若是這個依賴加載完畢
                    for(var p =0; p<module.dps.length; p++) {
                        //獲取絕對的地址;
                        var dp = getUrl(module.dps[p]);
                        //若是依賴加載完成了, 並且狀態爲complete;;
                        if( modules[dp] && modules[dp].state === "complete") {
                            params.push( modules[dp].exports );
                        };
                    };
                    //若是依賴所有加載完畢,就執行;
                    if( module.dps.length === params.length ) {
                        if(typeof module.exports === "function"){
                            module.exports = module.exports.apply(modules,params);
                            module.state = "complete";
                            //每一次有一個模塊加載完畢就從新檢測modules,看看是否有未加載完畢的模塊須要加載;
                            checkDps();
                        };
                    };
                };
            };
        };

        //[],fn; fn
        define = function(deps, fn, name) {
            if(typeof deps === "function") {
                fn = deps;
                deps = [];//咱們要把數組清空;
            };

            if( typeof deps !== "object" && typeof fn !== "function") {
                alert("參數錯誤")
            };

            var src = getCurrentPath();
            //沒有依賴, 沒有加載該模塊就新建一個該模塊;
            if( deps.length===0 ) {
                modules[ src ] = {
                    name : name || src,
                    src : src,
                    dps : [],
                    exports : (typeof fn === "function")&&fn(),
                    state : "complete"
                };
                return checkDps();
            }else{
                modules[ src ] = {
                    name : name || src,
                    src : src,
                    dps : deps,
                    exports : fn,
                    state : "initial"
                };
                return checkDps();
            }
        };

        window.define = define;
        window.require = function() {
            //若是是require的話那麼模塊的名字就是一個不重複的名字,避免和define重名;
            window.define.apply([], Array.prototype.slice.call(arguments).concat( "module|"+setTimeout(function() {},0) ));
        };
        require.config = {
            base : getBasePath()
        };
        require.loadScript = loadScript;
        var loadDefaultJS = getCurrentNode().getAttribute("data-main");
        loadDefaultJS && loadScript(loadDefaultJS);
    })();
    </script>
</head>
<body>
</body>
</html>

 

  從葉大大那邊偷的一個加載器, 這個加載器有點像jQuery中延遲對象($.Deferred)有關的方法when($.when)的實現; 

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <script>
        (function () {

            //存儲已經加載好的模塊
            var moduleCache = {};

            var define = function (deps, callback) {
                var params = [];
                var depCount = 0;
                var i, len, isEmpty = false, modName;

                //獲取當前正在執行的js代碼段,這個在onLoad事件以前執行
                modName = document.currentScript && document.currentScript.id || 'REQUIRE_MAIN';

                //簡單實現,這裏未作參數檢查,只考慮數組的狀況
                if (deps.length) {
                    for (i = 0, len = deps.length; i < len; i++) {
                        (function (i) {
                            //依賴加一
                            depCount++;
                            //這塊回調很關鍵
                            loadMod(deps[i], function (param) {
                                params[i] = param;
                                depCount--;
                                if (depCount == 0) {
                                    saveModule(modName, params, callback);
                                }
                            });
                        })(i);
                    }
                } else {
                    isEmpty = true;
                }

                if (isEmpty) {
                    setTimeout(function () {
                        saveModule(modName, null, callback);
                    }, 0);
                }

            };

            //考慮最簡單邏輯便可
            var _getPathUrl = function (modName) {
                var url = modName;
                //不嚴謹
                if (url.indexOf('.js') == -1) url = url + '.js';
                return url;
            };

            //模塊加載
            var loadMod = function (modName, callback) {
                var url = _getPathUrl(modName), fs, mod;

                //若是該模塊已經被加載
                if (moduleCache[modName]) {
                    mod = moduleCache[modName];
                    if (mod.status == 'loaded') {
                        setTimeout(callback(this.params), 0);
                    } else {
                        //若是未到加載狀態直接往onLoad插入值,在依賴項加載好後會解除依賴
                        mod.onload.push(callback);
                    }
                } else {

                    /*
                     這裏重點說一下Module對象
                     status表明模塊狀態
                     onLoad事實上對應requireJS的事件回調,該模塊被引用多少次變化執行多少次回調,通知被依賴項解除依賴
                     */
                    mod = moduleCache[modName] = {
                        modName: modName,
                        status: 'loading',
                        export: null,
                        onload: [callback]
                    };

                    _script = document.createElement('script');
                    _script.id = modName;
                    _script.type = 'text/javascript';
                    _script.charset = 'utf-8';
                    _script.async = true;
                    _script.src = url;

                    //這段代碼在這個場景中意義不大,註釋了
                    //      _script.onload = function (e) {};

                    fs = document.getElementsByTagName('script')[0];
                    fs.parentNode.insertBefore(_script, fs);

                }
            };

            var saveModule = function (modName, params, callback) {
                var mod, fn;

                if (moduleCache.hasOwnProperty(modName)) {
                    mod = moduleCache[modName];
                    mod.status = 'loaded';
                    //輸出項
                    mod.export = callback ? callback(params) : null;

                    //解除父類依賴,這裏事實上使用事件監聽較好
                    while (fn = mod.onload.shift()) {
                        fn(mod.export);
                    }
                } else {
                    callback && callback.apply(window, params);
                }
            };

            window.require = define;
            window.define = define;

        })();
    </script>
</head>
<body>
</body>
</html>
View Code

 

  一個例子

   寫一個MVC的小例子,代碼簡單, 高手無視,  目錄結構以下:

  咱們把全部的事件放到了controller/mainController.js裏面,

define(["model/data","view/view0"],function(data, view) {
    var init = function() {
        var body = document.getElementsByTagName("body")[0];
        var aBtn = document.getElementsByTagName("button");
        for(var i=0; i< aBtn.length; i++) {
            aBtn[i].onclick = (function(i) {
                return function() {
                    body.appendChild( view.getView(data[i]) );
                };
            })(i);
        };
    };
    return {
        init : init
    };
});

 

  把全部的數據放到了model/data.js裏面;

define(function() {
    return [
        {name : "qihao"},
        {name : "nono"},
        {name : "hehe"},
        {name : "gege"}
    ];
})

 

  視圖的JS放到了view的目錄下,view0.js主要負責生成HTML字符串或者DOM節點;

define(function() {
    return {
        getView : function(data) {
            var frag = document.createDocumentFragment();
                frag.appendChild( document.createTextNode( data.name + " ") );
            return frag;
        }
    }
});

 

  入口是app.js,他和load.html是同級目錄:

require(["controller/mainController"],function( controller ) {
    controller.init();
});

 

  load.html這個是主界面:

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title></head>
<body>
<button>0</button>
<button>1</button>
<button>2</button>
<button>3</button>
<script src="require.js" data-main="app.js"></script>
</body>
</html>

 

  例子的源碼下載地址: 下載

       下週開始複習jQuery1.8版本源碼, 而後下下週準備寫些書籍的讀後感, 包括js設計模式,js忍者,js衆妙之門,reactjs學習。

參考:

  JS魔法堂:獲取當前腳本文件的絕對路徑

  JavaSript模塊規範 - AMD規範與CMD規範介紹

  【模塊化編程】理解requireJS-實現一個簡單的模塊加載器

 

做者: NONO
出處:http://www.cnblogs.com/diligenceday/
QQ:287101329 

相關文章
相關標籤/搜索