上一篇我们分析了webpack4的入口,发现最后都是调用了 webpack-cli 这个工具包,本篇将对这个工具包里进一步发生的事情进行分析。

cli.js

这是webpack-cli 的入口文件,我们来看下它的基本结构

(function (){
    const importLocal = require("import-local"); //1
    if (importLocal(__filename)){...}//2
    require("v8-compile-cache"); //3
    const ErrorHelpers = require("./errorHelpers"); // 4
    const NON_COMPILATION_ARGS = []; //5
    const NON_COMPILATION_CMD = process.argv.find(arg=>{...})//6
    if (NON_COMPILATION_CMD){...}//7
    const yargs = require("yargs").usage(...);//8
    require("./config-yargs")(yargs);//9
    const DISPLAY_GROUP = "Stats options:";//10
    const BASIC_GROUP = "Basic options:";//11
    yargs.options({...}) //12
    yargs.parse(process.argv.slice(2), (err, argv, output) => {...}); //13
})();

我们可以看出这是一个立即执行函数。其中分为了以下步骤:

  • 1: 引入了import-local 包,这个包的作用在于判断某个包是本地安装的还是全局安装的;
  • 2:判断当前包(webpack-cli)是否全局安装,如果是全局安装,则中断执行。【意思是要求必须是项目本地安装】
  • 3:引入V8引擎的代码缓存功能,用于加速实例化。
  • 4:引入错误处理类
  • 5:定义了NON_COMPILATION_ARGS 数组,该数组存储了一些不需要编译的参数
  • 6:生成了NON_COMPILATION_CMD 数组,该数组存储了一些不需要编译的命令
  • 7:如果NON_COMPILATION_CMD 数组不为空,则直接执行这些命令
  • 8:使用yargs 包来生成一个更加优雅的交互式命令行界面
  • 9:对yargs命令行进行一些配置
  • 10:定义 DISPLAY_GROUP 变量;
  • 11:定义 BASIC_GROUP 变量;
  • 12:对yargs命令行进行进一步的配置;
  • 13:使用yargs 解析并执行命令行参数;

核心分析

NON_COMPILATION_ARGS数组

这个数组的内容如下:

const NON_COMPILATION_ARGS = [
        "init",
        "migrate",
        "add",
        "remove",
        "serve",
        "generate-loader",
        "generate-plugin",
        "info"
    ];

这些命令都是webpack-cli 内置的API,我们顺便来说下它们的作用:

  • webpack-cli init: 此命令允许你新建一份webpack 配置文件;
  • webpack-cli migrate: 此命令用于进行版本迁移;
  • webpack-cli add: 此命令用于往webpack配置文件中增加新属性;
  • webpack-cli remove: 此命令用于从webpack配置文件中删除属性;
  • webpack-cli serve: 此命令用于运行webpack-serve而不直接使用webpack-serve
  • webpack-cli generate-loader :用于生成新的 webpack 加载器
  • webpack-cli generate-plugin: 用于生成新的 webpack 插件
  • webpack-cli info : 返回与本地环境相关的一组信息

NON_COMPILATION_CMD 数组

这个数组是通过用户输入的命令行参数与上面的 NON_COMPILATION_ARGS 数组对比而生成的。

我们看下它的生成逻辑:

const NON_COMPILATION_CMD = process.argv.find(arg => {
        if (arg === "serve") {
            global.process.argv = global.process.argv.filter(a => a !== "serve");
            process.argv = global.process.argv;
        }
        return NON_COMPILATION_ARGS.find(a => a === arg);
    });

首先来说下 process.argv 这个数组,这个数组包含了启动Node.js进程时的命令行参数,例如,用户输入以下命令:

$ node ./node_modules/webpack/bin/webpack.js one two=three four

那么process.argv 的内容如下:

