叶落为重生每片落下的叶子都是为了下一次的涅槃...^_^

模块加载--站在巨人肩膀上的版本

  好久没有更新博客了。最近基本上也一直在闭门造车轮。在模块加载的问题上苦苦纠结。距离上次发的狗屁不通的一个版本,也过去了大概两个星期的时间了。

  说上次那篇博文狗屁不通的确不无道理。具体情况可以参考前文《关于前端基础框架的思考》。现在看来的确到处都是漏洞,没有处理不同操作的并行,也没有处理依赖和并行混淆的情况,连但操作,无并发的递归的依赖情况可能都有漏洞。甚至犯了极其低级的错误都还不知道。幸亏一位园友的提醒... 真是惭愧。

  其他的也不多说了,还是回到正题吧。又经过两个星期的辗转反侧,查阅了各大框架的源码,最终有了一个稍微有一点可用性的版本。只敢说是站在巨人肩膀上的版本,自知若没有前人的成果作为引导和启发,恐怕不是两个星期的事了...

【写代码前要考虑的问题】

  关于模块加载器,我个人觉得是一个框架不能缺的东西,或者说是核心的东西之一。这也是我为什么会想从这个地方开始我的自造车轮的原因。就是为了方便的管理我们需要颗粒化的js模块文件。并且通过异步加载的方式平衡颗粒化管理和多http链接的矛盾。

  1.在web技术发展迅猛的时候时候,出于页面的性能原因,web设计者们不得不开始考虑所谓的“按需加载”,比如当某个地方需要用户交互的地方,当获取到事件handle的时候才去加载对应的实现功能的js。曾经一段时间是用xhr的方式来加载,后来因为网络环境依赖程度高,依旧阻塞式的加载难以缓解页面性能等等原因。被新的异步方式取代了。

2.异步的加载方式其实和jsonp的形式很像,通过动态创建scriptNode,完全可控的插入到head中的方式,可以自定义js什么时候load,并且处理load后的回调。

3.当可以异步的实现文件加载的时候,人们的贪念就来了,开始考虑并发的异步,串行的异步,以及两种方式混淆的异步。比如有一个操作A需要两个js文件支持,a.js和b.js,他们俩是互不依赖的,当异步的去加载他们的时候,没必要等到一个加载完毕之后才去加载第二个,完全可以两个同时进行load,这就是最简单的并行。逻辑处理也很简单, 设个计数器为当前并发加载js的总数,每个js加载完毕之后计数器减一,检测到计数器为0时,证明并发加载已经完毕,可以执行操作A。

4.需要串行的情况一般都发生在有模块依赖的时候,比如操作A需要a.js 支持,a.js又需要b.js,那么这一条依赖链就形成了,加载顺序必须得是先加载b.js,然后才能加载a.js,要不然a.js中需要调用b.js中的函数的地方就会报错。这是个最简单的异步的,串行的加载情况。可是问题往往没那么简单...比如有操作B需要a.js和b.js,然后a.js需要c.js和d.js,然后d.js又需要b.js。我随便举个例子,这种多依赖并且有交叉的时候,往往就需要针对每一个需要加载的js针对他的依赖进去递归了。这其实就可以理解成一种递归型的依赖了。

5.即便我们处理好了递归的依赖情况,自动的帮所有依赖理清顺序,我们仍然可以把问题继续考虑复杂一些,比如还是上面的操作B,假如他需要a.js,b.js,e.js,f.js这四个, 然后同样a.js需要c和d,d需要b,但是e.js和f.js是无任何依赖的,也就是说理想状态是把没有任何依赖的e,f,单独看做两条独立的链,然后和需要递归或者串行的b->d->c->a这条链进行并发。这就是较为复杂的并行和串行混杂的模式。

