AMD加载器实现笔记(四)

简介:

 继续这一系列的内容,到目前为止除了AMD规范中config的map、config参数外,我们已经全部支持其他属性了。这一篇文章中,我们来为增加对map的支持。同样问题,想要增加map的支持首先要知道map的语义。

  

  主要用于解决在两个不同模块集中使用一个模块的不同版本,并且保证两个模块集的交互没有冲突。

  

  假设磁盘有如下文件:

  

  当'some/newmodule'请求'foo'模块时,它将从foo1.2.js总得到'foo1.2'模块;当'some/oldmodule'请求'foo'模块时它将从foo1.0中得到'foo1.0'模块。

  在map属性中可以使用任何的module ID前缀,并且mapping对象可以匹配任何别的module ID前缀。

  

  如果出现通配符‘*’,表示任何模块使用这个匹配配置。通配符匹配对象中的模块ID前缀可以被覆盖。

 

  通过上文的解释,可以明白,如果在'some/newmodule'中依赖的foo实际是上依赖的foo1.2。转化成代码逻辑应当是这样的:如果在‘some/module’模块中发现依赖foo模块那就将foo替换成foo1.2。但是在什么地方实现替换好呢?因为模块的定义从define开始,同时只有在define中才能获得模块的绝对路径,所以我们把替换的处理放在define中。那么问题来了,我们的模块大部分都是匿名模块,模块自己如何知道自己的模块Id?所以一定要有一个标记去告诉define函数当前模块的Id,我们知道每一个模块都是一个JavaScript文件,每一个模块都有一个对应的script元素,所以最好的做法是没每一个script都加一个自定义特性,来标记当前元素的模块Id。

  所以在loadJs中要为script加自定义特性:


function loadJS(url, mId) {
        var script = document.createElement('script');
        script.setAttribute('data-moduleId', mId); //为script元素保留原始模块Id
        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);
    };

在define函数中,通过文件的绝对路径,找出对应的script元素,拿到模块Id,判断如果在map中,则进行替换:


global.define = function(id, deps, callback) {
        //加上moduleId的支持
        if (typeof id !== "string" && arguments.length === 2) {
            callback = deps;
            deps = id;
            id = "";
        }
        var id = id || getCurrentScript();
        
        var script = document.querySelector('script[src="' + id + '"]');
        if (script || id in require.parsedConfig.shim) {
            var mId = script ? script.getAttribute('data-moduleId') : id;
            var maping = getMapSetting(mId);
            
            if (maping) {
                deps = deps.map(function(dep) {
                    return maping[dep] || dep;
                });
            }
        }
        if (modules[id]) {
            console.error('multiple define module: ' + id);
        }
        
        require(deps, callback, id);
    };

function getMapSetting(mId) {
        if (mId in require.parsedConfig.map) {
            return require.parsedConfig[mId];
        } else if ('*' in require.parsedConfig.map) {
            return require.parsedConfig.map['*'];
        } else {
            return null;
        }
    };

目前为止,我们的加载器已经支持了map属性,完整代码如下:

