Grunt-cli的執行過程以及Grunt加載原理

經過本篇你能夠了解到:node

  • 1 grunt-cli的執行原理
  • 2 nodeJS中模塊的加載過程

Grunt-cli原理

grunt-cli其實也是Node模塊,它能夠幫助咱們在控制檯中直接運行grunt命令。所以當你使用grunt的時候,每每都是先安裝grunt-cli,再安裝gruntlinux

若是你使用的是npm install -g grunt-cli命令,那麼安裝地址以下:npm

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

在這裏能夠直接看到編譯後的代碼。json

當執行grunt命令時,會默認先去全局的grunt-cli下找grunt-cli模塊,而不會先走當前目錄下的node_modulesgrunt-cli
加載相應的代碼後,grunt-cli作了下面的工做:windows

  • 1 設置控制檯的名稱
  • 2 獲取打開控制檯的目錄
  • 3 執行completion或者version或者help命令
  • 4 查找grunt,執行相應的命令
  • 5 調用grunt.cli(),繼續分析參數,執行相應的任務

源碼初探

首先Node的模塊都會有一個特色,就是先去讀取package.json,經過裏面的main或者bin來肯定主程序的位置,好比grunt-cli在package.json中能夠看到主程序位於:數組

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

找到主程序,下面就看一下它都作了什麼:app

首先加載必備的模塊:dom

// 與查找和路徑解析有關
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則是相關的信息。async

而後纔是真正的核心代碼!

查找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中模塊的加載機制。

若是對你有幫助,就點個贊吧!若有異議,還請及時指點!

相關文章
相關標籤/搜索