javascript【AMD模块加载器】浅析V3(添加CSS加载功能,重构内部流程)

由于今天正美大大的回复,然后前篇文章的评论里就出现了好多人站出来指责我抄袭,吓的我小心肝都扑通扑通的跳。

虽然这次的模块加载器是参照了正美大大的来写,但是至少我是一行一行代码自己写出来的。然后一个浏览器一个浏览器测试的,就连流程图都重画了好几次。

虽然大体上跟正美的差不多,但是细节上还是有很多不同的。看到哪些回复我也不想说啥。 抄没抄,会不会。明眼人一眼就能看出来,犯不着解释太多。

废话不多说,下面介绍这一版本的改进。额外增加了一个配置项控制并发的数量。因为浏览器的有效并发数是有限的。所以如果你一次性加载10个模块,就有可能阻塞掉其它的资源加载。

现在内部默认最大并发是4个。将原来的moduleCache变量删除,将所有加在信息都整合到modules中,并标记初始加载函数,在所有模块加载结束后删除初始加在函数。  

所有css加载,不计入模块加载中。而且加载css也不会在factory的参数中出现。也就是说如果你这样写也没关系。

require(['hello','test.css','test'], function(hello,test){
   console.log(hello,test); 
});

不过现在还有一个问题就是加载css检测加载完毕的问题。 由于浏览器对link标签的onload事件支持各不一样,加之就算为之做了兼容也是锦衣夜行。 因为根本不需要知道css的加载情况。

主要的改动就这些,还有一些细节性的改动。去掉了deps属性,检测循环依赖的方法改为使用args而不是之前的deps。将之前的loadJS方法改变为loadSource。 将require方法和load拆分开来。

使用文档碎片来将节点批量插入到页面中,尽量减少修改dom树,减少浏览器重排。

这一版中依然使用了正美大大博客中的哪个获得当前被解析的script的url的方法,不知道园子里的各位朋友有没有更好的办法。在最初的时候我是用模块名称来做唯一的,这样就不用获取正在解析的script的url。 但是重名模块就很难解决了。如果大家有更好的解决办法希望能告知一下。大家一起进步。 下面是最新的源码和使用的方法。

