看看 Grunt 的源碼(三):grunt 任務註冊相關源碼解析

上一篇分享了關於grunt中任務運行相關源碼的解析,這一篇來分享grunt中跟任務註冊相關的源碼解析,廢話很少說,開始吧。javascript

跟任務註冊相關的兩個方法是 grunt.registerTaskgrunt.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 源碼的解析就差很少了,更多的東西須要不斷在實踐中去理解,關於源碼的詳細註釋請看 這裏函數

最後,安利下個人我的博客,歡迎訪問: http://bin-playground.top

相關文章
相關標籤/搜索