Grunt-cli的执行过程以及Grunt加载原理

bc05007 8年前

来自: http://www.cnblogs.com/xing901022/p/5164257.html

通过本篇你可以了解到:

  • 1 grunt-cli的执行原理
  • 2 nodeJS中模块的加载过程

Grunt-cli原理

grunt-cli 其实也是 Node 模块,它可以帮助我们在控制台中直接运行 grunt 命令。因此当你使用 grunt 的时候,往往都是先安装 grunt-cli ,再安装 grunt 。

如果你使用的是 npm install -g grunt-cli 命令,那么安装地址如下:

windows:  C:\\Users\\neusoft\\AppData\\Roaming\\npm\\node_modules\\grunt-cli  linux:  /nodejs/node_modules/grunt-cli

在这里可以直接看到编译后的代码。

当执行 grunt 命令时,会默认先去全局的 grunt-cli 下找 grunt-cli 模块,而不会先走当前目录下的 node_modules 的 grunt-cli 。

加载相应的代码后,grunt-cli做了下面的工作:

  • 1 设置控制台的名称
  • 2 获取打开控制台的目录
  • 3 执行 completion 或者 version 或者 help 命令
  • 4 查找grunt,执行相应的命令
  • 5 调用 grunt.cli() ,继续分析参数,执行相应的任务

源码初探

首先Node的模块都会有一个特点,就是先去读取package.json,通过里面的main或者bin来确定主程序的位置,比如grunt-cli在package.json中可以看到主程序位于:

  "bin": {      "grunt": "bin/grunt"    }

找到主程序,下面就看一下它都做了什么:

首先加载必备的模块:

// 与查找和路径解析有关  var findup = require('findup-sync');  var resolve = require('resolve').sync;    //供grunt-cli使用  var options = require('../lib/cli').options;  var completion = require('../lib/completion');  var info = require('../lib/info');    //操作路径  var path = require('path');

然后就是判断下当前的参数,比如如果输入grunt --version,则会同时输出grunt-cli和grunt的版本:

//根据参数的不同,操作不同  if ('completion' in options) {    completion.print(options.completion);  } else if (options.version) {    //如果是grunt --version,则进入到这个模块。    //调用info的version方法    info.version();  } else if (options.base && !options.gruntfile) {    basedir = path.resolve(options.base);  } else if (options.gruntfile) {    basedir = path.resolve(path.dirname(options.gruntfile));  }

其中,cli定义了当前指令参数的别名,没什么关键作用。info则是相关的信息。

然后才是真正的核心代码!

查找grunt

