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属性,完整代码如下:

  1 (function(global){
  2     global = global || window;
  3     modules = {};
  4     loadings = [];
  5     loadedJs = [];
  6     //module: id, state, factory, result, deps;
  7     global.require = function(deps, callback, parent){
  8         var id = parent || "Bodhi" + Date.now();
  9         var cn = 0, dn = deps.length;
 10         var args = [];
 11
 12         var oriDeps = deps.slice();//保留原始dep的模块Id
 13
 14          // dep为非绝对路径形式,而modules的key仍然需要绝对路径
 15         deps = deps.map(function(dep) {
 16             if (modules[dep]) { //jquery
 17                 return dep;
 18             } else if (dep in global.require.parsedConfig.paths) {
 19                 return dep;
 20             }
 21             var rel = "";
 22             if (/^Bodhi/.test(id)) {
 23                 rel = global.require.parsedConfig.baseUrl;
 24             } else {
 25                 var parts = parent.split(‘/‘);
 26                 parts.pop();
 27                 rel = parts.join(‘/‘);
 28             }
 29             return getModuleUrl(dep, rel);
 30         });
 31
 32         var module = {
 33             id: id,
 34             deps: deps,
 35             factory: callback,
 36             state: 1,
 37             result: null
 38         };
 39         modules[id] = module;
 40
 41         if (checkCircleRef(id, id)) {
 42             return;
 43         }
 44
 45         deps.forEach(function(dep, i) {
 46             if (modules[dep] && modules[dep].state === 2) {
 47                 cn++
 48                 args.push(modules[dep].result);
 49             } else if (!(modules[dep] && modules[dep].state === 1) && loadedJs.indexOf(dep) === -1) {
 50                 loadJS(dep, oriDeps[i]);
 51                 loadedJs.push(dep);
 52             }
 53         });
 54         if (cn === dn) {
 55             callFactory(module);
 56         } else {
 57             loadings.push(id);
 58             checkDeps();
 59         }
 60     };
 61
 62     global.require.config = function(config) {
 63         this.parsedConfig = {};
 64         if (config.baseUrl) {
 65             var currentUrl = getCurrentScript();
 66             var parts = currentUrl.split(‘/‘);
 67             parts.pop();
 68             var currentDir = parts.join(‘/‘);
 69             this.parsedConfig.baseUrl = getRoute(currentDir, config.baseUrl);
 70         }
 71         var burl = this.parsedConfig.baseUrl;
 72         // 得到baseUrl后,location相对baseUrl定位
 73         this.parsedConfig.packages = [];
 74         if (config.packages) {
 75             for (var i = 0, len = config.packages.length; i < len; i++) {
 76                 var pck = config.packages[i];
 77                 var cp = {
 78                     name: pck.name,
 79                     location: getRoute(burl, pck.location)
 80                 }
 81                 this.parsedConfig.packages.push(cp);
 82             }
 83         }
 84
 85
 86         this.parsedConfig.paths = {};
 87         if (config.paths) {
 88             for (var p in config.paths) {
 89                 this.parsedConfig.paths[p] = /^http(s)?/.test(config.paths[p]) ? config.paths[p] : getRoute(burl, config.paths[p]);
 90             }
 91         }
 92
 93         this.parsedConfig.map = {};
 94         if (config.map) {
 95             this.parsedConfig.map = config.map;
 96         }
 97
 98         this.parsedConfig.shim = {};
 99         //shim 要放在最后处理
100         if (config.shim) {
101             this.parsedConfig.shim = config.shim;
102             for (var p in config.shim) {
103                 var item = config.shim[p];
104                 define(p, item.deps, function() {
105                     var exports;
106                     if (item.init) {
107                         exports = item.init.apply(item, arguments);
108                     }
109
110                     return exports ? exports : item.exports;
111                 });
112             }
113         }
114
115         console.log(this.parsedConfig);
116     }
117
118     global.define = function(id, deps, callback) {
119         //加上moduleId的支持
120         if (typeof id !== "string" && arguments.length === 2) {
121             callback = deps;
122             deps = id;
123             id = "";
124         }
125         var id = id || getCurrentScript();
126
127         var script = document.querySelector(‘script[src="‘ + id + ‘"]‘);
128         if (script || id in require.parsedConfig.shim) {
129             var mId = script ? script.getAttribute(‘data-moduleId‘) : id;
130             var maping = getMapSetting(mId);
131
132             if (maping) {
133                 deps = deps.map(function(dep) {
134                     return maping[dep] || dep;
135                 });
136             }
137         }
138         if (modules[id]) {
139             console.error(‘multiple define module: ‘ + id);
140         }
141
142         require(deps, callback, id);
143     };
144
145     global.define.amd = {};//AMD规范
146
147     function getMapSetting(mId) {
148         if (mId in require.parsedConfig.map) {
149             return require.parsedConfig[mId];
150         } else if (‘*‘ in require.parsedConfig.map) {
151             return require.parsedConfig.map[‘*‘];
152         } else {
153             return null;
154         }
155     };
156
157     function checkCircleRef(start, target){
158         var m = modules[start];
159         if (!m) {
160             return false;
161         }
162         var depModules = m.deps.map(function(dep) {
163             return modules[dep] || null;
164         });
165
166
167         return depModules.some(function(m) {
168             if (!m) {
169                 return false;
170             }
171             return m.deps.some(function(dep) {
172                 var equal = dep === target;
173                 if (equal) {
174                     console.error("circle reference: ", target, m.id);
175                 }
176
177                 return equal;
178             });
179         }) ? true : depModules.some(function(m) {
180             if (!m) {
181                 return false;
182             }
183             return m.deps.some(function(dep) {
184                 return checkCircleRef(dep, target);
185             });
186         });
187
188         //return hasCr ? true:
189     };
190
191     function getRoute(base, target) {
192         var bts = base.replace(/\/$/, "").split(‘/‘);  //base dir
193         var tts = target.split(‘/‘); //target parts
194         while (isDefined(tts[0])) {
195             if (tts[0] === ‘.‘) {
196                 return bts.join(‘/‘) + ‘/‘ + tts.slice(1).join(‘/‘);
197             } else if (tts[0] === ‘..‘) {
198                 bts.pop();
199                 tts.shift();
200             } else {
201                 return bts.join(‘/‘) + ‘/‘ + tts.join(‘/‘);
202             }
203         }
204     };
205
206     function isDefined(v) {
207         return v !== null && v !== undefined;
208     };
209
210     function getModuleUrl(moduleId, relative) {
211         function getPackage(nm) {
212             for (var i = 0, len = require.parsedConfig.packages.length; i < len; i++) {
213                 var pck = require.parsedConfig.packages[i];
214                 if (nm === pck.name) {
215                     return pck;
216                 }
217             }
218             return false;
219         }
220         var mts = moduleId.split(‘/‘);
221         var pck = getPackage(mts[0]);
222         if (pck) {
223             mts.shift();
224             return getRoute(pck.location, mts.join(‘/‘));
225         } else if (mts[0] === ‘.‘ || mts[0] === ‘..‘) {
226             return getRoute(relative, moduleId);
227         } else {
228             return getRoute(require.parsedConfig.baseUrl, moduleId);
229         }
230     };
231
232     function loadJS(url, mId) {
233         var script = document.createElement(‘script‘);
234         script.setAttribute(‘data-moduleId‘, mId); //为script元素保留原始模块Id
235         script.type = "text/javascript";
236         //判断模块是否在paths中定义了路径
237         script.src = (url in global.require.parsedConfig.paths ? global.require.parsedConfig.paths[url] : url) + ‘.js‘;
238         script.onload = function() {
239             var module = modules[url];
240             if (module && isReady(module) && loadings.indexOf(url) > -1) {
241                 callFactory(module);
242             }
243             checkDeps();
244         };
245         var head = document.getElementsByTagName(‘head‘)[0];
246         head.appendChild(script);
247     };
248
249     function checkDeps() {
250         for (var p in modules) {
251             var module = modules[p];
252             if (isReady(module) && loadings.indexOf(module.id) > -1) {
253                 callFactory(module);
254                 checkDeps(); // 如果成功,在执行一次,防止有些模块就差这次模块没有成功
255             }
256         }
257     };
258
259     function isReady(m) {
260         var deps = m.deps;
261         var allReady = deps.every(function(dep) {
262             return modules[dep] && isReady(modules[dep]) && modules[dep].state === 2;
263         })
264         if (deps.length === 0 || allReady) {
265             return true;
266         }
267     };
268
269     function callFactory(m) {
270         var args = [];
271         for (var i = 0, len = m.deps.length; i < len; i++) {
272             args.push(modules[m.deps[i]].result);
273         }
274         m.result = m.factory.apply(window, args);
275         m.state = 2;
276
277         var idx = loadings.indexOf(m.id);
278         if (idx > -1) {
279             loadings.splice(idx, 1);
280         }
281     };
282
283     function getCurrentScript(base) {
284         // 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js
285         var stack;
286         try {
287             a.b.c(); //强制报错,以便捕获e.stack
288         } catch (e) { //safari的错误对象只有line,sourceId,sourceURL
289             stack = e.stack;
290             if (!stack && window.opera) {
291                 //opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取
292                 stack = (String(e).match(/of linked script \S+/g) || []).join(" ");
293             }
294         }
295         if (stack) {
296             /**e.stack最后一行在所有支持的浏览器大致如下:
297              *chrome23:
298              * at http://113.93.50.63/data.js:4:1
299              *firefox17:
300              *@http://113.93.50.63/query.js:4
301              *opera12:http://www.oldapps.com/opera.php?system=Windows_XP
302              *@http://113.93.50.63/data.js:4
303              *IE10:
304              *  at Global code (http://113.93.50.63/data.js:4:1)
305              *  //firefox4+ 可以用document.currentScript
306              */
307             stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一个空格或@之后的部分
308             stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/\s/, ""); //去掉换行符
309             return stack.replace(/(:\d+)?:\d+$/i, "").replace(/\.js$/, ""); //去掉行号与或许存在的出错字符起始位置
310         }
311         var nodes = (base ? document : head).getElementsByTagName("script"); //只在head标签中寻找
312         for (var i = nodes.length, node; node = nodes[--i]; ) {
313             if ((base || node.className === moduleClass) && node.readyState === "interactive") {
314                 return node.className = node.src;
315             }
316         }
317     };
318 })(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模块。

时间: 2024-10-01 03:02:28

AMD加载器实现笔记(四)的相关文章

AMD加载器实现笔记(二)

AMD加载器实现笔记(一)中,我们实现了一个简易的模块加载器.但到目前为止这个加载器还并不能称为AMD加载器,原因很简单,我们还不支持AMD规范中的config配置.这篇文章中我们来添加对config的中baseUrl和packages的支持.API设计如下: 1 require.config({ 2 baseUrl: "./", 3 packages: [{ 4 name: "more", 5 location: "./more" 6 }, {

AMD加载器实现笔记(一)

之前研究过AMD,也写过一篇关于AMD的文章<以代码爱好者角度来看AMD与CMD>.代码我是有看过的,基本的原理也都明白,但实际动手去实现却是没有的.因为今年计划的dojo教程<静静的dojo>中,有一章节来专门讲解AMD,不免要把对AMD的研究回炉一下.时隔多日,再回头探索AMD实现原理时,竟抓耳挠腮,苦苦思索不得要领.作为开发人员,深感惭愧.故有此文,记录我在实现一个AMD加载器时的思考总结. requireJS是所有AMD加载器中,最广为人知的一个.目前的版本更凝聚了几位大牛

AMD加载器实现笔记(五)

前几篇文章对AMD规范中的config属性几乎全部支持了,这一节主要是进一步完善.到目前为止我们的加载器还无法处理环形依赖的问题,这一节就是解决环形依赖. 所谓环形依赖,指的是模块A的所有依赖项的依赖中有没有依赖A模块本身的模块.如果有那就说明存在环形依赖.所以检验的方式是利用递归,检查一个模块的依赖的依赖项中有没有依赖A模块,以及依赖项的依赖项的依赖项中有没有A模块,核心代码如下: function checkCircleRef(start, target){ var m = modules[

AMD加载器实现笔记(三)

上一篇文章中我们为config添加了baseUrl和packages的支持,那么这篇文章中将会看到对shim与paths的支持. 要添加shim与paths,第一要务当然是了解他们的语义与用法.先来看shim,shim翻译成中文是“垫片”的意思.在AMD中主要用途是把不支持AMD的某些变量包装AMD模块.shim是一个哈希对象,key为包装后的模块Id,value是关于这个包装模块的一些配置,主要配置项如下: deps:定义模块需要的依赖项的moduleId数组 exports:模块输出值 in

JavaScript AMD 模块加载器原理与实现

关于前端模块化,玉伯在其博文 前端模块化开发的价值 中有论述,有兴趣的同学可以去阅读一下. 1. 模块加载器 模块加载器目前比较流行的有 Requirejs 和 Seajs.前者遵循 AMD规范,后者遵循 CMD规范.前者的规范产出比较适合于浏览器异步环境的习惯,后者的规范产出对于写过 nodejs 的同学来说是比较爽的.关于两者的比较,有兴趣的同学请参看玉伯在知乎的回答 AMD和CMD的区别有哪些.本文希望能按照 AMD 规范来简单实现自己的一个模块加载器,以此来搞清楚模块加载器的工作原理.

自研模块加载器(四) 模块资源定位-异步加载

资源定位-动态加载 通过resolve方法进行异步解析,完整解析如下图所示: 根据上篇文章startUp.js代码,我们继续完善本章动态加载资源的代码. (function(global) { var startUp = global.startUp = { version: '1.0.1' } var data = {}; // 获取当前模块加载器配置信息 var cache = {}; // 缓存 //模块的生命周期 var status = { FETCHED: 1, SAVED: 2,

《你必须知道的.NET》读书实践:一个基于OO的万能加载器的实现

此篇已收录至<你必须知道的.Net>读书笔记目录贴,点击访问该目录可以获取更多内容. 一.关于万能加载器 简而言之,就是孝顺的小王想开发一个万能程序,可以一键式打开常见的计算机资料,例如:文档.图片和影音文件等,只需要安装一个程序就可以免去其他应用文件的管理(你让其他耗费了巨资打造的软件情何以堪...),于是就有了这个万能加载器(FileLoader). 初步分析之后,小王总结了这个万能加载器的功能点如下: (1)能够打开常见文档类资料:txt.word.pdf.visio等: (2)能够打开

JS模块加载器加载原理是怎么样的?

路人一: 原理一:id即路径 原则.通常我们的入口是这样的: require( [ 'a', 'b' ], callback ) .这里的 'a'.'b' 都是 ModuleId.通过 id 和路径的对应原则,加载器才能知道需要加载的 js 的路径.在这个例子里,就是 baseUrl + 'a.js' 和 baseUrl + 'b.js'. 但 id 和 path 的对应关系并不是永远那么简单,比如在 AMD 规范里就可以通过配置 Paths 来给特定的 id 指配 path. 原理二:crea

JS模块化编程之加载器原理

世面上有好多JavaScript的加载器,比如 sea.js, require.js, yui loader, labJs...., 加载器的使用范围是一些比较大的项目, 个人感觉如果是小项目的话可以不用,  我用过seaJS和requireJS, 在项目中用过requireJS, requireJS是符合AMD,全称是(Asynchronous Module Definition)即异步模块加载机制 , seaJS是符合CMD规范的加载器. AMD__和__CMD AMD规范是依赖前置, CM