看看 Grunt 的源碼(二):grunt 任務運行相關源碼解析

上一篇分享了關於grunt-cli的源碼解析,這篇開始grunt核心部分代碼的解析,仍是從上一篇結束部分開始。javascript

//調用grunt執行任務
require(gruntpath).cli();

gruntpath是經過解析獲得的grunt.js的文件路徑,經過require方法加載grunt模塊而後調用模塊的cli方法來運行命令行最後運行命令行中的任務。java

咱們先從大致上看看grunt從輸入命令行到任務運行完畢整個過程當中都通過了哪些步驟。下圖是我根據源碼得出的一個流程圖。node

圖片描述

  1. 首先,咱們輸入命令行以後調用require(gruntpath).cli()方法,在cli方法中會初始化命令行的默認參數列表,解析輸入命令行的參數以及任務名稱shell

  2. 而後調用grunt.tasks方法,將任務參數和名稱傳入。在grunt.tasks方法中,會進一步對參數進行解析,初始化log功能,若是參數帶有version或者help選項那麼直接執行相應的函數,不然就解析任務名稱。npm

  3. 接着調用task.init方法。加載Gruntfile.js文件,註冊任務信息以及配置信息。數組

  4. 接着調用task.run方法。task.run方法並不會運行任務,而是把任務相關信息添加到任務隊列中。緩存

  5. 最後纔是調用task.start方法來依次運行任務隊列中的任務。
    下面來一步步解析grunt核心源碼。首先,來看看lib/grunt/cli.js文件中的代碼。app

// 執行命令行時執行的函數
var cli = module.exports = function(options, done) {
  // 利用傳遞的參數設置cli.options對象,可是不覆蓋命令行的參數
  if (options) {
    Object.keys(options).forEach(function(key) {
      if (!(key in cli.options)) {
        // 若是輸入的命令行中不存在這個參數,那麼把它加入到cli的options屬性中
        cli.options[key] = options[key];
      } else if (cli.optlist[key].type === Array) {
        // 若是輸入的命令行中存在這個參數,而且參數的類型是數組,那麼把它加入到數組尾部
        [].push.apply(cli.options[key], options[key]);
      }
    });
  }

  // 運行任務
  grunt.tasks(cli.tasks, cli.options, done);
};

// 默認的參數選項列表
var optlist = cli.optlist = {
  help: {
    short: 'h',
    info: 'Display this help text.',
    type: Boolean
  },
  base: {
    info: 'Specify an alternate base path. By default, all file paths are relative to the Gruntfile. ' +
          '(grunt.file.setBase) *',
    type: path
  },
  color: {
    info: 'Disable colored output.',
    type: Boolean,
    negate: true
  },
  gruntfile: {
    info: 'Specify an alternate Gruntfile. By default, grunt looks in the current or parent directories ' +
          'for the nearest Gruntfile.js or Gruntfile.coffee file.',
    type: path
  },
  debug: {
    short: 'd',
    info: 'Enable debugging mode for tasks that support it.',
    type: [Number, Boolean]
  },
  stack: {
    info: 'Print a stack trace when exiting with a warning or fatal error.',
    type: Boolean
  },
  force: {
    short: 'f',
    info: 'A way to force your way past warnings. Want a suggestion? Don\'t use this option, fix your code.',
    type: Boolean
  },
  tasks: {
    info: 'Additional directory paths to scan for task and "extra" files. (grunt.loadTasks) *',
    type: Array
  },
  npm: {
    info: 'Npm-installed grunt plugins to scan for task and "extra" files. (grunt.loadNpmTasks) *',
    type: Array
  },
  write: {
    info: 'Disable writing files (dry run).',
    type: Boolean,
    negate: true
  },
  verbose: {
    short: 'v',
    info: 'Verbose mode. A lot more information output.',
    type: Boolean
  },
  version: {
    short: 'V',
    info: 'Print the grunt version. Combine with --verbose for more info.',
    type: Boolean
  },
  completion: {
    info: 'Output shell auto-completion rules. See the grunt-cli documentation for more information.',
    type: String
  },
};

// 利用optlist列表初始化aliases和known對象
// 傳遞給nopt模塊進行命令行參數解析
// nopt是一個用來解析命令行參數的第三方模塊
var aliases = {};
var known = {};

Object.keys(optlist).forEach(function(key) {
  var short = optlist[key].short;
  if (short) {
    aliases[short] = '--' + key;
  }
  known[key] = optlist[key].type;
});

var parsed = nopt(known, aliases, process.argv, 2);
// 獲取命令行中的任務名稱
cli.tasks = parsed.argv.remain;
// 得到命令行中的參數
cli.options = parsed;
delete parsed.argv;

// 初始化類型爲數組可是還沒被初始化的參數,好比npm和task
Object.keys(optlist).forEach(function(key) {
  if (optlist[key].type === Array && !(key in cli.options)) {
    cli.options[key] = [];
  }
});

這段代碼相對比較簡單,主要功能就是解析任務名和參數而後傳遞給grunt.tasks方法進行調用。
下面來看看grunt.js中關於grunt.tasks方法的代碼。異步

// 這個tasks方法通常只在grunt內部調用
// tasks方法用來將任務添加到任務隊列中,而且運行任務
grunt.tasks = function(tasks, options, done) {
  // option模塊對命令行參數進行包裝
  // init方法對參數進行了初始化,在方法內部判斷傳入參數是否爲空
  // 若是爲空則初始化爲空對象不然使用傳入的對象進行初始化
  option.init(options);

  var _tasks, _options;
  // option方法接受可變屬性的參數,
  // 若是傳入一個參數則在參數對象中找出對於的參數,
  // 若是傳入兩個參數則根據這兩個參數設置key-value鍵值對,並value
  // 同時方法內部會用正則匹配no-color、no-write的狀況,
  // 若是出現則設置option['color']或option['write']爲false,並返回false
  if (option('version')) {
    // 若是帶有version參數
    // 輸出版本信息
    log.writeln('grunt v' + grunt.version);

    if (option('verbose')) {
      // //輸出詳細信息,包括grunt的路徑
      verbose.writeln('Install path: ' + path.resolve(__dirname, '..'));

      grunt.log.muted = true;
      // 初始化任務系統,解析gruntfile以便輸出全部可用的任務
      grunt.task.init([], {help: true});
      grunt.log.muted = false;

      // 輸出可用的任務信息
      _tasks = Object.keys(grunt.task._tasks).sort();
      verbose.writeln('Available tasks: ' + _tasks.join(' '));

      // 輸出全部可用參數的詳細信息
      _options = [];
      Object.keys(grunt.cli.optlist).forEach(function(long) {
        var o = grunt.cli.optlist[long];
        _options.push('--' + (o.negate ? 'no-' : '') + long);
        if (o.short) { _options.push('-' + o.short); }
      });
      verbose.writeln('Available options: ' + _options.join(' '));
    }

    return;
  }

  // 初始化log的着色功能
  log.initColors();

  // 若是參數帶有help則輸出幫助信息
  if (option('help')) {
    help.display();
    return;
  }

  // 根據option輸出命令行參數,flags方法會過濾掉值爲空的參數
  verbose.header('Initializing').writeflags(option.flags(), 'Command-line options');

  // 判斷是否有傳入tasks參數而且任務長度大於0
  var tasksSpecified = tasks && tasks.length > 0;
  //將傳入參數進行轉換,轉換爲任務數組,若是沒有傳入有效的任務那麼使用默認default任務
  tasks = task.parseArgs([tasksSpecified ? tasks : 'default']);

  // 根據傳入的tasks參數初始化任務
  // 在方法中加載gruntfile.js文件,進行任務註冊和配置的解析
  // 也就是加載咱們編寫的任務代碼
  task.init(tasks, options);

  verbose.writeln();
  if (!tasksSpecified) {
    verbose.writeln('No tasks specified, running default tasks.');
  }
  verbose.writeflags(tasks, 'Running tasks');

  // 註冊異常處理函數,輸出異常信息
  var uncaughtHandler = function(e) {
    fail.fatal(e, fail.code.TASK_FAILURE);
  };
  process.on('uncaughtException', uncaughtHandler);

  task.options({
    error: function(e) {
      fail.warn(e, fail.code.TASK_FAILURE);
    },
    done: function() {
      // 當任務完成以後移除異常監聽函數,減小多餘的開銷
      process.removeListener('uncaughtException', uncaughtHandler);

      // 輸出最後的運行結果,失敗或者成功
      fail.report();

      if (done) {
        // 若是存在done函數的話,當完成任務時執行done函數
        done();
      } else {
        // 若是沒有done函數直接結束進程
        util.exit(0);
      }
    }
  });

  // 將任務依次加入內部的任務隊列中,run方法並不會運行任務,只是加入到隊列中
  tasks.forEach(function(name) { task.run(name); });
  // 開始運行任務隊列中的任務
  task.start({asyncDone:true});
};

grunt.tasks代碼中,首先會進行參數的初始化,接着判斷參數是否帶有version或者help選項,若是帶有這兩個選項就進行相應的工做而不運行任務任務,不然解析任務名進行任務初始化並添加到任務隊列中,最後運行任務。
grunt.tasks方法中比較重要的三個方法就是task.inittask.runtask.start方法。下面看看task.init方法的具體實現。這個方法位於lib/grunt/task.js文件中。async

// 初始化任務
task.init = function(tasks, options) {
  if (!options) { options = {}; }

  // 擁有init方法說明task是初始化任務,好比第三方插件
  var allInit = tasks.length > 0 && tasks.every(function(name) {
    var obj = task._taskPlusArgs(name).task;
    return obj && obj.init;
  });

  // 獲取gruntfile.js路徑,若是有指定路徑那麼直接使用不然在當前目錄及父目錄中查找
  var gruntfile, msg;
  if (allInit || options.gruntfile === false) {
    gruntfile = null;
  } else {
    gruntfile = grunt.option('gruntfile') ||
      grunt.file.findup('Gruntfile.{js,coffee}', {nocase: true});
    msg = 'Reading "' + (gruntfile ? path.basename(gruntfile) : '???') + '" Gruntfile...';
  }
  // 若是參數中將gruntfile設爲false,那麼說明任務是一個插件或者庫
  // 不作任何操做
  if (options.gruntfile === false) {
    // Grunt was run as a lib with {gruntfile: false}.
  } else if (gruntfile && grunt.file.exists(gruntfile)) {
    // 若是存在gruntfile
    grunt.verbose.writeln().write(msg).ok();
    // 修改進程的操做目錄,若是有指定base那麼使用base目錄不然就使用gruntfile所在的目錄
    process.chdir(grunt.option('base') || path.dirname(gruntfile));
    // 在verbose狀況下輸出Registering Gruntfile tasks信息
    loadTasksMessage('Gruntfile');
    // 加載gruntfile中的任務
    loadTask(gruntfile);
  } else if (options.help || allInit) {
    // 若是沒找到grunt可是有help參數的話,那麼不作任何操做
  } else if (grunt.option('gruntfile')) {
    // 若是指定了gruntfile參數可是找不到文件那麼輸出錯誤信息
    grunt.log.writeln().write(msg).error();
    grunt.fatal('Unable to find "' + gruntfile + '" Gruntfile.', grunt.fail.code.MISSING_GRUNTFILE);
  } else if (!grunt.option('help')) {
    grunt.verbose.writeln().write(msg).error();
    grunt.log.writelns(
      'A valid Gruntfile could not be found. Please see the getting ' +
      'started guide for more information on how to configure grunt: ' +
      'http://gruntjs.com/getting-started'
    );
    grunt.fatal('Unable to find Gruntfile.', grunt.fail.code.MISSING_GRUNTFILE);
  }

  // 加載用戶指定的npm包
  (grunt.option('npm') || []).forEach(task.loadNpmTasks);
  // 加載用戶指定的任務
  (grunt.option('tasks') || []).forEach(task.loadTasks);
};

在初始化任務以後grunt.tasks方法會調用task.run方法,將任務添加到任務隊列中等待執行。下面是task.run方法的代碼,它也是位於lib/util/task.js文件中。

// 將任務加入到隊列中
Task.prototype.run = function() {
  // 將參數轉換爲數組而且根據參數構建任務對象
  var things = this.parseArgs(arguments).map(this._taskPlusArgs, this);
  // 找出沒法構建的任務
  var fails = things.filter(function(thing) { return !thing.task; });
  if (fails.length > 0) {
    // 若是存在沒法構建的任務,拋出錯誤並返回
    this._throwIfRunning(new Error('Task "' + fails[0].nameArgs + '" not found.'));
    return this;
  }

  // 將任務加入到任務隊列相應的位置
  this._push(things);
  // 支持鏈式調用
  return this;
};
// 將任務名分離爲真實運行的任務名和參數的對象,好比:
// 'foo'          ==>  任務名爲foo,沒有參數
// 'foo:bar:baz'  ==>  若是'foo:bar:baz'任務存在,那麼任務名爲'foo:bar:baz',沒有參數
//                ==>  若是'foo:bar'任務存在,那麼任務名爲'foo:bar',參數爲'baz'
//                ==>  若是'foo'任務存在,那麼任務名爲'foo',參數爲'bar'和'baz'
Task.prototype._taskPlusArgs = function(name) {
  // 將傳入的任務名根據冒號轉換爲數組
  var parts = this.splitArgs(name);
  // 從數組最後開始遍歷數組
  var i = parts.length;
  var task;
  do {
    // 將0到i的數組轉換爲任務名,用冒號隔開
    // 而後根據獲得的任務名從任務緩存中獲得相應的任務
    task = this._tasks[parts.slice(0, i).join(':')];
    // 若是相應任務不存在,那麼i減1,知道i等於0
  } while (!task && --i > 0);
  // 除了任務名之外的部分屬於參數
  var args = parts.slice(i);
  // 根據參數列表,獲得相應的boolean型標記
  var flags = {};
  args.forEach(function(arg) { flags[arg] = true; });
  // 返回構建的任務對象,包括任務名和任務參數
  return {task: task, nameArgs: name, args: args, flags: flags};
};

task.run方法中,首先將參數進行分離,分隔出任務名和參數,而後利用任務名和參數構建一個任務對象,最後將這個對象放入任務隊列中,參數分離的實現方法爲_taskPlusArgs。調用task.run以後,grunt.tasks方法立刻就會調用task.start方法運行任務隊列中的任務。task.start方法的實現也在lib/util/task.js文件中,以下:

