AMD加載器實現筆記(一)


  之前研究過AMD,也寫過一篇關於AMD的文章《以代碼愛好者角度來看AMD與CMD》。代碼我是有看過的,基本的原理也都明白,但實際動手去實現卻是沒有的。因為今年計划的dojo教程《靜靜的dojo》中,有一章節來專門講解AMD,不免要把對AMD的研究回爐一下。時隔多日,再回頭探索AMD實現原理時,竟抓耳撓腮,苦苦思索不得要領。作為開發人員,深感慚愧。故有此文,記錄我在實現一個AMD加載器時的思考總結。

 

  requireJS是所有AMD加載器中,最廣為人知的一個。目前的版本更凝聚了幾位大牛數年心血,必然不是我這個小蝦米一晚上的粗制濫造能夠比擬的,所以目前為止這篇文章里的加載器尚不能稱為AMD加載器。它並不支持AMD規范中對config的配置項,甚至不支持在define中明確地聲明模塊Id,而且它現在只支持chrome瀏覽器。它的API如下:

require([
  'http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/bbb',
  'http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa.bbb.ccc',
  'http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ccc',
  'http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ddd',
  'http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/fff'], function(aaabbbccc){
    console.log('simple loader');
    console.log(arguments);
  });
define(["http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa"],function(aaa){
    $.log("已加載ccc模塊")
    return {
        aaa: aaa,
        ccc: "ccc555"
    }
})

  是的,目前並不支持模塊解析功能,所以模塊id只能是絕對路徑。但對於一個簡易的加載器已經足夠,因為它還將會被迭代。

  

  既然AMD是JavaScript模塊化的解決方案,解決不支持模塊化的JavaScript,那么任何一個解決方案都有必要在概念層面上去定義模塊。在這里模塊的定義是,使用define函數包裝的js文件。既然是文件那首要解決加載的問題,異步無阻塞的的加載方式有多種解決方案,但最終被開發者廣泛認可的是動態創建script標簽的方式(不明白的同學去看一下這篇文章探真無阻塞加載javascript腳本技術,我們會發現很多意想不到的秘密)。

function loadJS(url) {
        var script = document.createElement('script');
        script.type = "text/javascript";
        script.src = url + '.js';
        script.onload = function() {
            //干你的活
        };
        var head = document.getElementsByTagName('head')[0];
        head.appendChild(script);
    };    

  文件加載完畢后,會立即執行define函數。define函數包裝后的模塊在加載器內部的數據結構如下:

  module:

  • id: 模塊的唯一標識
  • deps:模塊依賴項的標識數組
  • factory:依賴項全部執行完畢后所執行的函數,所有模塊的代碼都寫在這個函數里
  • export:模塊代碼執行完畢后的輸出對象
  • state:模塊的狀態(AMD是要解決JavaScript模塊依賴的問題,所以一個模塊需要等待所有依賴項完成后才能執行模塊的factory函數。我們需要state屬性標識模塊的狀態,注冊為1,執行完畢為2.)

  

  我們先從define函數開始。

global.define = function(deps, callback) {
        var id = getCurrentScript();
        if (modules[id]) {
            console.error('multiple define module: ' + id);
        }
        
        require(deps, callback, id);
    };
function getCurrentScript(base) {
        // 參考 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, "").replace(/\.js$/, ""); //去掉行號與或許存在的出錯字符起始位置
        }
        var nodes = (base ? document : head).getElementsByTagName("script"); //只在head標簽中尋找
        for (var i = nodes.length, node; node = nodes[--i]; ) {
            if ((base || node.className === moduleClass) && node.readyState === "interactive") {
                return node.className = node.src;
            }
        }
    };
getCurrentScript

  我們的define僅支持匿名模塊,所以第一件事便是需要一個模塊id。根據這個id我們需要能夠找出對應的Js文件。這里我們利用了Chrome的ReferenceError實例的stack屬性。強制瀏覽器報錯,獲取error的stack屬性,通過正則表達式匹配出文件的絕對路徑。 依賴的模塊的加載只需加載一次即可,禁止多次加載,所以遇到重復加載情況需要報錯。注冊模塊與加載依賴項的工作交給了require函數來處理。

 

  require函數是這里的大頭,接下來我們便去揭開它神秘面紗。