(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 = [];
        
        var oriDeps = deps.slice();//保留原始dep的模块Id
        
         // 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;
        
        if (checkCircleRef(id, id)) {
            return;
        }
        
        deps.forEach(function(dep, i) {
            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, oriDeps[i]);
                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]);
            }
        }
        
        this.parsedConfig.map = {};
        if (config.map) {
            this.parsedConfig.map = config.map;
        }
        
        this.parsedConfig.shim = {};
        //shim 要放在最后处理
        if (config.shim) {
            this.parsedConfig.shim = 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();
        
        var script = document.querySelector('script[src="' + id + '"]');
        if (script || id in require.parsedConfig.shim) {
            var mId = script ? script.getAttribute('data-moduleId') : id;
            var maping = getMapSetting(mId);
            
            if (maping) {
                deps = deps.map(function(dep) {
                    return maping[dep] || dep;
                });
            }
        }
        if (modules[id]) {
            console.error('multiple define module: ' + id);
        }
        
        require(deps, callback, id);
    };
    
    global.define.amd = {};//AMD规范
    
    function getMapSetting(mId) {
        if (mId in require.parsedConfig.map) {
            return require.parsedConfig[mId];
        } else if ('*' in require.parsedConfig.map) {
            return require.parsedConfig.map['*'];
        } else {
            return null;
        }
    };
    
    function checkCircleRef(start, target){
        var m = modules[start];
        if (!m) {
            return false;
        }
        var depModules = m.deps.map(function(dep) {
            return modules[dep] || null;
        });
        
        
        return depModules.some(function(m) {
            if (!m) {
                return false;
            }
            return m.deps.some(function(dep) {
                var equal = dep === target;
                if (equal) {
                    console.error("circle reference: ", target, m.id);
                }
                
                return equal;
            });
        }) ? true : depModules.some(function(m) {
            if (!m) {
                return false;
            }
            return m.deps.some(function(dep) {
                return checkCircleRef(dep, target);
            });
        });
        
        //return hasCr ? true: 
    };
    
    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, mId) {
        var script = document.createElement('script');
        script.setAttribute('data-moduleId', mId); //为script元素保留原始模块Id
        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:

  使用我们的加载器来加载jquery,同时禁用jquery的全局模式$:


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);
                console.log($);
                return something + " in shim";
            }
        }
    },
    map: {
        '*': {
            'jquery': 'jquery-private'
        },
        'jquery-private': {
            'jquery': 'jquery'
        }
    },
    paths: {
        'jquery': "../../Bodhi/src/roots/jquery"
    }
  });
  require([
  'bbb',
  //'aaa.bbb.ccc',
  //'ccc',
  //'ddd',
  //'fff',
  'something'
  ], function(aaabbbccc){
    console.log('simple loader');
    console.log(arguments);
  });

jquery-private代码如下:


define(['jquery'], function(jquery) {
    return jquery.noConflict(true);
});

如果某一模块依赖jquery,那么将会加载jquery-private。而在jquery中,因为配置了map和paths,所以jquery-private中的jquery根据paths找到jquery文件,并加载。同时在jquery-private中将禁用了全局模式之后的jquery对象返回给something模块。通过这个配置所有的模块在引用jquery时,实际上是引用了jquery-private模块。


目录
相关文章
|
JSON 关系型数据库 MySQL
实时计算 Flink版产品使用问题之对于百亿数据的三张表关联,该如何操作
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
监控 NoSQL Redis
|
机器学习/深度学习 数据采集 人工智能
探索机器学习在金融风控领域的应用
【6月更文挑战第19天】在金融科技迅猛发展的今天,机器学习技术已成为金融风控系统的核心。本文将深入探讨如何通过机器学习模型提高风险识别的准确性和效率,同时分析面临的挑战和应对策略。
|
新能源
两级电力市场环境下计及风险的省间交易商最优购电模型(Matlab代码实现)
两级电力市场环境下计及风险的省间交易商最优购电模型(Matlab代码实现)
230 0
|
SQL 关系型数据库 MySQL
MySQL学习必备SQL_DDL_DML_DQL
MySQL学习必备SQL_DDL_DML_DQL
100 0
|
Web App开发 JavaScript 测试技术
【如何学习Python自动化测试】—— 警告框处理
【如何学习Python自动化测试】—— 警告框处理
157 0
|
存储 Java 程序员
Python 设计和历史的 27 个问题
Python 设计和历史的 27 个问题
70 1
|
人工智能 Cloud Native Serverless
邀请报名|12月22日阿里云原生 Serverless 技术实践营 西安站
12月22日,西安 · 阿里中心举办阿里云原生 Serverless 技术实践营
邀请报名|12月22日阿里云原生 Serverless 技术实践营 西安站
|
存储 弹性计算 安全
阿里云服务器2核4G、4核8G、8核16G配置最新收费标准及活动价格参考
阿里云个人和一般企业用户在购买阿里云服务器时通常比较喜欢购买2核4G、8核16G、4核8G等配置,这些配置既能满足各种图文类中小型网站和应用又能满足企业网站应用、批量计算、中小型数据库系统等场景,2核4G配置适合新手入门或初创企业,4核8G与8核16G兼具成本与性能优势,适合通用场景,本文介绍这些配置的最新购买价格,包含原价收费标准和最新活动价格。
阿里云服务器2核4G、4核8G、8核16G配置最新收费标准及活动价格参考
|
Java 编译器 C++
C++异常处理
C++异常处理
81 0

热门文章

最新文章