AMD加载器实现笔记(三)

 上一篇文章中我们为config添加了baseUrl和packages的支持,那么这篇文章中将会看到对shim与paths的支持。

  要添加shim与paths,第一要务当然是了解他们的语义与用法。先来看shim,shim翻译成中文是“垫片”的意思。在AMD中主要用途是把不支持AMD的某些变量包装AMD模块。shim是一个哈希对象,key为包装后的模块Id,value是关于这个包装模块的一些配置,主要配置项如下:

  • deps:定义模块需要的依赖项的moduleId数组
  • exports:模块输出值
  • init:如果它的返回值不是undefined,则返回值作为‘some/thind’模块的返回值,否则以exports作为模块的返回值。

  举个例子:

  AMD加载器实现笔记(三)

  这个配置的目的是想将window.some.thing这个全局变量包装成id为‘some/thing’的模块。模块的依赖项Id为'a'、'b',输出值为some.thing但因为定义了init函数,所以最后模块的输出值变成了some.thing + 'another'。

  因为shim是要将全局变量包装成模块,所以直接用shim中的配置项来定义模块即可。上例中的配置最终将转化为:


1 define('some/thing', ['a', 'b'], function(a, b) {
2    var initResult = shimItem.init(a, b);
3    return initResult === undefined ? shimItem.exports : initResult;  
4 });

但前面我的加载器中,define只支持匿名模块,现在我们让它来支持显示定义模块Id:


global.define = function(id, deps, callback) {
        //加上moduleId的支持
        if (typeof id !== "string" && arguments.length === 2) {
            callback = deps;
            deps = id;
            id = "";
        }
        var id = id || getCurrentScript();
        if (modules[id]) {
            console.error('multiple define module: ' + id);
        }
        
        require(deps, callback, id);
    };

完成后我们在require.config函数中加入对shim的支持。


//shim 要放在最后处理
        if (config.shim) {
            for (var p in config.shim) {
                var item = config.shim[p];
                define(p, item.deps, function() {
                    var exports;
                    if (item.init) {
                        exports = item.init.apply(item, arguments);
                    }
                    
                    return exports ? exports : item.exports;
                });
            }
        }

于此同时,require中也要改一下,现在已经不能一股脑的将dep都转化为绝对路径了。


// dep为非绝对路径形式,而modules的key仍然需要绝对路径
        deps = deps.map(function(dep) {
            if (modules[dep]) { //jquery 
                return dep;
            } else if (dep in global.require.parsedConfig.paths) {
                return dep;
            }
            var rel = "";
            if (/^Bodhi/.test(id)) {
                rel = global.require.parsedConfig.baseUrl;
            } else {
                var parts = parent.split('/');
                parts.pop();
                rel = parts.join('/');
            }
            return getModuleUrl(dep, rel);
        });

 到此,shim已经完美支持了。

 

  下面轮到了paths,paths也是一个对象,格式为{模块Id:模块所在路径}。当其他模块引用该模块时,该模块的加载地址使用paths中所配置的地址,这个地址可以是绝对的,也可以是相对路径(相对于baseUrl)。首先要在require.config函数中解析path路径


this.parsedConfig.paths = {};
        if (config.paths) {
            for (var p in config.paths) {
                this.parsedConfig.paths[p] = /^http(s)?/.test(config.paths[p]) ? config.paths[p] : getRoute(burl, config.paths[p]);
            }
        }

然后当依赖模块Id在paths中有配置时,那就需要从paths中拿到加载地址,所以需要修改loadJs中代码


function loadJS(url) {
        var script = document.createElement('script');
        script.type = "text/javascript";
        //判断模块是否在paths中定义了路径
        script.src = (url in global.require.parsedConfig.paths ? global.require.parsedConfig.paths[url] : 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);
    };

  同时如果dep在paths中有配置,也不能将dep转化为绝对路径(require函数)

 

  这里面需要注意的是,模块的Id必须与路径所在文件中定义的模块的id保持一致。以jquery为例:


require.config({
    baseUrl: "./",
    packages: [{
        name: "more",
        location: "./more"
    }, {
        name: "mass",
        location: "../"
    }, {
        name: "wab",
        location: "../../../"
    }],
    paths: {
        'jquery': "../../Bodhi/src/roots/jquery"
    }
  });

当在paths中配置了jquery模块时,那么路径对应的文件一定要定义jquery模块,如jquery源码中用define定义jquery模块:


1 if ( typeof define === "function" && define.amd ) {
2     define( "jquery", [], function() {
3         return jQuery;
4     });
5 }

本文内容结束,目前为止loader加载器的源码为:


