• 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了。

      

      如果您觉得这篇文章对您有帮助,请不吝点击右下方推荐~

  • 相关阅读:
    第七周作业
    第六周作业
    第四周作业
    第三次作业
    第二次作业
    最后一次作业-- 总结报告
    第十四,十五周作业
    第七周作业
    第六周作业
    第四周作业
  • 原文地址:https://www.cnblogs.com/dojo-lzz/p/5138108.html
Copyright © 2020-2023  润新知