[ 'C:\\Program Files\\nodejs\\node.exe',
  'F:\\项目\\yp\\svn_new_new\\scdx_new_epg\\node_modules\\webpack\\bin\\webpack.js',
  'one',
  'two=three',
  'four' ]
  • 数组的第一个元素是该进程的执行路径,此例中即 node.exe 所在目录;
  • 数组的第二个元素为当前执行的js文件的路径;
  • 剩余的数组元素就是其他的命令行参数了,我们看到是以空格为分割符的。

回到上面的生成逻辑,我们可以看到,它首先判断这个数组中有没有serve这个命令,如果有,则从 global.process.argv (实际上和process.argv 是一个东西)中取出serve以外的其他元素组成新的process.argv数组,以供后面使用。 这一步,相当于把 serve 参数从process.argv 中移除掉了。

最后,对比process.argv 数组和NON_COMPILATION_ARGS ,查出两个数组的交集后组成最终的 NON_COMPILATION_CMD 数组

使用 yargs 解析并执行命令行参数

这一块我们直接贴源码,并在源码上贴出相关解析和注释:

// 入参:
//process.argv.slice(2) : 通过上面我们知道是我们的命令行附加参数;
//(err, argv, output) =>{...} 回调函数
    //err: 解析时引发任何验证错误,则填充
    //argv :  解析后的argv对象
    //output : 任何输出到终端的文本
yargs.parse(process.argv.slice(2), (err, argv, output) => {
        // 设置堆栈跟踪限制,这相当于要显示的错误帧数为30个
        Error.stackTraceLimit = 30;

        // arguments validation failed
        // 如果解析出错并且有输出内容,则输出相关错误信息并退出执行;
        if (err && output) {
            console.error(output);
            process.exitCode = 1;
            return;
        }

        // help or version info
        // 如果有输出内容,就输出它们,并返回;
        if (output) {
            console.log(output);
            return;
        }

        //如果argv对象有verbose属性,则将它的display属性设置为‘varbose’
        if (argv.verbose) {
            argv["display"] = "verbose";
        }

        let options;
        try {
            // 对配置文件和参数进行转换与合法性检测并生成最终的配置选项参数(options)
            options = require("./convert-argv")(argv);
        } catch (err) {
            //如果出错则报错并退出执行;

            if (err.name !== "ValidationError") {
                throw err;
            }

            const stack = ErrorHelpers.cleanUpWebpackOptions(err.stack, err.message);
            const message = err.message + "\n" + stack;

            if (argv.color) {
                console.error(`\u001b[1m\u001b[31m${message}\u001b[39m\u001b[22m`);
            } else {
                console.error(message);
            }

            process.exitCode = 1;
            return;
        }

        /**
         * When --silent flag is present, an object with a no-op write method is
         * used in place of process.stout
         * 当出现 silent 标志时,使用 no-op write 方法的对象 * 代替 process.stout
         */
        const stdout = argv.silent
            ? {
                write: () => {}
                } // eslint-disable-line
            : process.stdout;

        //工具函数
        function ifArg(name, fn, init) {...}

        //定义对配置项(options)进行处理的函数
        function processOptions(options) {...}
        //调用processOptions方法来对options进行处理
        processOptions(options);
    });

ifArg()方法

由于下面要用到,所以我们先看看 ifArg()方法 做了什么事

function ifArg(name, fn, init) {
            if (Array.isArray(argv[name])) {
                if (init) init();
                argv[name].forEach(fn);
            } else if (typeof argv[name] !== "undefined") {
                if (init) init();
                fn(argv[name], -1);
            }
        }

我们看到它接收了三个参数,一个name , 从使用方法看应该是个字符串,一个fn 是个函数,一个init 也是个函数。

  1. 先使用传入的name作为键,在argv对象中找到对应的值,然后判断这个值是否为数组
  2. 如果是数组,则进入第一个分支,在这个分支里,先判断是否传入 init 参数,如果有,则执行它;然后遍历这个数组,对数组中的每一项执行fn 这个函数;
  3. 如果不是数组,则判断它是否未定义,如果不是,则进入第二个分支,在这个分支里,先判断是否传入 init 参数,如果有,则执行它;然后以argv[name]-1 为参数,调用fn这个函数。
  4. 这里面还隐含了一个逻辑,如果argv 对象中没有 name 所指代的属性,则什么都不做;

