學着寫一個異步模塊加載器

一年前,剛來網易實習的時候接觸了NEJ,那是第一次接觸模塊化開發,感受想出這個idea的人簡直是天才,同時也對於這種框架的實現很是好奇,慚愧的是,那時甚至連jQuery的原理都不知道。html

隨着這一年對於JS面向對象的理解有所加深,看着《JavaScript設計模式》就跟着本身動手碼碼代碼,因此這是一篇讀書筆記,並非發明創造,而且這個加載器是比較簡陋的,頗有改進空間。json

模塊的長相

模塊採用的是匿名模塊,它的js絕對路徑做爲它的唯一標識:設計模式

define([
    '{lib}dom',
    '{pro}extend'    
], function(dom, extend) {
    //TODO
})

異步加載的思路

從上面咱們能夠看出,模塊是由define函數來定義,傳入參數爲:依賴列表和回調函數,爲了實現依賴注入,要等到依賴列表的全部js加載完後再來執行回調函數。緩存

因此第一步,咱們循環遍歷依賴列表,而後依次加載列表的模塊,可想而知,在循環遍歷加載模塊的代碼的結構應該是下面這樣子的:app

//modules = ['lib/dom.js', 'js/extend.js']
var modCount = modules.length;
var params = [];  //保存依賴列表的對象
for (var i = 0, len = modules.length; i < len; i++) {
    (function(i){
        var url = modules[i];
        loadModule(url, function(module) {
            modCount--;
            params[i] = module;
            if (modCount == 0) {
                defineModule(uid, params, callback);   //uid爲該模塊絕對路徑,callback爲傳入的回調函數
            }
        })
    })(i)
}

上面的代碼只是部分代碼,可是咱們能夠很清楚地看到思路就是循環加載模塊,同時傳入一個回調,加載完成後觸發回調,回調函數裏會將modCount(模塊個數)減1,若是modCount變爲0,那麼說明就所有模塊都加載完成了,就執行defineModule函數,同時傳入所有的依賴對象。框架

異步加載觸發回調

要觸發回調,首先要知道何時js腳本何時加載完成。咱們建立一個script標籤,append進body,這樣就能夠加載js腳本,那麼何時腳本加載完成呢?dom

有的人可能立刻就想到了,當js代碼開始執行的時候就說明這個腳本加載完了。注意,只是這個腳本,不要忘記在這個腳本當中,咱們可能還依賴了其餘模塊,這樣咱們還要等待這個依賴模塊加載完它所擁有的依賴模塊列表後執行其回調函數纔算這個模塊加載完成。異步

因此這樣子咱們能夠知道最終的加載完成的標誌就是執行defineModule函數,因此在loadModule函數中,咱們須要將加載回調函數進行緩存,等待後面加載完成後執行。async

loadModule函數

//moduleCache = {} 是定義在全局的一個模塊緩存對象

function loadModule(uid, callback) {
    var _module;
    if (moduleCache[uid]) {
        _module = moduleCache[uid];
        if (_module.status == 'loaded') {
            setTimeout(callback(_module.exports), 0);
        } else {
            _module.onload.push(callback);
        }
    } else {
        moduleCache[uid] = {
            uid: uid,
            status: 'loading',
            exports: null,
            onload: [callback]
        };
        loadScript(uid);
    }
}

function loadScript(url) {
    var _script = document.createElement('script');
    _script.charset = 'utf-8';
    _script.async = true;
    _script.src = url;
    document.body.appendChild(_script);
}

上面代碼的思路是加載模塊的時候,先在緩存對象中尋找看看有沒有存在的模塊。ide

  1. 存在,那麼就看是已經加載完了仍是在加載當中,若是加載中,那麼就在其回調列表push一個新的回調。
  2. 不存在,那麼就往緩存中添加一個新的模塊,exports保存這個模塊的對象,onload保存這個模塊加載完成後的回調函數執行列表。而後添加script標籤。

defineModule函數

到這裏,咱們能夠感受到快要寫完了,可是咱們仍然沒有執行加載模塊後的回調函數,上面也交代了,模塊加載完成後總會執行defineModule函數,因此在這裏執行回調,上代碼:

function defineModule(uid, params, callback) {
    if (moduleCache[uid]) {
        var _module = moduleCache[uid];
        _module.status = 'loaded';
        _module.exports = callback ? callback.apply(_module, params) : null;
        while (fn = _module.onload.shift()) {
            fn(_module.exports);
        }
    } else {
        moduleCache[uid] = {
            uid: uid,
            status: 'loaded',
            onload: [],
            exports: callback && callback.apply(null, params)
        }
    }
}

能夠看到,定義模塊時咱們判斷是否存在,若是存在,說明這個模塊是被依賴的,因此就執行onload裏緩存的回調函數。

添添補補