//module: id, state, factory, result, deps;
    global.require = function(deps, callback, parent){
        var id = parent || "Bodhi" + Date.now();
        var cn = 0, dn = deps.length;
        var args = [];
        
        var module = {
            id: id,
            deps: deps,
            factory: callback,
            state: 1,
            result: null
        };
        modules[id] = module;
        
        deps.forEach(function(dep) {
            if (modules[dep] && modules[dep].state === 2) {
                cn++
                args.push(modules[dep].result);
            } else if (!(modules[dep] && modules[dep].state === 1) && loadedJs.indexOf(dep) === -1) {
                loadJS(dep);
                loadedJs.push(dep);
            }
        });
        if (cn === dn) {
            callFactory(module);
        } else {
            //loadJS(id);// require只是用來加載其他模塊的
            loadings.push(id);
            checkDeps();
        }
    };

  因為define將責任推給了require,所以require的首要任務便是注冊模塊。JavaScript對於hash結構有着原生的支持,原生的對象{}做模塊倉庫最適合不過了。

  接下來就是處理依賴項,如果模塊的依賴項並未被加載,那就去加載它;另外記錄下已加載的依賴模塊數量。

  如果依賴模塊被執行完畢,那就去執行模塊的factory函數;如果依賴項沒有執行完畢,那就把模塊id放入加載隊列中,並執行依賴檢查。

 

  加載模塊的工作交給了loadJs函數:

function loadJS(url) {
        var script = document.createElement('script');
        script.type = "text/javascript";
        script.src = url + '.js';
        script.onload = function() {
            var module = modules[url];
            if (module && isReady(module) && loadings.indexOf(url) > -1) {
                callFactory(module);
            }
            checkDeps();
        };
        var head = document.getElementsByTagName('head')[0];
        head.appendChild(script);
    };

  無論模塊的依賴關系是多么復雜,當所有的依賴關系被確定后,必然有一個最后被等待的模塊。這就好比武俠小說中,每個殺陣都有陣眼,只要破去陣眼就能破陣。我們稱這最后被等待的模塊為陣眼模塊。當陣眼模塊被執行完畢后,整個依賴網便被盤活,一層層的回歸似的,執行factory函數。

   而如何判斷一個模塊是陣眼模塊呢?我們以deps為0作為依據。放在isRedy函數中。

function isReady(m) {
        var deps = m.deps;
        var allReady = deps.every(function(dep) {
            return modules[dep] && isReady(modules[dep]) && modules[dep].state === 2;
        })
        if (deps.length === 0 || allReady) {
            return true;
        }
    };

  而盤活的契機放在script的onload函數中。一個script元素的生命周期為:

  創建元素-》加載腳本文件-》解析腳本文件(執行js代碼)-》onload事件-》銷毀

  所以如果onload中模塊是陣眼模塊,或者依賴模塊已被全部加載完畢,則執行factory函數。然后循環檢查依賴,一層一層的盤活其他依賴網。

script.onload = function() {
            var module = modules[url];
            if (module && isReady(module) && loadings.indexOf(url) > -1) {
                callFactory(module);
            }
            checkDeps();
        };

 

 

  整個加載器代碼如下:

