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


    通过本篇你可以了解到:

    • 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_modulesgrunt-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中模块的加载机制。

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

  • 相关阅读:
    build、host和target选项
    第一篇博客
    C++中的new和delete
    新分类:C++复习笔记
    泛读英文小说推荐
    借助查询分析器对遗留项目进行分析
    程序员等级(非本人观点)
    线程并发时的四种数据同步方法
    单元测试之什么是优秀的单元测试
    多线程之进度条
  • 原文地址:https://www.cnblogs.com/xing901022/p/5164257.html
Copyright © 2020-2023  润新知