// 開始運行任務隊列中的任務
Task.prototype.start = function(opts) {
  //初始化opts對象
  if (!opts) {
    opts = {};
  }
  // 若是任務正在運行則退出
  if (this._running) { return false; }
  // 經過nextTask依次運行隊列中的任務
  var nextTask = function() {
    // 用來保存從隊列中取出的任務對象
    var thing;
    // 取出隊列中的元素,直到取出的元素不是placeholder和marker
    // placeholder用來處理嵌套任務的狀況
    do {
      //取出隊列中的任務對象
      thing = this._queue.shift();
    } while (thing === this._placeholder || thing === this._marker);
    // 若是隊列爲空,那麼完成任務,執行可選的done函數並返回
    if (!thing) {
      this._running = false;
      if (this._options.done) {
        this._options.done();
      }
      return;
    }
    // 向隊列中插入一個placeholder
    this._queue.unshift(this._placeholder);
    
    // 使用取出的任務對象構造任務函數的上下文對象
    var context = {
      // 任務名稱:target名稱:參數
      nameArgs: thing.nameArgs,
      // 任務名稱
      name: thing.task.name,
      // 任務參數,這個參數包括了除了任務名之外的東西,包括target名稱和參數
      args: thing.args,
      // 以args爲鍵的鍵值對,值爲true
      flags: thing.flags
    };
    
    // 運行任務的註冊函數,上下文設置爲上面構造的context函數
    this.runTaskFn(context, function() {
      return thing.task.fn.apply(this, this.args);
    }, nextTask, !!opts.asyncDone);

  }.bind(this);

  // 把任務標記爲正在運行
  this._running = true;
  // 運行任務隊列中的下一個任務
  nextTask();
};
// 運行任務的註冊函數
Task.prototype.runTaskFn = function(context, fn, done, asyncDone) {
  // 標記是否異步
  var async = false;

  // 執行函數完成以後的工做,更新任務狀態,執行done函數也就是運行下一個任務
  var complete = function(success) {
    var err = null;
    if (success === false) {
      // 任務運行失敗,建立錯誤對象
      err = new Error('Task "' + context.nameArgs + '" failed.');
    } else if (success instanceof Error || {}.toString.call(success) === '[object Error]') {
      // 若是傳入的是錯誤對象,表示任務執行失敗
      err = success;
      success = false;
    } else {
      // 任務運行成功
      success = true;
    }
    // 任務結束後重置當前運行任務
    this.current = {};
    // 記錄任務執行結構
    this._success[context.nameArgs] = success;
    // 若是任務失敗則調用錯誤處理函數
    if (!success && this._options.error) {
      this._options.error.call({name: context.name, nameArgs: context.nameArgs}, err);
    }
    // 若是指定了異步執行,那麼使用node自帶的nextTick來運行done
    // 不然直接運行done
    if (asyncDone) {
      process.nextTick(function() {
        done(err, success);
      });
    } else {
      done(err, success);
    }
  }.bind(this);

  // 用來支持異步任務,也就是this.async()方法的實現,
  // 返回函數在異步任務完成時被調用執行complete方法
  context.async = function() {
    async = true;
    // 返回的函數在任務中的異步工做完成後被調用
    return function(success) {
      setTimeout(function() { complete(success); }, 1);
    };
  };

  // 記錄當前正在運行的任務上下文
  this.current = context;

  try {
    // 執行任務的註冊函數
    var success = fn.call(context);
    // 若是沒有使用this.async
    // 也就是說async標記爲false時在任務完成以後直接調用complete方法
    if (!async) {
      complete(success);
    }
  } catch (err) {
    complete(err);
  }
};

task.start方法中定義了一個nextTask方法,方法的做用是依次執行任務隊列中的任務,從任務隊列中取出任務對象,利用任務對象構建一個上下文對象,而後在這個上下文中執行任務的註冊函數,執行完註冊函數以後執行隊列中的下一個任務。執行註冊函數的功能有task.runTaskFn方法實現。在這個方法中定義了一個complele方法,會在任務註冊函數執行完成後備調用,進行錯誤處理工做。同時在task.runTaskFn方法中還向上下文對象context中添加了一個async方法,這個方法就是當咱們須要在任務中進行一些異步操做是首先須要調用的方法,調用這個方法以後會返回一個函數,這個函數會異步執行complete方法,若是沒有async方法,那麼在咱們任務中的異步操做還未返回時,grunt內部就會調用complete方法,這樣就會形成錯誤。有了async方法,咱們就能夠確保complete方法是在咱們任務完成以後才被調用。

上面所涉及到的幾個方法就是grunt中運行任務過程當中主要的幾個方法。你們確定還以爲少了點什麼,想要運行任務首先須要在gruntfile.js中註冊任務,因此下一次我將和你們分享任務註冊相關的源碼解析,敬請期待。

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

相關文章
相關標籤/搜索