grunt任务之seajs模块打包
grunt是前端流行的自定义任务的脚手架工具,我们可以使用grunt来为我们做一些重复度很高的事情,如压缩,合并,js语法检查等。通过定义grunt的配置文件Gruntfile.js,配置并注册grunt的任务,最终我们可以通过命令行来执行任务。
seajs主要用于模块化,通过define定义一个模块,可以通过require加载模块,exports导出模块。具体的seajs实现可通过本博客的系列博文--Seajs源码解析系列来进一步了解。
在实际生产中,如果紧紧定义一系列seajs模块而并不进行合并压缩的话,加载性能很低,原因大家都懂的,seajs在浏览器端处理依赖模块,并进行异步加载,这个过程中会有多个http请求,大大降低页面的加载速度。所以结合grunt构建工具,我们可以将模块的依赖处理放到服务端进行,并将所有模块合并压缩,完成生产所需的最终文件。
在seajs社区中,已经提供了一款npm模块,即grunt-cmd-transport。我们通过该模块给seajs模块命名,并处理各模块之间的依赖。这项工作听起来很简单,但是在笔者的实践过程中出现的问题却不少,因此本文着重讲解transport任务的相关配置。
grunt相关文件包括了2个,首先是Gruntfile.js,另一个是package.json文件。Gruntfile进行grunt任务的配置及注册,package.json用于向Gruntfile提供参数,并设置依赖的npm模块。
在下面package.json中,定义spm键,设置模块的别名,在Gruntfile中,通过pkg = grunt.file.readJSON()来读取package配置文件,并通过<%= pkg.spm.alias %>获取模块别名。
package.json { "name" : "HelloSeaJS", "version" : "1.0.0", "author" : "yang li", "spm": { "alias": { "jquery": "jquery" } }, "devDependencies" : { "grunt" : "0.4.1", "grunt-cmd-transport": "~0.2.0", "grunt-cmd-concat": "~0.2.0", "grunt-contrib-uglify" : "0.2.0", "grunt-contrib-clean" : "0.4.0" } }
接下来我们进行设置grunt。Gruntfile.js其实就是一个node模块,依然使用闭包将所有的逻辑进行包裹,并提供了grunt参数,通过grunt.initConfig进行任务的配置。
对于seajs模块而言,首先需要处理各模块之间的依赖,我们通过设置transport任务来完成。seajs遵循的是CMD规范,在定义模块时不需要制定模块名和模块的依赖组,只需设置工厂函数即可。其实在未使用grunt进行合并seajs时(即在浏览器端处理模块依赖),seajs设置模块id和uri相同,为绝对路径。在这个过程中有些小技巧,在Seajs源码解析系列中并未提到,现在在这里着重分析下:
<script src="../sea-debug.js"></script> <script> seajs.config({ base: "../gallery/", alias:{ jquery: 'jquery/jquery-1.11.1' } }) seajs.use("../application.js") </script>
对于上述代码,application.js并没有合并seajs模块,我们通过seajs.use创建了一个匿名use模块,通过
var mod = Module.get(uri, isArray(ids) ? ids : [ids])
来实现,并设置依赖。在此处,依赖为[‘../application.js’];然后设置use模块的callback,并调用load函数加载依赖模块。在load函数中,use模块调用resolve函数解析出依赖的绝对路径,即[‘http://localhost:63342/mywork/js/application.js’],并创建一个新的Module表示该模块,这里用appMod表示,并以uri为key保存到modCache中。调用appMod.fetch加载对应的文件并设置回调函数onRequest,在application.js中定义了一个匿名模块define(function(){return {};}),此时模块的配置信息
meta = { id: id, uri: Module.resolve(id), // 绝对url deps: deps, factory: factory }
中id=‘undefined’,url=’’,
meta.uri ? Module.save(meta.uri, meta) : // Save information for "saving" work in the script onload event anonymousMeta = meta
由于此时meta.uri为空,因此meta信息保存在全局变量anonymousMeta中,用于后续处理。
红色字体强调了在调用fetch时设置的回调onRequest函数,当文件加载完毕,执行onRequest,
function onRequest() { delete fetchingList[requestUri] fetchedList[requestUri] = true // Save meta data of anonymous module if (anonymousMeta) { Module.save(uri, anonymousMeta) anonymousMeta = null } // Call callbacks var m, mods = callbackList[requestUri] delete callbackList[requestUri] while ((m = mods.shift())) m.load() }
此时anonymousMeta已在模块define时设置,因此将该meta配置文件配置到uri对应的Module对象上,
Module.save = function(uri, meta) { var mod = Module.get(uri) // Do NOT override already saved modules if (mod.status < STATUS.SAVED) { mod.id = meta.id || uri mod.dependencies = meta.deps || [] mod.factory = meta.factory mod.status = STATUS.SAVED } }
由于meta.id=undefined,因此最终mod.id=uri。
对于通过define(id,deps,function(){})设置了id的具名模块,是根据id生成uri。在meta中,通过Module.resolve(id)完成,
Module.resolve = function(id, refUri) { if (!id) return "" id = parseAlias(id) id = parsePaths(id) id = parseVars(id) id = normalize(id) var uri = addBase(id, refUri) uri = parseMap(uri) return uri }
通过对id的一系列设置(别名解析,路径修正,变量解析以及添加扩展名,最终添加协议等)生成uri。
transport任务是打包seajs模块的难点,上节提到了seajs模块的id和uri之间的关系,它们是由seajs来维护的。但是如果通过grunt对seajs进行打包,则模块之间的关系由transport来维护。通过transport生产的seajs模块,有一个显著的变化,即匿名模块变为了具名模块,并且设置了依赖模块。
下面通过配置项来讲解transport任务:
grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), transport : { options : { path: ['.'], alias: '<%= pkg.spm.alias %>' // 注意在package.json中jquery的alias设置 }, utils: { // 存放引用的模块,设定模块名和依赖,模块的idleading要与在application中引用的路径一致 options: { idleading: '../dist/src/' }, files: [{ expand: true, cwd: 'lib/src', src: '*', filter: 'isFile', dest: '.build/lib/src' }] }, application : { options: { idleading: '../dist/' }, files: [ { expand: true, cwd : 'lib', src : 'application.js', filter : 'isFile', dest : '.build/lib' } ] } } }
在options中,设定了路径为‘.’,即相对于Gruntfile文件的当前路径,alias为package.json中定义的alias;在utils任务中,设置了idleading选项,最终模块的id = idleading + 文件名。值得注意的是idleading路径的设置,这里需要小心设置,它是根据引用最终打包后文件的html的位置决定的。最后,将lib/src下的所有文件设置完id和依赖后放到.build/lib/src下。application任务和utils任务类似,只是单独设置application.js文件的id和依赖。
着重讲解idleading的设置。我们计划将生成的文件(处理完依赖且合并压缩后的文件)放到dist文件夹下面,最终通过view/hello.html引用,