6.最后,单一操作有并行,串行混杂的情况,多操作的时候就更为麻烦一点了。而且也是必然需要考虑的一点。一个页面多操作,这肯定是常态,而且多操作没理由串行,肯定是并行的。比如一个页面有两个操作,操作A,操作B,操作A需要模块a,b。操作B需要模块b,c,模块c依赖于a,这种情况是可能的。那么操作A,操作B并发的话,模块a在操作A中属于应该并发加载的模块,在操作B中又属于应该由a->c串行加载的模块。这样在判断逻辑的时候很容易起冲突,于是乎,一个页面中看似没什么关联的多操作就没那么简单的可以独立开了。或许需要把页面内的多操作也全局的统一管理起来,才可能理得清操作与操作之间的微妙关系了,类似于YUI的add。

说了这么多,看来一个完善的模块加载器需要考虑的东西还真不少。通过这两周“站在巨人肩膀”上的尝试,解决了上面大部分的问题,但还是不够完善,等会会接着说。还是先贴代码吧:

代码
/**
 * @Author: hongru.chen
 * @version: 0.2
*
*/
if (typeof HR === 'undefined' || !HR) HR = {};

(
function () {
    
//-- namespace from YUI
    HR.namespace = function () {
        
var a = arguments, o = null, i, j, d;
        
for (i=0; i<a.length; i++) {
            d 
= ('' + a[i]).split('.');
            o 
= this;
            
for (j = (d[0== 'HR'? 1 : 0; j<d.length; j++) {
                o[d[j]] 
= o[d[j]] || {};
                o 
= o[d[j]];
            }
        }
        
return o;
    }

/**
 * Method 使用模块的主函数
 * @param (String or Array) 要使用的模块名
 * @param (Function) *optional 加载模块后的回调函数
 * @param (Object) *optional 回调绑定对象
 * @return undefined
*
*/ 
var _module = function (moduleName, callback, context) {
    
var argIndex=-1;
    
    
// private method 监测moduleName,如果是url(http://*)路径形式,register后load
        function checkURL(src) {
            
var dsrc = src;
            
if (src && src.substring(04== "url(") {
                dsrc 
= src.substring(4, src.length - 1);
            }
            
var r = _module.registered[dsrc];
            
return (!&& (!_module.__checkURLs || !_module.__checkURLs[dsrc]) && src && src.length > 4 && src.substring(04== "url(");
        }
        
    
// 并发调用的模块列表
    var moduleNames = new Array();
    
    
if (typeof(moduleName) != "string" && moduleName.length) {
        
var _moduleNames = moduleName;
        
for (var s=0;s<_moduleNames.length; s++) {
            
if (_module.registered[_moduleNames[s]] || checkURL(_moduleNames[s])) {
                moduleNames.push(_moduleNames[s]);
            }
        }
        moduleName 
= moduleNames[0];
        argIndex 
= 1;
    } 
else {
        
while (typeof(arguments[++argIndex]) == "string") {
            
if (_module.registered[moduleName] || checkURL(moduleName)) {
                moduleNames.push(arguments[argIndex]);
            }
        }
    }
    callback 
= arguments[argIndex];
    context 
= arguments[++argIndex];
    
    
if (moduleNames.length > 1) {
        
var cb = callback;
        callback 
= function() {
            _module(moduleNames, cb, context);
        }
    }
    
    
// 已经register过的模块hash
    var reg = _module.registered[moduleName];
    
// 处理直接使用url的情况
    if (!_module.__checkURLs) _module.__checkURLs = {};
    
if (checkURL(moduleName) && moduleName.substring(04== "url(") {
        moduleName 
= moduleName.substring(4, moduleName.length - 1);
        
if (!_module.__checkURLs[moduleName]) {
            moduleNames[
0= moduleName;
            _module.register(moduleName, moduleName);
            reg 
= _module.registered[moduleName];
            
var callbackQueue = _module.prototype.getCallbackQueue(moduleName);
            
var cbitem = new _module.prototype.curCallBack(function() {
                _module.__checkURLs[moduleName] 
= true;
            });
            callbackQueue.push(cbitem);
            callbackQueue.push(
new _module.prototype.curCallBack(callback, context));
            callback 
= undefined;
            context 
= undefined;
        }
    }
    
    
if (reg) {
        
// 先处理被依赖的模块
        for (var r=reg.requirements.length-1; r>=0; r--) {
            
if (_module.registered[reg.requirements[r].name]) {
                _module(reg.requirements[r].name, 
function() {
                    _module(moduleName, callback, context); 
                }, context);
                
return;
            }
        }
        
        
// load每个模块
        for (var u=0; u<reg.urls.length; u++) {
            
if (u == reg.urls.length - 1) {
                
if (callback) {
                    _module.load(reg.name, reg.urls[u], reg.asyncWait, 
new _module.prototype.curCallBack(callback, context));
                } 
else {
                    _module.load(reg.name, reg.urls[u], reg.asyncWait);
                }
            } 
else {
                _module.load(reg.name, reg.urls[u], reg.asyncWait);
            }
        }
        
    } 
else {
        
!!callback && callback.call(context);
    }
}
    
_module.prototype 
= {

    
/**
     * Method 模块注册
     * @param (String or Object) 注册的模块名或者对象字面量
     * @param (Number) *optional 异步等待时间
     * @param (String or Array) 注册模块对应的url地址
     * @return (Object) 注册模块的相关信息对象字面量
    *
*/
    register : 
function(name, asyncWait, urls) {
        
var reg;
        
if (typeof(name) == "object") {
            reg 
= name;
            reg 
= new _module.prototype.__register(reg.name, reg.asyncWait, urls);
        } 
else {
            reg 
= new _module.prototype.__register(name, asyncWait, urls);
        }
        
if (!_module.registered) _module.registered = { };
        
if (_module.registered[name] && window.console) {
            window.console.log(
"Warning: Module named \"" + name + "\" was already registered, Overwritten!!!");
        }
        _module.registered[name] 
= reg;
        
return reg;
    },
    
// -- 注册模块的行动函数,并提供链式调用
    __register : function(_name, _asyncWait, _urls) {
        
this.name = _name;
        
var a=0;
        
var arg = arguments[++a];

        
if (arg && typeof(arg) == "number") { this.asyncWait = _asyncWait } else { this.asyncWait = 0 }
        
this.urls = new Array();
        
if (arg && arg.length && typeof(arg) != "string") {
            
this.urls = arg;
        } 
else {
            
for (a=a; a<arguments.length; a++) {
                
if (arguments[a] && typeof(arguments[a]) == "string"this.urls.push(arguments[a]);
            }
        }
        
// 依赖列表
        this.requirements = new Array();
        
        
this.require = function(resourceName) {
            
this.requirements.push({ name: resourceName });
            
return this;
        }
        
this.register = function(name, asyncWait, urls) {
            
return _module.register(name, asyncWait, urls);
        }
        
return this;
    },

    defaultAsyncTime: 
5000,
    
    
// -- 处理加载模块逻辑
    load: function(moduleName, scriptUrl, asyncWait, cb) {
        
if (asyncWait == undefined) asyncWait = _module.defaultAsyncTime;
        
        
if (!_module.loadedscripts) _module.loadedscripts = new Array();

         
var callbackQueue = _module.prototype.getCallbackQueue(scriptUrl);
         callbackQueue.push(
new _module.prototype.curCallBack( function() {
             _module.loadedscripts.push(_module.registered[moduleName]);
             _module.registered[moduleName] 
= undefined;
         }, 
null));
         
if (cb) {
             callbackQueue.push(cb);
             
if (callbackQueue.length > 2return;
         }
        
         _module.loadScript(scriptUrl, asyncWait, callbackQueue);
    }, 
    
    
// -- 加载模块行动函数
    loadScript : function(scriptUrl, asyncWait, callbackQueue) {
        
var scriptNode = _module.prototype.createScriptNode();
        scriptNode.setAttribute(
"src", scriptUrl);
        
if (callbackQueue) {
            
// 执行callback队列
            var execQueue = function() {
                _module.__callbackQueue[scriptUrl] 
= undefined;
                
for (var q=0; q<callbackQueue.length; q++) {
                    callbackQueue[q].runCallback();
                }
                
// 重置callback队列
                callbackQueue = new Array(); 
            }
            scriptNode.onload 
= scriptNode.onreadystatechange = function() {
                
if ((!scriptNode.readyState) || scriptNode.readyState == "loaded" || scriptNode.readyState == "complete" || scriptNode.readyState == 4 && scriptNode.status == 200) {
                    asyncWait 
> 0 ? setTimeout(execQueue, asyncWait) : execQueue();
                }
            };
        }
        
var headNode = document.getElementsByTagName("head")[0];
        headNode.appendChild(scriptNode);
    },    
    
    
// -- 执行当前 callback
    curCallBack : function(_callback, _context) {
        
this.callback = _callback;
        
this.context = _context;
        
this.runCallback = function() {
            
!!this.context ? this.callback.call(this.context) : this.callback();
        };
    },
    
// -- 获取callback列表
    getCallbackQueue: function(scriptUrl) {
        
if (!_module.__callbackQueue) _module.__callbackQueue = {};    
         
var callbackQueue = _module.__callbackQueue[scriptUrl];        
         
if (!callbackQueue) callbackQueue = _module.__callbackQueue[scriptUrl] = new Array();
         
return callbackQueue;
    },
    
    createScriptNode : 
function() {
        
var scriptNode = document.createElement("script");
        scriptNode.setAttribute(
"type""text/javascript");
        scriptNode.setAttribute(
"language""Javascript");
        
return scriptNode;    
    }
    
}
// 提供静态方法
_module.register = _module.prototype.register;
_module.load 
= _module.prototype.load;
_module.defaultAsyncTime 
= _module.prototype.defaultAsyncTime;
_module.loadScript 
= _module.prototype.loadScript;
    
// 公开接口    
HR.module = _module;
})();

 

代码逻辑复杂了不少,比起之前那个漏洞百出的版本考虑的东西更多了一些,也改变了一些设计的思路。核心依然是提供了两个主要的方法 ,在命名空间HR下,模块注册HR.module.register(),它的arguments有3个,分别为模块名,异步加载等待时间,模块对应js文件路径。其中模块名和js路径是必须得。异步等待时间为可选,默认为0.可以这样使用它:HR.module.register('test1''scripts/test-1.js?')
同时一个模块可以由多个js文件组成,如:
HR.module.register('Test', ['scripts/test-1.js','scripts/test-2.js'])

 为了更方便的标记它的依赖,把依赖作为它链式的方法来处理了。比如模块有依赖的时候直接在其后添加require()即可。链式的风格参照jQuery。

HR.module.register('test1''test-script/test-1.js').require('a').require('b')
    .register(
'a''test-script/a.js')
    .register(
'b''test-script/b.js')

 

模块注册之后,不会加载,调用的时候才会异步的加载。并通过回调来执行加载后的操作。可以直接在页面load的时候进行异步加载,也可放在任何一个事件handle里面来进行按需加载。

HR.module('test1'function () {
    
//TODO
});

或者:多模块一起调用,执行回调:

 HR.module('test1', 'test2', function () {

    //TODO
});

或者写成数组的形式:

  HR.module(['test1', 'test2'], function () {

    //TODO
});

都行 

 

后来考虑到每次都必须先注册模块,才能使用,有点不方便。于是加了一个功能,如果直接用 url(...)的方式使用模块,可以省略注册模块这一步。
比如,可以不用register,直接
HR.module('url(scripts/test.js)'function () {
    
//TODO
});

 当然,这种方式就不能对当前调用的这个js添加依赖,所以只适合一些简单的情况。

【结束语】 

经过我简单的测试,上面贴的代码基本能处理我上面列的几个问题。(但不排除我考虑不周,测试不全,以及犯低级错误的情况。)可还有问题在这一版本中没有解决,就是依赖死循环的问题。比如a模块依赖b模块,b模块又依赖a模块。虽然这实际是不可能的,但不能排除编码者的书写失误或者某些极端情况。

 

 好了,大致就到此为止吧。代码仅仅做了简单的测试。感兴趣的朋友可以拍拍砖,一个人能力有限,在大家的指正下可能才能真正的完善,以及有可用性。

posted on 2011-01-15 13:49  岑安  阅读(2988)  评论(12编辑  收藏  举报

导航