var basedir = process.cwd();  var gruntpath;  ...  try {    console.log("寻找grunt");    gruntpath = resolve('grunt', {basedir: basedir});    console.log("找到grunt,位置在:"+gruntpath);  } catch (ex) {    gruntpath = findup('lib/grunt.js');    // No grunt install found!    if (!gruntpath) {      if (options.version) { process.exit(); }      if (options.help) { info.help(); }      info.fatal('Unable to find local grunt.', 99);    }  }

可以看到它传入控制台开启的目录,即process.cwd();然后通过resolve方法解析grunt的路径。

最后调用grunt.cli()方法

require(gruntpath).cli();

查找grunt

这部分内容,可以广泛的理解到其他的模块加载机制。resolve是grunt-cli依赖的模块:

var core = require('./lib/core');  exports = module.exports = require('./lib/async');  exports.core = core;  exports.isCore = function (x) { return core[x] };  exports.sync = require('./lib/sync');

其中async为异步的加载方案,sync为同步的加载方案。看grunt-cli程序的最上面,可以发现grunt-cli是通过同步的方式查找grunt的。

sync就是标准的node模块了:

var core = require('./core');  var fs = require('fs');  var path = require('path');    module.exports = function (x, opts) {};

主要看看内部的加载机制吧!

首先判断加载的模块是否是核心模块:

if (core[x]) return x;

core其实是个判断方法:

module.exports = require('./core.json').reduce(function (acc, x) {      acc[x] = true;//如果是核心模块,则返回该json。      return acc;  }, {});

核心模块有下面这些:

[      "assert",      "buffer_ieee754",      "buffer",      "child_process",      "cluster",      "console",      "constants",      "crypto",      "_debugger",      "dgram",      "dns",      "domain",      "events",      "freelist",      "fs",      "http",      "https",      "_linklist",      "module",      "net",      "os",      "path",      "punycode",      "querystring",      "readline",      "repl",      "stream",      "string_decoder",      "sys",      "timers",      "tls",      "tty",      "url",      "util",      "vm",      "zlib"  ]

回到sync.js中,继续定义了两个方法:

//判断是否为文件  var isFile = opts.isFile || function (file) {          console.log("查询文件:"+file);          try {               var stat = fs.statSync(file)           }catch (err) {               if (err && err.code === 'ENOENT')                   return false           }          console.log("stat.isFile:"+stat.isFile());          console.log("stat.isFIFO:"+stat.isFIFO());          return stat.isFile() || stat.isFIFO();      };    //定义加载的方法  var readFileSync = opts.readFileSync || fs.readFileSync;    //定义扩展策略,默认是添加.js,因此如果模块的名称为grunt.js,可以直接写成grunt  var extensions = opts.extensions || [ '.js' ];    //定义控制台开启的路径  var y = opts.basedir || path.dirname(require.cache[__filename].parent.filename);

至此,会得到两个变量:

  • y 代表控制台开启的路径,查找会从这个路径开始
  • x 加载模块的名称

然后根据文件名称判断加载的方式。加载的方式,主要包括两类:

  • 只传入模块的名称,则从当前路径逐级向上查找
  • 传入标准的路径,直接在该路径下查找
//匹配D:\\workspace\\searcher\\ui-dev\\node_modules\\grunt这种名称  if (x.match(/^(?:\.\.?\/|\/|([A-Za-z]:)?\\)/)) {          var m = loadAsFileSync(path.resolve(y, x))              || loadAsDirectorySync(path.resolve(y, x));          if (m) return m;      } else {          var n = loadNodeModulesSync(x, y);          if (n) return n;      }  //还没找到就报错  throw new Error("Cannot find module '" + x + "'");

如果正常的使用grunt xxx的时候,就会进入loadNodeMudelsSync()方法中。

这个方法中使用了另一个关键的方法来获取加载的路径:

    function loadNodeModulesSync (x, start) {          //从模块加载,start是当前目录          var dirs = nodeModulesPathsSync(start);                    console.log("dirs:"+dirs);            for (var i = 0; i < dirs.length; i++) {              var dir = dirs[i];              var m = loadAsFileSync(path.join( dir, '/', x));              if (m) return m;              var n = loadAsDirectorySync(path.join( dir, '/', x ));              if (n) return n;          }      }

nodeModulesPathsSync方法可以分解目录,并返回加载模块的路径。

举个例子,如果我的路径是 D:/a/b/c

那么会得到如下的数组:

D:/a/b/c/node_modules  D:/a/b/node_modules  D:/a/node_modules  D:/node_modules

执行的代码如下:

function nodeModulesPathsSync (start) {          var splitRe = process.platform === 'win32' ? /[\/\\]/ : /\/+/;//根据操作系统的类型,判断文件的分隔方法          var parts = start.split(splitRe);//分解各个目录层次                    var dirs = [];          for (var i = parts.length - 1; i >= 0; i--) {//从后往前,在每个路径上,添加node_modules目录,当做查找路径              if (parts[i] === 'node_modules') continue;//如果该目录已经是node_modules,则跳过。              var dir = path.join(                  path.join.apply(path, parts.slice(0, i + 1)),                  'node_modules'              );                if (!parts[0].match(/([A-Za-z]:)/)) {//如果是Linux系统,则开头加上/                  dir = '/' + dir;                  }              dirs.push(dir);          }          return dirs.concat(opts.paths);      }

获取到了加载的路径后,就一次执行加载方法。

如果是文件,则使用下面的方法加载,其实就是遍历一遍后缀数组,看看能不能找到:

function loadAsFileSync (x) {          if (isFile(x)) {              return x;          }                    for (var i = 0; i < extensions.length; i++) {              var file = x + extensions[i];              if (isFile(file)) {                  return file;              }          }      }

如果是目录,则尝试读取package.json,查找它的main参数,看看能不能直接找到主程序;如果找不到,则自动对 当前路径/index下进行查找。

//如果是目录      function loadAsDirectorySync (x) {          var pkgfile = path.join(x, '/package.json');//如果是目录,首先读取package.json          if (isFile(pkgfile)) {              var body = readFileSync(pkgfile, 'utf8');//读取成utf-8的格式              try {                  var pkg = JSON.parse(body);//解析成json                  if (opts.packageFilter) {//暂时不知道这个参数时干嘛的!                      pkg = opts.packageFilter(pkg, x);                  }                  //主要在这里,读取main参数,main参数指定了主程序的位置                  if (pkg.main) {                      var m = loadAsFileSync(path.resolve(x, pkg.main));//如果main中指定的是文件,则直接加载                      if (m) return m;                      var n = loadAsDirectorySync(path.resolve(x, pkg.main));//如果main中指定的是目录,则继续循环                      if (n) return n;                  }              }              catch (err) {}          }          //再找不到,则直接从当前目录下查找index文件          return loadAsFileSync(path.join( x, '/index'));      }

这样,就完成了模块的加载了。

结论

因此,如果你同时安装了本地的grunt-cli、grunt和全局的grunt-cli、grunt,就不会纳闷为什么grunt-cli执行的是全局的、而grunt执行的是当前目录下的node_modules中的。另外,也有助于你了解Node中模块的加载机制。

如果对你有帮助,就点个赞吧!如有异议,还请及时指点!

</div>