上一篇分享了關於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 源碼的解析就差很少了,更多的東西須要不斷在實踐中去理解,關於源碼的詳細註釋請看 這裏。函數