(function(global){
    global.$ = {
        log: function(m) {
            console.log(m);
        }
    };
    global = global || window;
    modules = {};
    loadings = [];
    loadedJs = [];
    //module: id, state, factory, result, deps;
    global.require = function(deps, callback, parent){
        var id = parent || "Bodhi" + Date.now();
        var cn = 0, dn = deps.length;
        var args = [];
        
        var module = {
            id: id,
            deps: deps,
            factory: callback,
            state: 1,
            result: null
        };
        modules[id] = module;
        
        deps.forEach(function(dep) {
            if (modules[dep] && modules[dep].state === 2) {
                cn++
                args.push(modules[dep].result);
            } else if (!(modules[dep] && modules[dep].state === 1) && loadedJs.indexOf(dep) === -1) {
                loadJS(dep);
                loadedJs.push(dep);
            }
        });
        if (cn === dn) {
            callFactory(module);
        } else {
            //loadJS(id);// require只是用來加載其他模塊的
            loadings.push(id);
            checkDeps();
        }
    };
    
    global.define = function(deps, callback) {
        var id = getCurrentScript();
        if (modules[id]) {
            console.error('multiple define module: ' + id);
        }
        
        require(deps, callback, id);
    };
    
    function loadJS(url) {
        var script = document.createElement('script');
        script.type = "text/javascript";
        script.src = url + '.js';
        script.onload = function() {
            var module = modules[url];
            if (module && isReady(module) && loadings.indexOf(url) > -1) {
                callFactory(module);
            }
            checkDeps();
        };
        var head = document.getElementsByTagName('head')[0];
        head.appendChild(script);
    };
    
    function checkDeps() {
        for (var p in modules) {
            var module = modules[p];
            if (isReady(module) && loadings.indexOf(module.id) > -1) {
                callFactory(module);
                checkDeps(); // 如果成功,在執行一次,防止有些模塊就差這次模塊沒有成功
            }
        }
    };
    
    function isReady(m) {
        var deps = m.deps;
        var allReady = deps.every(function(dep) {
            return modules[dep] && isReady(modules[dep]) && modules[dep].state === 2;
        })
        if (deps.length === 0 || allReady) {
            return true;
        }
    };
    
    function callFactory(m) {
        var args = [];
        for (var i = 0, len = m.deps.length; i < len; i++) {
            args.push(modules[m.deps[i]].result);
        }
        m.result = m.factory.apply(window, args);
        m.state = 2;
        
        var idx = loadings.indexOf(m.id);
        if (idx > -1) {
            loadings.splice(idx, 1);
        }
    };
    
    function getCurrentScript(base) {
        // 參考 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, "").replace(/\.js$/, ""); //去掉行號與或許存在的出錯字符起始位置
        }
        var nodes = (base ? document : head).getElementsByTagName("script"); //只在head標簽中尋找
        for (var i = nodes.length, node; node = nodes[--i]; ) {
            if ((base || node.className === moduleClass) && node.readyState === "interactive") {
                return node.className = node.src;
            }
        }
    };
})(window)
View Code

  測試代碼:

<!DOCTYPE HTML>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <meta http-equiv="X-UA-Compatible" content="IE=EDGE" />
    <title>Web AppBuilder for ArcGIS</title>
    <link rel="shortcut icon" href="builder/images/shortcut.png">
  </head>
  <body class="claro">
  <script src="./loader.js"></script>
  <script>
  require([
  'http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/bbb',
  'http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa.bbb.ccc',
  'http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ccc',
  'http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ddd',
  'http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/fff'], function(aaabbbccc){
    console.log('simple loader');
    console.log(arguments);
  });
  </script>
  </body>
</html>
define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa",
"http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ccc"],function(a, c){
    console.log("已加載bbb模塊", 7)
    return {
        aaa: a,
        ccc: c.ccc,
        bbb: "bbb"
    }
})
bbb
define([], function(){
    console.log("已加載aaa.bbb.ccc模塊", 7)
    return "aaa.bbb.ccc";
});
aaa.bbb.ccc
define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa"],function(aaa){
    $.log("已加載ccc模塊")
    return {
        aaa: aaa,
        ccc: "ccc555"
    }
})
ccc
define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa",
"http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/bbb",
"http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ccc",
"http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/fff"],function(a,b,c,f){
    $.log("已加載ddd模塊", 7);
    return {
        bbb: b,
        ddd: "ddd",
        length: arguments.length
    }
})
ddd
define(['http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/more/ggg'], function(g){
    $.log("已加載fff模塊")
    return {
        ggg: g,
        fff: "fff"
    }
})
fff
define([], function(){
    console.log("已加載aaa模塊", 7)
    return "aaa"
});
aaa
define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/more/ggg"],function(ret){
    $.log("已加載eee模塊",7)
    return {
        eee: "eee",
        aaa: ret.aaa,
        ggg: ret.ggg
    }
})
eee
define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa"],function(a){
    $.log("已加載ggg模塊",7)
    return {
        aaa: a,
        ggg:"ggg"
    }
})
ggg

  執行結果如下:

已加載aaa模塊 7
loader.js:4 已加載ggg模塊
loader.js:4 已加載fff模塊
aaa.bbb.ccc.js:2 已加載aaa.bbb.ccc模塊 7
loader.js:4 已加載ccc模塊
bbb.js:3 已加載bbb模塊 7
loader.js:4 已加載ddd模塊
index.html:19 simple loader
index.html:20 Arguments[5]

  下一篇文章將會為我們的加載器加上模塊路徑解析功能,到時候我們便不用書寫如此丑陋的模塊id了。

  

  如果您覺得這篇文章對您有幫助,請不吝點擊右下方推薦~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM