上一篇分享了关于grunt中任务运行相关源码的解析,这一篇来分享grunt中跟任务注册相关的源码解析,废话很少说,开始吧。javascript
跟任务注册相关的两个方法是 grunt.registerTask
和grunt.registerMultiTask
。这两个方法都位于 lib/grunt/task.js
文件中。首先来看看 grunt.registerTask
方法的实现,这个方法还涉及到了 lib/util/task.js
文件中的 registerTask
方法。java
//lib/grunt/task.js task.registerTask = function(name) { // 将任务加入到registry中 registry.tasks.push(name); // 调用parent的registerTask方法注册任务 parent.registerTask.apply(task, arguments); // 调用parent.registerTask方法以后,任务会被加入到_tasks缓存中 var thisTask = task._tasks[name]; // 复制任务的元数据 thisTask.meta = grunt.util._.clone(registry.meta); // 对注册的任务函数进行封装 // 在真实函数执行以前进行一些预处理 var _fn = thisTask.fn; thisTask.fn = function(arg) { // 缓存任务名称 var name = thisTask.name; // 初始化任务的errorcount errorcount = grunt.fail.errorcount; // 返回任务运行期间的errorcount Object.defineProperty(this, 'errorCount', { enumerable: true, get: function() { return grunt.fail.errorcount - errorcount; } }); // 将task.requires方法添加到this对象中 this.requires = task.requires.bind(task); // 将grunt.config.requires方法添加到this对象中 this.requiresConfig = grunt.config.requires; // options方法返回任务的相关option参数,能够经过参数覆盖默认的配置 this.options = function() { var args = [{}].concat(grunt.util.toArray(arguments)).concat([ grunt.config([name, 'options']) ]); var options = grunt.util._.extend.apply(null, args); grunt.verbose.writeflags(options, 'Options'); return options; }; // 初始化log输出工做 var logger = _fn.alias || (thisTask.multi && (!arg || arg === '*')) ? 'verbose' : 'log'; grunt[logger].header('Running "' + this.nameArgs + '"' + (this.name !== this.nameArgs ? ' (' + this.name + ')' : '') + ' task'); grunt[logger].debug('Task source: ' + thisTask.meta.filepath); // 运行真实注册的任务函数 return _fn.apply(this, arguments); }; return task; }; //lib/util/task.js // 注册任务 Task.prototype.registerTask = function(name, info, fn) { // 若是没有传递info,调整参数 // 好比grunt.registerTask('taskName',function(){})的状况 // 这时候info为function函数,因此把info赋值给fn if (fn == null) { fn = info; info = null; } // 若是fn是字符串或者字符串数组 // 好比grunt.registerTask('task',['task1','task2','task3'])的状况 var tasks; if (typeof fn !== 'function') { // 针对上面的状况,这时候tasks=['task1','task2','task3'] tasks = this.parseArgs([fn]); // 将任务的函数改成将每一个子任务添加到任务队列中 // 也就是分别将task1,task2和task3加入任务队列中 fn = this.run.bind(this, fn); fn.alias = true; // 这种状况下task至关于task1,task2和task3任务组合的别名 if (!info) { info = 'Alias for "' + tasks.join('", "') + '" task' + (tasks.length === 1 ? '' : 's') + '.'; } } else if (!info) { info = 'Custom task.'; } // 将任务加入到缓存中 this._tasks[name] = {name: name, info: info, fn: fn}; // 返回任务对象,支持链式调用 return this; };
在 registerTask
方法中,首先会调用 lib/util/task.js
中的 registerTask
方法,而在这个方法中会修正方法的参数,而后将任务对象加入到任务缓存中;接着回到 registerTask
方法中对注册的函数进行封装,在封装的函数中会在函数执行前进行一些初始化工做,最后再执行注册函数。git
下面来看看 grunt.registerMultiTask
方法的实现。这个方法是针对具备多个target的任务的注册。github
// 组成含有多target的task task.registerMultiTask = function(name, info, fn) { // 针对grunt.registerMultiTask('taskName',function(){})的状况 if (fn == null) { fn = info; info = 'Custom multi task.'; } var thisTask; task.registerTask(name, info, function(target) { var name = thisTask.name; // 得到除了任务名之外的参数 this.args = grunt.util.toArray(arguments).slice(1); // 若是没有指定target或者指定为*,那么运行因此target if (!target || target === '*') { return task.runAllTargets(name, this.args); } else if (!isValidMultiTaskTarget(target)) { // 若是存在不合法的target则抛出错误 throw new Error('Invalid target "' + target + '" specified.'); } // 判断是否存在对应target的配置 this.requiresConfig([name, target]); // options方法返回任务的相关option参数,能够经过参数覆盖默认的配置 this.options = function() { var targetObj = grunt.config([name, target]); var args = [{}].concat(grunt.util.toArray(arguments)).concat([ grunt.config([name, 'options']), grunt.util.kindOf(targetObj) === 'object' ? targetObj.options : {} ]); var options = grunt.util._.extend.apply(null, args); grunt.verbose.writeflags(options, 'Options'); return options; }; // 将target添加到this对象中 this.target = target; // 为this对象添加flags属性,而且初始化flags对象 // flags对象用来记录参数列表中是否存在对象的参数 // 若是存在值为true this.flags = {}; this.args.forEach(function(arg) { this.flags[arg] = true; }, this); // 将target的对于配置添加到this对象中 // 这个配置也就是咱们经过initConfig定义的配置 this.data = grunt.config([name, target]); // 将封装以后的files对象添加到this对象中 this.files = task.normalizeMultiTaskFiles(this.data, target); // 将src的相关值添加到this的filesSrc属性中 Object.defineProperty(this, 'filesSrc', { enumerable: true, get: function() { return grunt.util._(this.files).chain().pluck('src').flatten().uniq().value(); }.bind(this) }); // 调用任务注册函数,传入相应参数 return fn.apply(this, this.args); }); // 缓存任务 thisTask = task._tasks[name]; // 将任务标记为多任务 thisTask.multi = true; };
在 registerMultiTask
方法中会调用 registerTask
方法注册任务,而在注册的函数中首先会根据传入的target执行相应操做,若是没有传入target或者传入 *
那么就调用 runAllTargets
方法将全部target都加入任务队列中,不然执行对应的target,接着获取target的相应配置,调用 normalizeMultiTaskFiles
方法将配置数据转换为内部的file对象(PS:这个过程是grunt比较方便的一个地方,它有多种形式来定义文件路径之间的映射,而且支持多种表达式,file对象也是我一开始看grunt的东西,以为这很神奇。后面我会说到这个方法),最后调用任务实际注册的函数。数组
下面咱们就来看看 normalizeMultiTaskFiles
方法的具体实现。缓存
task.normalizeMultiTaskFiles = function(data, target) { var prop, obj; var files = []; if (grunt.util.kindOf(data) === 'object') { if ('src' in data || 'dest' in data) { /* *Compact Format的状况,好比: *'bar' : { * 'src' : ['a.js','b.js'] , * 'dest' : 'c.js' *} */ obj = {}; // 将除了options之外的配置复制到obj对象中 for (prop in data) { if (prop !== 'options') { obj[prop] = data[prop]; } } files.push(obj); } else if (grunt.util.kindOf(data.files) === 'object') { /* *Files Object Format的状况,好比: *'bar' : { * 'files' : { * 'c.js' : ['a.js','b.js'] * } *} */ for (prop in data.files) { files.push({src: data.files[prop], dest: grunt.config.process(prop)}); } } else if (Array.isArray(data.files)) { /* *Files Array Format的状况,好比: *'bar' : { * 'files' : [ * {'src':['a.js','b.js'],'dest':'c.js'}, * {'src':['a.js','b.js'],'dest':'d.js'} * ] *} */ grunt.util._.flatten(data.files).forEach(function(obj) { var prop; if ('src' in obj || 'dest' in obj) { files.push(obj); } else { for (prop in obj) { files.push({src: obj[prop], dest: grunt.config.process(prop)}); } } }); } } else { /* *Older Format的状况,好比: *'bar' : ['a.js','b.js'] */ files.push({src: data, dest: grunt.config.process(target)}); } // 若是没找到合法的文件配置对象,那么返回空的文件数组 if (files.length === 0) { grunt.verbose.writeln('File: ' + '[no files]'.yellow); return []; } // 对须要扩展的文件对象进行扩展 files = grunt.util._(files).chain().forEach(function(obj) { // 调整obj.src属性,使其成为一维数组 // 若是不存在src属性,则直接返回不须要进行任何操做 if (!('src' in obj) || !obj.src) { return; } // 若是obj.src是数组则压缩成一维数组,不然直接转换为数组 if (Array.isArray(obj.src)) { obj.src = grunt.util._.flatten(obj.src); } else { obj.src = [obj.src]; } }).map(function(obj) { // 在obj的基础上建立对象,移除不须要的属性,处理动态生成src到dest的映射 var expandOptions = grunt.util._.extend({}, obj); delete expandOptions.src; delete expandOptions.dest; // 利用expand中的配置,扩展文件映射关系,并返回扩展后的file对象 if (obj.expand) { return grunt.file.expandMapping(obj.src, obj.dest, expandOptions).map(function(mapObj) { // 将obj对象复制为result对象 var result = grunt.util._.extend({}, obj); // 将obj对象复制为result的orig属性 result.orig = grunt.util._.extend({}, obj); // 若是src或dest为模板,则解析为真正的路径 result.src = grunt.config.process(mapObj.src); result.dest = grunt.config.process(mapObj.dest); // 移除不须要的属性 ['expand', 'cwd', 'flatten', 'rename', 'ext'].forEach(function(prop) { delete result[prop]; }); return result; }); } // 复制obj对象,而且向副本添加一个orig属性,属性的值也是obj对象的一个副本 // 保存一个obj的副本orig是由于在后面可能会对result中的属性进行修改 // orig使得result中能够访问到原始的file对象 var result = grunt.util._.extend({}, obj); result.orig = grunt.util._.extend({}, obj); if ('src' in result) { // 若是result对象中具备src属性,那么给src属性添加一个get方法, // 方法中对src根据expand进行扩展 Object.defineProperty(result, 'src', { enumerable: true, get: function fn() { var src; if (!('result' in fn)) { src = obj.src; // 将src转换为数组 src = Array.isArray(src) ? grunt.util._.flatten(src) : [src]; // 根据expand参数扩展src属性,并把结果缓存在fn中 fn.result = grunt.file.expand(expandOptions, src); } return fn.result; } }); } if ('dest' in result) { result.dest = obj.dest; } return result; }).flatten().value(); // 若是命令行带有--verbose参数,则在log中输出文件路径 if (grunt.option('verbose')) { files.forEach(function(obj) { var output = []; if ('src' in obj) { output.push(obj.src.length > 0 ? grunt.log.wordlist(obj.src) : '[no src]'.yellow); } if ('dest' in obj) { output.push('-> ' + (obj.dest ? String(obj.dest).cyan : '[no dest]'.yellow)); } if (output.length > 0) { grunt.verbose.writeln('Files: ' + output.join(' ')); } }); } return files; };
grunt提供了多种格式来进行文件参数的配置,normalizeMultiTaskFiles
方法会将相应target的配置转换为一个files
数组,这个数组中存放的是每对文件的源地址和目的地址,该方法还负责对expand
属性相关参数进行解析,最后生成多个源地址和目的地址对存在在files
数组中。这个方法大大方便了grunt中关于文件的操做和配置。app
到这里 grunt 源码的解析就差很少了,更多的东西须要不断在实践中去理解,关于源码的详细注释请看 这里。函数