上面就把功能實現了,可是仍是有很多問題的,好比依賴列表的js路徑問題,uid怎麼獲取,還有可能須要加載html文件等等,可是這些都是一些小問題,總體模塊加載器已經完成,剩下的就是修修補補,下面附上我目前的define.js文件代碼:

(function(win, doc){

    var moduleCache = {};

    var t = /(\S+)define\.js(?:\?pro=(\S+))?/.exec(getCurrentUrl()),
        lib = t[1],
        pro = t[2] || '/',
        dir = win.location.href;
    var tReg = /^\.\/|^\//;

    while (tReg.test(pro)) {
        pro = pro.replace(tReg, '')
    }

    var backCount = 0;
    tReg = /^\.\.\//;
    while (tReg.test(pro)) {
        backCount++;
        pro = pro.replace(tReg, '')
    }

    pro = backUrl(lib, backCount) + pro;


    var tplReg = /\.html$/;


    function getCurrentUrl(){
        return document.currentScript.src;
    }

    function backUrl(url, count) {
        for (var i = 0; i < count; i++) {
            url = url.replace(/[^/]+\/?$/, '');
        }
        return url;
    }

    function fixUrl(url) {
        if (tplReg.test(url)) {
            if (/^\{lib\}/.test(url)){
                return url.replace(/^\{lib\}/, lib);
            } else if (/^\{pro\}/.test(url)) {
                return url.replace(/^\{pro\}/, pro);
            } else {
                return url;
            }
        }
        return url.replace(/^\{lib\}/, lib).replace(/^\{pro\}/, pro).replace(/\.js$/g, '') + '.js';
    }

    function loadScript(url) {
        var _script = document.createElement('script');
        _script.charset = 'utf-8';
        _script.async = true;
        _script.src = fixUrl(url);
        document.body.appendChild(_script);
    }

    function defineModule(uuid, mParams, callback) {
        if (moduleCache[uuid]) {
            var _module = moduleCache[uuid];
            _module.status = 'loaded';
            _module.exports = callback ? callback.apply(_module, mParams) : null;
            while (fn = _module.onload.shift()) {
                fn(_module.exports);
            }
        } else {
            moduleCache[uuid] = {
                uuid: uuid,
                status: 'loaded',
                exports: callback && callback.apply(null, mParams),
                onload: []
            }
        }
    }


    function loadModule(uuid, callback) {
        var _module;
        if (moduleCache[uuid]) {
            _module = moduleCache[uuid];
            if (_module.status == 'loaded') {
                setTimeout(callback(_module.exports), 0);
            } else {
                _module.onload.push(callback);
            }
        } else {
            moduleCache[uuid] = {
                uuid: uuid,
                status: 'loading',
                exports: null,
                onload: [callback]
            };
            loadScript(uuid);
        }
    }


    var define = function(modules, callback) {
        
        modules = Array.isArray(modules) ? modules : [];

        for (var i = 0, len = modules.length; i < len; i++) {

            modules[i] = fixUrl(modules[i]);
        }

        var uuid = getCurrentUrl(),
            mlen = modules.length,
            mParams = [],
            i = 0,
            loadCount = 0;

        if (mlen) {
            while (i < mlen) {
                loadCount++;
                (function(i){
                    if (tplReg.test(modules[i])) {
                        loadText(modules[i], function(_json){
                            
                            var tpl = '';

                            if (_json.code == 200) {
                                tpl = _json.result;
                            }
                            loadCount--;
                            mParams[i] = tpl;
                            if (loadCount == 0) {
                                defineModule(uuid, mParams, callback);
                            }
                        })
                    } else {

                        loadModule(modules[i], function(module) {
                            loadCount--;
                            mParams[i] = module;
                            if (loadCount == 0) {
                                defineModule(uuid, mParams, callback);
                            }

                        });
                    }

                })(i);
                i++;
            }
        } else {
            defineModule(uuid, [], callback)
        }


    }


    function loadText(url, callback) {
        var xhr = new XMLHttpRequest();
        xhr.open("get", url, true);
        xhr.send(null);
        xhr.onreadystatechange = function() {
            if (xhr.readyState == 4) {
                if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
                    var code = 200;
                } else {
                    code = xhr.status;
                }
                callback({
                    code: code,
                    result: xhr.responseText
                })
            }
        }
    }


    loadScript(fixUrl('{lib}router'));

    win.define = define;

    win.gObj = {
        loadScript: loadScript,
        loadText: loadText,
        lib: lib,
        pro: pro,
        fixUrl: fixUrl
    }

})(window, document)

這個加載器目前我知道的問題有:

  1. 沒法處理循環依賴的問題,也就是a依賴b,b再依賴a,並不會報錯。

  2. 獲取js路徑函數沒有作兼容處理,在IE上並不能這麼獲取

  3. 代碼寫得比較糙,至少在路徑上處理能夠作優化

相關文章
相關標籤/搜索