RainZhai / rainzhai.github.com

宅鱼
http://rainzhai.github.io
Apache License 2.0
2 stars 0 forks source link

AMD loader实现 #13

Open RainZhai opened 7 years ago

RainZhai commented 7 years ago

AMD相关文档: https://github.com/amdjs/amdjs-api/wiki/AMD-(%E4%B8%AD%E6%96%87%E7%89%88)

(function(global) {
    global = global || window;
    /**模块对象用来存放所有模块 */
    modules = {};
    /**正在加载的模块 */
    loadings = [];
    /**已经加载完成的模块 */
    loadedJs = [];

    /**
     * 获取模块方法
     * @param deps{Array} 依赖模块数组
     * @param callback{Function} 回调方法
     * @param parent{Object} 父对象
     */
    global.require = function(deps, callback, parent) {
        var id = parent || "amdloader" + 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 (/^amdloader/.test(id)) {
                rel = global.require.parsedConfig.baseUrl;
            } else {
                var parts = parent.split('/');
                parts.pop();
                rel = parts.join('/');
            }
            return getModuleUrl(dep, rel);
        });

        /**定义模块并将模块加入到modules对象表 */
        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();
        }
    };

    /**
     * 模块加载的配置
     * @param config{Object} 配置对象
     */
    global.require.config = function(config) {
        this.parsedConfig = {};
        /**baseUrl :所有模块的查找根路径,基于baseUrl 来获取当前的url,并拼装完整的url地址 */
        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 = {};
        /**paths :paths映射那些不直接放置于baseUrl下的模块名.也可以拼装baseUrl下的模块 */
        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中的内容没有通过define()来声明依赖关系、设置模块.通过define定义模块并由exports或item.exports 将其输出 */
        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);
    }

    /**
     * 模块定义
     * @param id{String} 模块id
     * @param deps{Array} 依赖模块数组
     * @param callback{Function} 回调方法
     */
    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);
        }
        //请求依赖模块,当前id作为父模块
        require(deps, callback, id);
    };

    global.define.amd = {}; //AMD规范

    /**
     * 获取路径
     * @param base{String} baseurl路径
     * @param target{String} 依赖模块数组
     * @return url{String} 拼接的最终url
     */
    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 if (tts[0] === '') {
                return bts.join('/');
            } else {
                return bts.join('/') + '/' + tts.join('/');
            }
        }
    };

    function isDefined(v) {
        return v !== null && v !== undefined;
    }

    /**
     * 获取模块路径,基于relative
     * @param moduleId{String} 模块id
     * @param relative{String} 基本url
     * @return url{String} 拼接的最终url
     */
    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]);
        //若package存在先通过package路径拼装url
        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);
        }
    }

    /**加载js文件
     * @param url{String} js路径
     */
    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)

调用:

    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': "jquery"
        }
    });
    require([
        'bbb',
        'aaa.bbb.ccc',
        'ccc', 
        'something'
    ], function(aaabbbccc) {
        console.log('simple loader');
        console.log(arguments);
    });