(function(global){
    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 = [];
        
         // dep为非绝对路径形式,而modules的key仍然需要绝对路径
        deps = deps.map(function(dep) {
            if (modules[dep]) { //jquery 
                return dep;
            } else if (dep in global.require.parsedConfig.paths) {
                return dep;
            }
            var rel = "";
            if (/^Bodhi/.test(id)) {
                rel = global.require.parsedConfig.baseUrl;
            } else {
                var parts = parent.split('/');
                parts.pop();
                rel = parts.join('/');
            }
            return getModuleUrl(dep, rel);
        });
        
        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 {
            loadings.push(id);
            checkDeps();
        }
    };
    
    global.require.config = function(config) {
        this.parsedConfig = {};
        if (config.baseUrl) {
            var currentUrl = getCurrentScript();
            var parts = currentUrl.split('/');
            parts.pop();
            var currentDir = parts.join('/');
            this.parsedConfig.baseUrl = getRoute(currentDir, config.baseUrl);
        }
        var burl = this.parsedConfig.baseUrl;
        // 得到baseUrl后,location相对baseUrl定位
        this.parsedConfig.packages = [];
        if (config.packages) {
            for (var i = 0, len = config.packages.length; i < len; i++) {
                var pck = config.packages[i];
                var cp = {
                    name: pck.name,
                    location: getRoute(burl, pck.location)
                }
                this.parsedConfig.packages.push(cp);
            }
        }
        
        
        this.parsedConfig.paths = {};
        if (config.paths) {
            for (var p in config.paths) {
                this.parsedConfig.paths[p] = /^http(s)?/.test(config.paths[p]) ? config.paths[p] : getRoute(burl, config.paths[p]);
            }
        }
        //shim 要放在最后处理
        if (config.shim) {
            for (var p in config.shim) {
                var item = config.shim[p];
                define(p, item.deps, function() {
                    var exports;
                    if (item.init) {
                        exports = item.init.apply(item, arguments);
                    }
                    
                    return exports ? exports : item.exports;
                });
            }
        }
        
        console.log(this.parsedConfig);
    }
    
    global.define = function(id, deps, callback) {
        //加上moduleId的支持
        if (typeof id !== "string" && arguments.length === 2) {
            callback = deps;
            deps = id;
            id = "";
        }
        var id = id || getCurrentScript();
        if (modules[id]) {
            console.error('multiple define module: ' + id);
        }
        
        require(deps, callback, id);
    };
    
    global.define.amd = {};//AMD规范
    
    function getRoute(base, target) {
        var bts = base.replace(/\/$/, "").split('/');  //base dir
        var tts = target.split('/'); //target parts
        while (isDefined(tts[0])) {
            if (tts[0] === '.') {
                return bts.join('/') + '/' + tts.slice(1).join('/');
            } else if (tts[0] === '..') {
                bts.pop();
                tts.shift();
            } else {
                return bts.join('/') + '/' + tts.join('/');
            }
        }
    };
    
    function isDefined(v) {
        return v !== null && v !== undefined;
    }
    
    function getModuleUrl(moduleId, relative) {
        function getPackage(nm) {
            for (var i = 0, len = require.parsedConfig.packages.length; i < len; i++) {
                var pck = require.parsedConfig.packages[i];
                if (nm === pck.name) {
                    return pck;
                }
            }
            return false;
        }
        var mts = moduleId.split('/');
        var pck = getPackage(mts[0]);
        if (pck) {
            mts.shift();
            return getRoute(pck.location, mts.join('/'));
        } else if (mts[0] === '.' || mts[0] === '..') {
            return getRoute(relative, moduleId);
        } else {
            return getRoute(require.parsedConfig.baseUrl, moduleId);
        }
    }
    
    function loadJS(url) {
        var script = document.createElement('script');
        script.type = "text/javascript";
        //判断模块是否在paths中定义了路径
        script.src = (url in global.require.parsedConfig.paths ? global.require.parsedConfig.paths[url] : 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)

测试demo:


window.something = "Bodhi";
  require.config({
    baseUrl: "./",
    packages: [{
        name: "more",
        location: "./more"
    }, {
        name: "mass",
        location: "../"
    }, {
        name: "wab",
        location: "../../../"
    }],
    shim: {
        "something": {
            "deps": ['jquery'],
            exports: 'something',
            init: function(jq, ol) {
                console.log(jq);
                return something + " in shim";
            }
        }
    },
    paths: {
        'jquery': "../../Bodhi/src/roots/jquery"
    }
  });
  require([
  'bbb',
  'aaa.bbb.ccc',
  'ccc',
  'ddd',
  'fff',
  'something'
  ], function(aaabbbccc){
    console.log('simple loader');
    console.log(arguments);
  });


上一篇:Android笔记:ViewPager和TabLayout连用时,去除ViewPager预加载


下一篇:【人生苦短,我用Python】Python免费精品课连载(2)——序列、列表、元组与字符串