模块加载--站在巨人肩膀上的版本
好久没有更新博客了。最近基本上也一直在闭门造车轮。在模块加载的问题上苦苦纠结。距离上次发的狗屁不通的一个版本,也过去了大概两个星期的时间了。
说上次那篇博文狗屁不通的确不无道理。具体情况可以参考前文《关于前端基础框架的思考》。现在看来的确到处都是漏洞,没有处理不同操作的并行,也没有处理依赖和并行混淆的情况,连但操作,无并发的递归的依赖情况可能都有漏洞。甚至犯了极其低级的错误都还不知道。幸亏一位园友的提醒... 真是惭愧。
其他的也不多说了,还是回到正题吧。又经过两个星期的辗转反侧,查阅了各大框架的源码,最终有了一个稍微有一点可用性的版本。只敢说是站在巨人肩膀上的版本,自知若没有前人的成果作为引导和启发,恐怕不是两个星期的事了...
【写代码前要考虑的问题】
关于模块加载器,我个人觉得是一个框架不能缺的东西,或者说是核心的东西之一。这也是我为什么会想从这个地方开始我的自造车轮的原因。就是为了方便的管理我们需要颗粒化的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(0, 4) == "url(") {
dsrc = src.substring(4, src.length - 1);
}
var r = _module.registered[dsrc];
return (!r && (!_module.__checkURLs || !_module.__checkURLs[dsrc]) && src && src.length > 4 && src.substring(0, 4) == "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(0, 4) == "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 > 2) return;
}
_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文件组成,如:
为了更方便的标记它的依赖,把依赖作为它链式的方法来处理了。比如模块有依赖的时候直接在其后添加require()即可。链式的风格参照jQuery。
.register('a', 'test-script/a.js')
.register('b', 'test-script/b.js')
模块注册之后,不会加载,调用的时候才会异步的加载。并通过回调来执行加载后的操作。可以直接在页面load的时候进行异步加载,也可放在任何一个事件handle里面来进行按需加载。
//TODO
});
或者:多模块一起调用,执行回调:
HR.module('test1', 'test2', function () {
});
或者写成数组的形式:
HR.module(['test1', 'test2'], function () {
});
都行
后来考虑到每次都必须先注册模块,才能使用,有点不方便。于是加了一个功能,如果直接用 url(...)的方式使用模块,可以省略注册模块这一步。
比如,可以不用register,直接
//TODO
});
当然,这种方式就不能对当前调用的这个js添加依赖,所以只适合一些简单的情况。
【结束语】
经过我简单的测试,上面贴的代码基本能处理我上面列的几个问题。(但不排除我考虑不周,测试不全,以及犯低级错误的情况。)可还有问题在这一版本中没有解决,就是依赖死循环的问题。比如a模块依赖b模块,b模块又依赖a模块。虽然这实际是不可能的,但不能排除编码者的书写失误或者某些极端情况。
好了,大致就到此为止吧。代码仅仅做了简单的测试。感兴趣的朋友可以拍拍砖,一个人能力有限,在大家的指正下可能才能真正的完善,以及有可用性。