时间:2015-08-9 作者:剧中人
小剧客栈一直使用Sea.js实现模块化。开发模式下碎片化加载各个资源,极大的方便了代码的调试,无需过多顾虑模块的增删。
本地开发模式下这样处理固然效率很高,然而在发布上线之后,若再以碎片化文件加载模块,大量的并发数会明显拖慢页面加载速度。因此绝大多数同学会选择使用spm、grunt或gulp等工具来合并碎片资源。
显然,在页面碎片化资源体积的总和并不是很大的情况下,这样的好处极为明显,因为JS资源变成两个,一个Sea.js文件,起着大总管的作用,一个合并后的文件。
聪明的同学们可能会发现,项目里可能会有一些基础类库,如jquery,如果一股脑的合并起来,每次发布新版本都会重新加载这部分代码,而这部分代码一般是固化不会被修改的。于是优化后文件结构变成了另外两个打包后的文件,一个是基础类库 + 大总管Sea.js,另一个则是项目开发的碎片文件总和。既避合理利用了浏览器的缓存,又能相对高效的处理版本的更替。
相信做到这样,项目已经比较令人满意的运行了。此时引发了小剧去思考另一个问题。线上的Sea.js 起到的作用是什么?
A Module Loader for Web。
Sea.js 官网是这样介绍自己的,整理下来就是模块管理、模块加载。
然而在一个模块已经完全合并的项目里,模块加载这部分功能本质上已经完全是冗余的。有什么方式做改进么?
既然模块加载已经不需要了,那我们是不是只要实现模块管理这一部分就可以替代Sea.js在项目中的作用了呢?
貌似是可以的,那我们研究一下经过抽出(transport)、合并之后的代码长什么样子。下面是两个模块的例子。
//Module navigation
define("public/js/navigation", [], function(require, exports,module) {
//......
return nav;
});
//Module page blog
define("public/js/blogList", [ "public/js/stick", "public/js/tie" ], function(require, exports,module) {
//......
exports.page = page;
});
//Module pagination
define("public/js/pagination", [], function(require, exports,module) {
//......
module.exports= pagination;
});
经过分析会发现,每个标准的Sea.js模块都被转化为上面这种格式,每个模块皆是调用define来实现自身定义。
Sea.js意为海洋,我给自己这个阉割版的Sea.js取名叫Seaport 即海港。造轮子之前还要搞明白一件事情,就是一个模块是如何被定义的?
Seaport 定义了一个全局的define方法,用来根据模块定义接收到的各个参数完成模块初始化。具体步骤为:
既然发现了模块定义的模式,那么就可以开始实现模块管理的核心部分了。这里主要需要实现这么几点:
用过Sea.js的小伙伴了解,模块定义的时候有三种提供模块接口的方式。
return
的返回值,如上面代码的第一个模块;module.exports
返回模块本身。根据这三种模块返回方式,即可编写出下面的接收模块初始化结果,id即为path,factory为传入define参数的function。
//缓存模块对象
var modules = {};
/**
* 初始化模块(依赖全部加载完毕方可执行)
**/
function initModule (id,factory){
var module = {
exports: {}
},
returns = factory(require,module.exports,module);
//优先使用return传递的模块接口
modules[id] = returns || module.exports;
emitModuleInit(id);
}
每个模块的初始化,都必须延迟到依赖的模块初始化完成,因而需要监听所需模块是否全部初始化完成。
这一步小剧创建了一个事件集moduleInitEvents
,用来存储所有的监听模块初始化事件。当有模块初始化完成即调用emitModuleInit
方法通知模块已经初始化完成,onceModuleInit
用来监听对应模块初始化完成事件。
//模块加载完成回调事件集合
var moduleInitEvents = [];
//注册模块加载完成监听(仅触发一次)
function onceModuleInit(id,callback){
moduleInitEvents.push([
id,
callback
]);
}
//通知模块加载完成,并立即删除监听
function emitModuleInit(id,args){
for(var i = moduleInitEvents.length-1;i!=-1;i--){
if(moduleInitEvents[i][0] == id){
moduleInitEvents[i][1].apply(this,args);
moduleInitEvents.splice(i,1);
}
}
}
因为Sea.js加载模块可以省略.js
后缀名,为了统一模块名称定义方式,ID一律过滤.js
。根据前面分析模块是如何被定义可以把这一步拆解为这么几步:
/**
* 接收模块定义的主函数
**/
function define(id,depends,factory){
id = id.replace(/\.js$/,'');
var need_load = depends.length;
//等待依赖加载完毕,初始化模块
for(var index=depends.length-1;index>=0;index--){
if(modules[id]){
need_load--;
}else{
onceModuleInit(depends[index],function(){
need_load--;
if(need_load == 0){
initModule(id,factory);
}
});
}
}
if(need_load == 0){
initModule(id,factory);
}
}
Sea.js中引用模块绝大多数是通过require方法来拿到模块的接口,在Sea.js中可以说是除了define之外最重要的一个方法。然而因为前面几点的实现,这一步变得异常简单,因为我们已经可以直接从缓存中取到模块的接口。
//从缓存中读取模块
function require(id){
id = id.replace(/\.js$/,'');
return modules[id] ? modules[id] : null;
}
这样的确已经完成了模块间的相互通信及依赖关系的管理,但是项目一般是通过seajs.use
一个主文件,因此Seaport也提供了此接口,只是此方法并不等同于Sea.js里的seajs.use
,具体原因下文会说明。
Sea.js中常会通过map
配置文件版本,Seaport也增加了对应的支持。项目基础目录base
的配置也纳入了进来。
只要项目满足以下要求,Seaport这枚轮子就能圆润的滚起来了。
seajs.use
引用其他文件错,首先正面回答,Seaport完全替代不了Sea.js。因为Sea.js实际使用过程中,有着很多被我们忽略掉的细节。
seajs.use
也是同理哈哈,就知道你会这样反问,实话说Seaport也只是小剧在小剧客栈上精炼代码走的最后一公里,并不适合所有情况,再把上面这段话摘抄下来,如果你的项目同时满足下面的这些条件,还是可以一试的。
PS:实现Seaport的过程解开了小剧对模块化内部实现的郁积很久的小疑问,同时也了解到了模块化管理的一些精妙之处,如果你对此项目感兴趣,欢迎前来蹂躏小剧的代码:https://github.com/bh-lay/seaportjs。