View Code
  1 ;(function(win, undefined){
  2     win = win || window;
  3     var doc = win.document || document,
  4         head = doc.head || doc.getElementsByTagName("head")[0],
  5         fragment = document.createDocumentFragment(),
  6         hasOwn = Object.prototype.hasOwnProperty,
  7         slice = Array.prototype.slice,
  8         configure = {total : 4},
  9         basePath = (function(nodes){
 10             var node, url;
 11             if(!configure.baseUrl){
 12                 node = nodes[nodes.length - 1];
 13                 url = (node.hasAttribute ? node.src : node.getAttribute("src", 4)).replace(/[?#].*/, "");
 14             }else{
 15                 url = configure.baseUrl;
 16             }
 17             return url.slice(0, url.lastIndexOf('/') + 1);
 18         }(doc.getElementsByTagName('script'))),
 19         _lynx = win.lynx;
 20 
 21     /**
 22      * 框架入口
 23      */
 24     function lynx(exp, context){
 25         return new lynx.prototype.init(exp, context);
 26     }
 27     
 28     lynx.prototype = {
 29         constructor : lynx,
 30 
 31         /**
 32          * 初始化
 33          * @param {All} expr 
 34          * @param {All} context
 35          * @return {Object}
 36          * 
 37          */
 38         init : function(expr, context){
 39             if(typeof expr === 'function'){
 40                 require('ready', expr);
 41             }
 42             //TODO
 43         }
 44     }
 45     lynx.fn = lynx.prototype.init.prototype = lynx.prototype;
 46 
 47     /**
 48      * 继承方法
 49      */
 50     lynx.fn.extend = lynx.extend = function(){
 51         var args = slice.call(arguments), deep = typeof args[args.length - 1] == 'bollean' ? args.pop() : false;
 52         
 53         if(args.length == 1){
 54             args[1] = args[0];
 55             args[0] = this;
 56             args.length = 2;
 57         }
 58 
 59         var target = args[0], i = 1, len = args.length, source, prop;
 60         
 61         for(; i < len; i++){
 62             source = args[i];
 63             for(prop in source){
 64                 if(hasOwn.call(source, prop)){
 65                     if(typeof source[prop] == 'object'){
 66                         target[prop] = {};
 67                         this.extend(target[prop],source[prop]);
 68                     }else{
 69                         if(target[prop] === undefined){
 70                             target[prop] = source[prop];
 71                         }else{
 72                             deep && (target[prop] = source[prop]);
 73                         }
 74                     }
 75                 }
 76             }
 77         } 
 78     };
 79 
 80     /**
 81      * mix
 82      * @param  {Object} target   目标对象
 83      * @param  {Object} source   源对象
 84      * @return {Object}          目标对象
 85      */
 86     lynx.mix = function(target, source){
 87         if( !target || !source ) return;
 88         var args = slice.call(arguments), i = 1, override = typeof args[args.length - 1] === "boolean" ? args.pop() : true, prop;
 89         while ((source = args[i++])) {
 90             for (prop in source) {
 91                 if (hasOwn.call(source, prop) && (override || !(prop in target))) {
 92                     target[prop] = source[prop];
 93                 }
 94             }
 95         }
 96         return target;
 97     };
 98 
 99     lynx.mix(lynx, {
100         modules : {               //保存加载模块
101             ready : {
102                 state : 1,
103                 type : 1,
104                 args : [],
105                 exports : lynx
106             }
107         },
108         urls : [],
109         loading : 0,
110         stacks : [],             //getCurrentScript取不到值的时候用来存储当前script onload的回调函数数组
111 
112         /**
113          * get uuid
114          * @param {String} prefix
115          * @return {String} uuid
116          */
117         guid : function(prefix){
118             prefix = prefix || '';
119             return prefix + (+new Date()) + String(Math.random()).slice(-8);
120         },
121 
122         /**
123          * noop 空白函数
124          */
125         noop : function(){
126 
127         },
128 
129         /**
130          * error 
131          * @param {String} str
132          */
133         error : function(str){
134             throw new Error(str);
135         },
136 
137         /**
138          * @return {Object} lynx
139          */
140         noConflict : function(deep) {
141             if ( window.lynx === lynx ) {
142                 window.lynx = _lynx;
143             }
144             return lynx;
145         }
146     });
147 
148 
149     //================================ 模块加载 ================================
150     /**
151      * 模块加载方法
152      * @param {String|Array}   ids      需要加载的模块
153      * @param {Function} callback 加载完成之后的回调
154      */
155     win.require = lynx.require = function(ids, callback){
156         ids = typeof ids === 'string' ? [ids] : ids;
157         var modules = lynx.modules, urls = lynx.urls, uuid = lynx.guid('cb_'), data;
158         data = parseModules(ids, basePath);
159         modules[uuid] = {
160             name : 'initialize',
161             type : 2,
162             state : 1,
163             args : data.args,
164             factory : callback
165         };
166         urls = urls.concat(data.urls);
167         lynx.load(urls);
168     };
169 
170     /**
171      * @param  {String} id           模块名
172      * @param  {String|Array} [dependencies] 依赖列表
173      * @param  {Function} factory      工厂方法
174      */
175     win.define = function(id, dependencies, factory){
176         if(typeof dependencies === 'function'){
177             factory = dependencies;
178             if(typeof id === 'array'){
179                 dependencies = id;
180             }else if(typeof id === 'string'){
181                 dependencies = [];
182             }
183         }else if (typeof id == 'function'){
184             factory = id;
185             dependencies = [];
186         }
187         id = lynx.getCurrentScript();
188 
189         dependencies = typeof dependencies === 'string' ? [dependencies] : dependencies;
190 
191         var handle = function(id, dependencies, factory){
192             var modules = lynx.modules, urls = lynx.urls;
193             modules[id].factory = factory;
194             modules[id].state = 2;
195             if(!dependencies.length){
196                 fireFactory(id);
197             }else{
198                 var data = parseModules(dependencies, id, true);
199                 urls = urls.concat(data.urls);
200                 lynx.load(urls);
201             }
202         }
203         if(!id){
204             lynx.stacks.push(function(dependencies, factory){
205                 return function(id){
206                     handle(id, dependencies, factory);
207                     id = null; dependencies = null; factory = null;
208                 }
209             }(dependencies, factory));
210         }else{
211             handle(id, dependencies, factory);
212         }
213     }
214 
215     require.amd = define.amd = lynx.modules;
216 
217     /**
218      * 解析加载模块信息
219      * @param {Array} list
220      * @param {String} path 
221      * @param {boolean} flag 
222      * @return {Object}
223      */
224     function parseModules(list, basePath, flag){
225         var modules = lynx.modules, urls = [], args = [], uniqurl = {}, id, result;
226         while(id = list.shift()){
227             if(modules[id]){ 
228                 args.push(id);
229                 continue;
230             }
231             result = parseModule(id, basePath);
232             modules[basePath] && modules[basePath].args.push(result[1]);
233             flag && checkCircularDeps(result[1], basePath) && lynx.error('模块[url:'+ basePath +']与模块[url:'+ result[1] +']循环依赖');
234             modules[result[1]] = {
235                 type : result[2] === 'js' ? 1 : 2,
236                 name : result[0],
237                 state : 0,
238                 exports : {},
239                 args : [],
240                 factory : lynx.noop
241             };
242             (result[2] === 'js') && args.push(result[1]);
243             if(!uniqurl[result[1]]){
244                 uniqurl[result[1]] = true;
245                 urls.push(result[1]);
246             }
247         }
248 
249         return {
250             args : args,
251             urls : urls
252         }
253     }
254 
255     /**
256      * parse module
257      * @param {String} id 模块名
258      * @param {String} basePath 基础路径
259      * @return {Array} 
260      */
261     function parseModule(id, basePath){
262         var url, result, ret, dir, paths, i, len, type, modname, protocol = /^(\w+\d?:\/\/[\w\.-]+)(\/(.*))?/;
263         if(result = protocol.exec(id)){
264             url = id;
265             paths = result[3] ? result[3].split('/') : [];
266         }else{
267             result = protocol.exec(basePath);
268             url = result[1];
269             paths = result[3] ? result[3].split('/') : [];
270             modules = id.split('/');
271             paths.pop();
272             for(i = 0, len = modules.length; i < len; i++){
273                 dir = modules[i];
274                 if(dir == '..'){
275                     paths.pop();
276                 }else if(dir !== '.'){
277                     paths.push(dir);
278                 }
279             }
280             url = url + '/' + paths.join('/');
281         }
282         modname = paths[paths.length - 1];
283         type = modname.slice(modname.lastIndexOf('.') + 1);
284         if(type !== 'js' && type !== 'css'){
285             type = 'js';
286             url += '.js';
287         }
288         return [modname, url, type];
289     }
290 
291     /**
292      * fire factory
293      * @param  {String} uuid
294      */
295     function fireFactory(uuid){
296         var modules = lynx.modules,
297         data = modules[uuid], deps = data.args,
298         i = 0, len = deps.length, args = [];
299         for(; i < len; i++){
300             args.push(modules[deps[i]].exports)
301         }
302         data.exports = data.factory.apply(null, args);
303         data.state = 3;
304         delete data.factory;
305         delete data.args;
306         if(data.type == 2 && data.name == 'initialize'){
307             delete modules[uuid];
308         }
309         checkLoadReady();
310     }
311 
312     /**
313      * 检测是否全部加载完毕
314      */
315     function checkLoadReady(){
316         var modules = lynx.modules, flag = true, data, prop, deps, mod, i , len;
317         for (prop in modules) {
318             data = modules[prop];
319             if(data.type == 1 && data.state != 2){    //如果还没执行到模块的define方法
320                 continue;
321             }
322             deps = data.args;
323             for(i = 0, len = deps.length; mod = deps[i], i < len ; i++){
324                 if(hasOwn.call(modules, mod) && modules[mod].state != 3){
325                     flag = false;
326                     break;
327                 }
328             }
329             if(data.state != 3 && flag){
330                 fireFactory(prop);
331             }
332         }
333     }
334 
335     /**
336      * 检测循环依赖
337      * @param  {String} id         
338      * @param  {Array} dependencie
339      */
340     function checkCircularDeps(id, dependencie){
341         var modules = lynx.modules, depslist = modules[id] ? modules[id].args : [];
342         return ~depslist.join(' ').indexOf(dependencie);
343     }
344 
345     /**
346      * create
347      * @param {String} type CSS|JS
348      * @param {String} url
349      * @param {Function} callback
350      */
351     function loadSource(type, url, callback){
352         var ndoe, modules = lynx.modules;
353         if(type == 'JS'){
354             var node = doc.createElement("script");
355             node[node.onreadystatechange ? 'onreadystatechange' : 'onload'] = function(){
356                 if(!node.onreadystatechange || /loaded|complete/i.test(node.readyState)){
357                     callback();
358                     node.onload = node.onreadystatechange = node.onerror = null;
359                     var fn = lynx.stacks.pop();
360                     fn && fn.call(null, node.src);
361                     head.removeChild(node);
362                 }
363             }
364             node.src = url;
365             modules[url].state = 1;
366             lynx.loading++;
367         }else if(type == 'CSS'){
368             var node = doc.createElement("link");
369             node.rel = 'stylesheet';
370             node.href = url;
371             delete modules[url];
372         }
373         node.onerror = function(){
374             lynx.error('模块[url:'+ node.src +']加载失败');
375             node.onload = node.onreadystatechange = node.onerror = null;
376             lynx.loading--;
377             head.removeChild(node);
378         }
379         return node;
380 
381     };
382 
383     lynx.mix(lynx, {
384         load : function(urls){
385             var loading , total = configure.total,modules = lynx.modules, url, node = fragment, type;
386             while((loading = lynx.loading) < total && (url = urls.shift())){
387                 type = url.slice(url.lastIndexOf('.') + 1).toUpperCase();
388                 node.appendChild(loadSource(type, url, function(){
389                     lynx.loading--;
390                     var urls = lynx.urls;
391                     urls.length && lynx.load(urls); 
392                 }));
393             }
394             head.insertBefore(node, head.firstChild);
395         },
396 
397         /**
398          * 加载JS文件
399          * @param {String} url
400          * @param {Function} callback 
401          */
402         loadJS : function(url, callback){
403             var node = loadSource('JS', url, callback)
404             head.insertBefore(node, head.firstChild);
405         },
406 
407         /**
408          * 加载CSS文件
409          * @param {String} url
410          * @param {Function} callback
411          */
412         loadCSS : function(url, callback){
413             var node = loadSource('CSS', url, callback);
414             head.insertBefore(node, head.firstChild);
415         },
416 
417         /**
418          * get current script [此方法来自司徒正美的博客]
419          * @return {String}
420          */
421         getCurrentScript : function(){
422             //取得正在解析的script节点
423             if (doc.currentScript) { //firefox 4+
424                 return doc.currentScript.src;
425             }
426             // 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js
427             var stack;
428             try {
429                 a.b.c(); //强制报错,以便捕获e.stack
430             } catch (e) { //safari的错误对象只有line,sourceId,sourceURL
431                 stack = e.stack; 
432                 if (!stack && window.opera) {
433                     //opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取
434                     stack = (String(e).match(/of linked script \S+/g) || []).join(" ");
435                 }
436             }
437             if (stack) {
438                 /**e.stack最后一行在所有支持的浏览器大致如下:
439                  *chrome23:
440                  * at http://113.93.50.63/data.js:4:1
441                  *firefox17:
442                  *@http://113.93.50.63/query.js:4
443                  *opera12:http://www.oldapps.com/opera.php?system=Windows_XP
444                  *@http://113.93.50.63/data.js:4
445                  *IE10:
446                  *  at Global code (http://113.93.50.63/data.js:4:1)
447                  */
448                 stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一个空格或@之后的部分
449                 stack = stack[0] === "(" ? stack.slice(1, -1) : stack;
450                 return stack.replace(/(:\d+)?:\d+$/i, ""); //去掉行号与或许存在的出错字符起始位置
451             }
452             var nodes = head.getElementsByTagName("script"); //只在head标签中寻找
453             for (var i = 0, node; node = nodes[i++]; ) {
454                 if (node.readyState === "interactive") {
455                     return node.src;
456                 }
457             }    
458         },
459 
460         /**
461          * 配置模块信息
462          * @param  {Object} option
463          */
464         config : function(option){
465             lynx.mix(configure, option);
466         },
467 
468 
469     //============================== DOM Ready =============================
470         
471         /**
472          * dom ready
473          * @param {Function} callback
474          */
475         ready : function (){                              
476             var isReady = false;
477             var readyList = [];
478             var ready = function(fn){
479                 if(isReady){
480                     fn();
481                 }else{
482                     readyList.push(fn);
483                 }
484             };
485 
486             var fireReady = function(){
487                 for(var i = 0,len = readyList.length; i < len; i++){
488                     readyList[i]();
489                 }
490                 readyList = [];
491                 lynx.modules.ready.state = 3;
492                 checkLoadReady();
493             };
494 
495             var bindReady = function(){
496                 if(isReady){
497                     return;
498                 }
499                 isReady=true;
500                 fireReady();
501                 if(doc.removeEventListener){
502                     doc.removeEventListener("DOMContentLoaded",bindReady,false);
503                 }else if(doc.attachEvent){
504                     doc.detachEvent("onreadystatechange", bindReady);
505                 }               
506             };
507 
508             if( doc.readyState === "complete" ) {
509                 bindReady();
510             }else if(doc.addEventListener){
511                 doc.addEventListener("DOMContentLoaded", bindReady, false);
512             }else if(doc.attachEvent){
513                 doc.attachEvent("onreadystatechange", function(){
514                     if((/loaded|complete/).test(doc.readyState)){
515                         bindReady();
516                     }                       
517                 });
518                 (function(){
519                     if(isReady){
520                         return;
521                     }
522                     var node = new Image();
523                     var timer = setInterval(function(){
524                         try{
525                             isReady || node.doScroll('left');
526                             node = null;
527                         }catch(e){
528                             return;
529                         }
530                         clearInterval(timer);
531                         bindReady();
532                     }, 16);
533                 }());
534             }
535             return ready;
536         }()
537     });
538     
539     win.lynx = lynx;
540 }(window));
//使用方法
//index.html 页面
<html>
    <head>
        <title>测试</title>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <script type="text/javascript" src="lynx.js"></script>
        <script type="text/javascript">
            lynx.require(['test.css','hello'], function(hello){
                alert(hello);
            })
        </script>
    </head>
    <body>
        <div class="test"></div>
    </body>
</html>

//test.css 文件
.test{
    width: 200px;
    height: 100px;
    background-color: red;
}

//hello.js 文件
define('hello', 'test',function(test){
    return 'a' + test;
});

//test.js 文件
define('test',function(test){
    return 'b';
});

 

posted @ 2013-03-12 01:34  猫猫大侠  阅读(1742)  评论(2编辑  收藏  举报