综合以上,我们可以知道,这个函数实际上就是将argv[name]的所有元素,包括嵌套数组中的元素,作为参数传递给 fn 去执行。

processOptions() 方法

我们单独看一下 processOptions() 方法:

            // 如果options.then 是一个function,说明它是一个Promise对象,则将本函数作为参数传进去处理;
            if (typeof options.then === "function") {
                options.then(processOptions).catch(function(err) {
                    console.error(err.stack || err);
                    process.exit(1); // eslint-disable-line
                });
                return;
            }

            //获取选项数组的第一项;
            const firstOptions = [].concat(options)[0];

            //引入webpack包的配置项预处理函数
            const statsPresetToOptions = require("webpack").Stats.presetToOptions;

            //定义outputOptions 为options的stats字段;
            let outputOptions = options.stats;

            //如果outputOptions 是布尔或字符串类型,则使用预处理函数对它做处理,如果它没有定义,则返回一个空对象;
            if (typeof outputOptions === "boolean" || typeof outputOptions === "string") {
                outputOptions = statsPresetToOptions(outputOptions);
            } else if (!outputOptions) {
                outputOptions = {};
            }

            // 如果argv中有display属性,则对 argv[display] 中的各种基础值执行statsPresetToOptions 函数,并赋值给outputOptions 
            ifArg("display", function(preset) {
                outputOptions = statsPresetToOptions(preset);
            });

            //新建outputOptions对象的一个实例
            outputOptions = Object.create(outputOptions);

            //如果options是数组,并且outputOptions实例没有children属性,则设置children属性为options.stats的值
            if (Array.isArray(options) && !outputOptions.children) {
                outputOptions.children = options.map(o => o.stats);
            }
            //如果outputOptions的context属性未定义,则设置该属性为onptions的context值;
            if (typeof outputOptions.context === "undefined") outputOptions.context = firstOptions.context;

            //如果argv中有参数env,并且如果outputOptions有env属性,则将它的_env 设置为参数env的值
            ifArg("env", function(value) {
                if (outputOptions.env) {
                    outputOptions._env = value;
                }
            });
            //如果argv中有参数json,并且它的值不为空,则将outputOptions 的json属性和modules属性设置为这个值
            ifArg("json", function(bool) {
                if (bool) {
                    outputOptions.json = bool;
                    outputOptions.modules = bool;
                }
            });
            //如果outputOptions的colors 属性未定义,则将此属性的值设置为`supports-color` 的检测值,该值是布尔类型,用来说明
