首先要说明一下,为什么会有创建seajs minifier的想法。
首先,必须强调的一点是,我非常喜欢seajs的规范及风格,虽然我更早接触的是requirejs,但是我却更喜欢使用的是seajs。
而对于minifier,seajs官方推荐的包管理工具spm3,不可否认的是spm3很强大,但是由于个人性格原因,我却不太喜欢这种过于强大的东东,我更加喜欢一个功能对应一个框架那种简单的方式,意思就是minifier就是minifier,包管理工具就是包管理工具,两者不应该被合并在一起。
于是就有了编写seajs minifier的想法。好在spm3也单独推出了关于grunt-cmd-transport和grunt-cmd-concat的两个插件,所以seajs minifier的大部分思想都是基于这两个插件的基础上做的,也相当于是站在XX的肩膀上的感觉。
这篇博客也仅仅是为了记录一下创作这个的过程,包括一下个人的想法及思路。
好了,进入正题。
首先第一个面临的问题就是:
minifier应该做点什么?
对于我自己而言,minifier应该做的是,指定一个入口module,由minifier抽取出该module所需的依赖,并通过一些配置判定哪些module需要排除出去,最后将该入口module与其相关的需要组合在一起的module合并,并进行minify。
这是minifier需要做的基本功能。
首先第一步就是需要transport seajs module。
原因是seajs的define函数在没有id标识时,会自动根据标识规则为该module生成一个标识,并通过正则表达式找出require语句,从而得到该module所需的依赖。而要引用该module,则又会通过标识规则将引用的path转变成id,然后在cache中查找,找到了就不会再去加载文件,找不到则会根据id得到module的文件地址而动态下载文件。
于是,第一个问题就是,grunt-cmd-transport插件能将seajs module转变成正确的格式么?
建立一个测试工程试试。
目录结构(已包括grunt并添加了grunt-cmd-transport任务)
index.html
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
<script src="lib/sea-debug.js" id="seajsnode"></script>
<script>
seajs.use("config",function(config){
seajs.config(config);
seajs.use("main",function(main){
main();
})
})
</script>
</html>
config.js
define(function(){
return {
base : "./js",
alias:{
jquery : "http://code.jquery.com/jquery.min.js"
},
paths : {
"index":"page/index"
}
}
})
main.js
define(function(require){
require("jquery");
var index = require("index/index");
return function(){
$("body").append("<div>main method 启动了</div>");
index.render();
}
});
index.js
define(function(require,exports){
require("jquery");
exports.render = function(){
$("body div").append("index page 启动");
}
});
运行index.html,界面上显示
main method 启动了<span style="font-family: Arial, Helvetica, sans-serif;">index page 启动</span>
OK,seajs测试工程搭建成功。
接着就是尝试使用transport。添加grunt、grunt-contrib-clean、grunt-cmd-transport。
编写gruntfile.js
module.exports = function(grunt){
grunt.initConfig({
transport: {
js : {
options:{
debug:false
},
files:[{
expand:true,
cwd : "js",
src : "**/*.js",
dest : "tmp/transport"
}]
}
},
clean:{
transports:["tmp"]
}
});
grunt.loadNpmTasks("grunt-contrib-clean");
grunt.loadNpmTasks('grunt-cmd-transport')
// By default, lint and run all tests.
grunt.registerTask('default', ['clean','transport']);
}
得到的结果为:
tmp目录结构
main.js
define("main", [ "jquery", "index/index" ], function(require) {
require("jquery");
var index = require("index/index");
return function() {
$("body").append("<div>main method 启动了</div>");
index.render();
};
});
index.js
define("page/index/index", [ "jquery" ], function(require, exports) {
require("jquery");
exports.render = function() {
$("body div").append("index page 启动");
};
});
同时,控制台报了3个warning
Warning: can't find module jquery Used --force, continuing.
Warning: can't find module index/index Used --force, continuing.
Warning: can't find module jquery Used --force, continuing.
transport 2 files
由结果可见,配置中的cwd地址指定的文件夹为base地址,在transport过程中,根据这个base地址去生成module标识。
在这次转换过程中,cwd指定地址为js,main在js文件夹下,于是module标识为文件名,index的地址相对于js文件夹为page/index/index,于是module标识为"page/index/index".而在js目录中无法找到“jquery.js”,所以当转换main的时候报了第一个warning,同样的道理,index相对于main的地址为page/index/index,顾通过"index/index"去找也找不到,故报出第二个warning,以此类推第三个warning的原因也就明白了。
手动将index.js的内容加入到main.js中去,并更改config里的base为"./tmp/transport",再次运行index.html.
界面显示
main method 启动了<span style="font-family: Arial, Helvetica, sans-serif;">index page 启动</span>
居然运行成功了,调试了一下,发现,因为不匹配任何一个规则,于是用了该url直接作为module的标识地址,而这个地址正好是文件的正式地址,从而在main module查找依赖时,通过标识匹配转换"index/index"正好与该标识相同,因此可以运行成功。
而一旦我们将guntfile.js中的cwd地址更换一下,这转换后,这index.html就无法运行了。由此我们可以认为transport的cwd地址必须和seajs中定义的config地址保持一致才行,这样就可以保证针对于seajs的base或者是transport的根地址,两个通过标识转换出来的结果是一致的。
阅读transport的源码发现,transport一个module中除了会取出当前module直接依赖的deps,还会去寻找其deps所依赖的deps,最后将所有的deps合并在一起,形成deps数组。关于这个操作,目前还没想明白是否有必要。
既然transport的插件能满足目前所需,那么就开始第二步吧。
第二步就是concat,先说明一下grunt-cmd-concat做的事,指定一个入口module,取出该module的dependencies,然后将其拼接在一个文件中输出。
貌似和我的想法是一致的,但是,concat插件在查找依赖的时候,有3种include的模式,self、relative、all。
self模式是指不拼接依赖。relative模式则会根据形如"./"的相对于该module的文件。all则是全部拼接。
突然想起曾经看seajs文档时发现的一句话,官方推荐的是与module相关的deps尽量采取相对路径的写法,看了这个插件才明白是什么意思。
而实际使用过程中,涉及了很多的具体情况,比如,在config中定义了alias,paths,vars,base,preload,map等等配置,以至于如果仅仅分析相对路径的写法又显得简单了点。仔细查看文档,发现了concat的配置中有paths的设置,但是并没有写得很详细。
查看源码后发现
options.paths.some(function(base) {
var filepath = path.join(base, file);
if (grunt.file.exists(filepath)) {
grunt.log.verbose.writeln('find module "' + filepath + '"');
fpath = filepath;
return true;
}
});
paths的配置仅仅用在了这样一个地方。不难发现上面的意思是通过定义paths去帮助module查找到dependency,但是是采用遍历的后判定文件是否存在的方式。
也就是说,如果你定义了2个paths,且这两个paths里都有依赖的文件,则会一起拼接到该文件中。
老实说,这个paths的意义并不是很大。
这样,难道就没有办法实现一个完美的拼接方案了么?
现阶段有了下面的几个想法
1、通过transport之后的module,按照我们之前的测试,发现其dependencies里的路径并没有发生改变。该路径可能是相对路径,也可能是绝对路径,也可能是通过alias,paths等seajs的配置定义出来的路径。
2、在seajs的配置中,vars是属于一个变化的参数,有使用vars的路径往往都是变化的路径,而我们在做minifier的时候,变化的路径并不会拼接在一起,比如locale,会根据当前的应用情况而选择不同的locale文件,这个时候把所有的locale文件并在一起也是不符合常规的。这种情况我们就不考虑了。
3、在seajs中map的配置情况比较复杂,但这只是加载文件的一种变化,如果把这种变化拼接进minfier中,也不太符合常规,这种情况也不考虑了。
4、在seajs中还有preload的配置。虽然2.0版本后,已经讲preload做成了插件移出了主代码中。但是这个配置依然需要考虑。preload的意义在意use任何module之前先加载一些js,或者module。虽然在页面有多个use的情况下preload是不太好拼接进module中,但在单个module的情况下,preload还是有可能拼接进module中的。
所以,以上所述表明,我们要做拼接需要考虑到配置中的alias,paths,base和preload。且preload是否需要拼接进module也要可以选择。
如果,我们把seajs config里的这些配置移到concat上去帮忙查找dependency,是否可行呢?
先细想一下流程。
首先,transport所有的js文件,其次,通过定义一个入口module,查找出该module的dependencies,要先满足preload 的module可以选择性的拼接进输出文件中,那么此时就需要一个配置,配置的类型这如同preload:[]的形式,当该数组为空的时候,我们就跳过这一步。然后通过alias,paths,base等配置去解析dependencies数组中的路径,将其还原成文件的真实位置,最后找到文件,读取内容拼接进module中输出。
看似流程的关键在于通过alias,paths,base等参数将dependencies中的路径还原这一步最为关键。不过这一步,完全可以参照seajs解析路径的方式去解析。因为seajs在生成module标识的时候,采用的就是这种解析方式。
那么,就上手试试
终于理解了“transport一个module中除了会取出当前module直接依赖的deps,还会去寻找其deps所依赖的deps,最后将所有的deps合并在一起,形成deps数组。”
有了这一步,在concat的过程中就不想要去递归的查找dependencies的依赖而进行拼接。只是,为什么要把这一步放在transport过程中呢?有点不太理解。
文章太长了。。新开一篇继续吧。