终端是否支持多色彩显示
            if (typeof outputOptions.colors === "undefined") outputOptions.colors = require("supports-color").stdout;
            //如果argv有sort-modules-by属性,则将outputOptions的modulesSort设置为它的值,该参数用来为模块排序
            ifArg("sort-modules-by", function(value) {
                outputOptions.modulesSort = value;
            });
            //如果argv有sort-chunks-by属性,则将outputOptions的chunksSort 设置为它的值,该参数用来为分片排序
            ifArg("sort-chunks-by", function(value) {
                outputOptions.chunksSort = value;
            });
            //如果argv有sort-assets-by属性,则将outputOptions的assetsSort 设置为它的值,该参数用来为静态资源排序
            ifArg("sort-assets-by", function(value) {
                outputOptions.assetsSort = value;
            });
            //如果argv有display-exclude属性,则将outputOptions的exclude 设置为它的值
            ifArg("display-exclude", function(value) {
                outputOptions.exclude = value;
            });
            //如果outputOptions的json属性为空,则执行此分支
            if (!outputOptions.json) {
                //如果cached和cachedAssets 属性未定义,则设置为false,默认不缓存
                if (typeof outputOptions.cached === "undefined") outputOptions.cached = false;
                if (typeof outputOptions.cachedAssets === "undefined") outputOptions.cachedAssets = false;
                //如果display-chunks属性不为空,则设置不显示modules,而显示分片
                ifArg("display-chunks", function(bool) {
                    if (bool) {
                        outputOptions.modules = false;
                        outputOptions.chunks = true;
                        outputOptions.chunkModules = true;
                    }
                });
                //设置是否通过对应的 bundle 显示入口起点
                ifArg("display-entrypoints", function(bool) {
                    outputOptions.entrypoints = bool;
                });
                // 设置是否显示模块被引入的原因
                ifArg("display-reasons", function(bool) {
                    if (bool) outputOptions.reasons = true;
                });
                //设置是否显示显示每个模块到入口起点的距离(distance)
                ifArg("display-depth", function(bool) {
                    if (bool) outputOptions.depth = true;
                });
                //设置是否显示哪个模块导出被用到
                ifArg("display-used-exports", function(bool) {
                    if (bool) outputOptions.usedExports = true;
                });
                //设置是否显示模块的导出
                ifArg("display-provided-exports", function(bool) {
                    if (bool) outputOptions.providedExports = true;
                });
                //设置是否显示优化帮助
                ifArg("display-optimization-bailout", function(bool) {
                    if (bool) outputOptions.optimizationBailout = bool;
                });
                //设置是否显示错误的详细信息(就像解析日志一样)
                ifArg("display-error-details", function(bool) {
                    if (bool) outputOptions.errorDetails = true;
                });
                //设置是否显示分片的源文件
                ifArg("display-origins", function(bool) {
                    if (bool) outputOptions.chunkOrigins = true;
                });
                //设置是否显示最大模块数
                ifArg("display-max-modules", function(value) {
                    outputOptions.maxModules = +value;
                });
                //设置是否显示缓存(但未构建)模块的信息
                ifArg("display-cached", function(bool) {
                    if (bool) outputOptions.cached = true;
                });
                //设置是否显示缓存的资源
                ifArg("display-cached-assets", function(bool) {
                    if (bool) outputOptions.cachedAssets = true;
                });
                //如果eclude选项未定义,则定义为下面的数组,数组中的目录将被排除
                if (!outputOptions.exclude) outputOptions.exclude = ["node_modules", "bower_components", "components"];
                //如果设置了显示模块信息,则设置显示的最大模块数为无穷大,不排除任何模块,并且输出所有模块信息
                if (argv["display-modules"]) {
                    outputOptions.maxModules = Infinity;
                    outputOptions.exclude = undefined;
                    outputOptions.modules = true;
                }
            }
            //如果设置了隐藏模块,则设置不输出模块信息,并且不将构建模块信息添加到分片信息
            ifArg("hide-modules", function(bool) {
                if (bool) {
                    outputOptions.modules = false;
                    outputOptions.chunkModules = false;
                }
            });
            //设置信息显示方式,可能值为errors-only(只在发生错误时输出),minimal(只在发生错误或有新的编译时输出),none(没有输出),normal(标准输出),verbose(全部输出)
            ifArg("info-verbosity", function(value) {
                outputOptions.infoVerbosity = value;
            });
            //设置构建时的分隔符
            ifArg("build-delimiter", function(value) {
                outputOptions.buildDelimiter = value;
            });
            //引入webpack包
            const webpack = require("webpack");

            let lastHash = null;
            let compiler;
            //尝试根据配置选项使用webpack构建编译
            try {
                compiler = webpack(options);
            } catch (err) {
                if (err.name === "WebpackOptionsValidationError") {
                    if (argv.color) console.error(`\u001b[1m\u001b[31m${err.message}\u001b[39m\u001b[22m`);
                    else console.error(err.message);
                    // eslint-disable-next-line no-process-exit
                    process.exit(1);
                }

                throw err;
            }
            //如果传入了progress参数,则引用webpack的ProgressPlugin插件进行编译
            if (argv.progress) {
                const ProgressPlugin = require("webpack").ProgressPlugin;
                new ProgressPlugin({
                    profile: argv.profile
                }).apply(compiler);
            }
            //如果设置了全部输出,则根据参数中是否包含w(是否为监听模式)参数来决定使用哪个方法来显示编译信息
            if (outputOptions.infoVerbosity === "verbose") {
                if (argv.w) {
                    compiler.hooks.watchRun.tap("WebpackInfo", compilation => {
                        const compilationName = compilation.name ? compilation.name : "";
                        console.error("\nCompilation " + compilationName + " starting…\n");
                    });
                } else {
                    compiler.hooks.beforeRun.tap("WebpackInfo", compilation => {
                        const compilationName = compilation.name ? compilation.name : "";
                        console.error("\nCompilation " + compilationName + " starting…\n");
                    });
                }
                compiler.hooks.done.tap("WebpackInfo", compilation => {
                    const compilationName = compilation.name ? compilation.name : "";
                    console.error("\nCompilation " + compilationName + " finished\n");
                });
            }
            //编译回调
            function compilerCallback(err, stats) {
                //如果不处于监听模式或者编译出错,则净化文件系统,即不再缓存文件
                if (!options.watch || err) {
                    // Do not keep cache anymore
                    compiler.purgeInputFileSystem();
                }
                //如果出错,则输出错误信息并退出执行;
                if (err) {
                    lastHash = null;
                    console.error(err.stack || err);
                    if (err.details) console.error(err.details);
                    process.exit(1); // eslint-disable-line
                }
                //如果outputOptions有json属性,则将选项序列化为字符串作为标准输出
                if (outputOptions.json) {
                    stdout.write(JSON.stringify(stats.toJson(outputOptions), null, 2) + "\n");
                } else if (stats.hash !== lastHash) {
                    lastHash = stats.hash;
                    if (stats.compilation && stats.compilation.errors.length !== 0) {
                        const errors = stats.compilation.errors;
                        if (errors[0].name === "EntryModuleNotFoundError") {
                            console.error("\n\u001b[1m\u001b[31mInsufficient number of arguments or no entry found.");
                            console.error(
                                "\u001b[1m\u001b[31mAlternatively, run 'webpack(-cli) --help' for usage info.\u001b[39m\u001b[22m\n"
                            );
                        }
                    }
                    const statsString = stats.toString(outputOptions);
                    const delimiter = outputOptions.buildDelimiter ? `${outputOptions.buildDelimiter}\n` : "";
                    if (statsString) stdout.write(`${statsString}\n${delimiter}`);
                }
                if (!options.watch && stats.hasErrors()) {
                    process.exitCode = 2;
                }
            }
            //如果处于监听模式,则获取监听模式的配置项,并在标准输入为“end”时结束执行;
            if (firstOptions.watch || options.watch) {
                const watchOptions = firstOptions.watchOptions || firstOptions.watch || options.watch || {};
                if (watchOptions.stdin) {
                    process.stdin.on("end", function(_) {
                        process.exit(); // eslint-disable-line
                    });
                    process.stdin.resume();
                }
                //以监听模式进行编译,编译按成后执行编译回调
                compiler.watch(watchOptions, compilerCallback);
                //如过显示模式为不输出,则输出错误提示:webpack 正在监听文件
                if (outputOptions.infoVerbosity !== "none") console.error("\nwebpack is watching the files…\n");
            } else {
                //否则,运行编译模块,并在编译关闭后执行编译回调
                compiler.run((err, stats) => {
                    if (compiler.close) {
                        compiler.close(err2 => {
                            compilerCallback(err || err2, stats);
                        });
                    } else {
                        compilerCallback(err, stats);
                    }
                });
            }

我们看到,其实webpack-cli 主要做了三件事:

  • 1. 引入了细节更加丰富,更具定制性的命令行工具包 yargs;
  • 2. 分析命令参数,对各个参数进行转换,统一组成新的编译配置项
  • 3. 引用webpack ,根据这些参数项对源代码进行编译和构建

下一篇我们将开始详细分析整个编译和构建过程,这篇就到这。