文章首发于我的博客 https://github.com/mcuking/bl...

本文以 webpack 源码来剖析其外部的工作流程,筹备剖析的版本为 4.41.5。

首先咱们要确认的是 webpack 的执行入口文件,通过查看 node_modules 中 webpack 的 package.json 的 bin 字段(如下),咱们能够晓得入口文件是 bin 文件下的 webpack.js,即 node_modules\webpack\bin\webpack.js

"bin": {  "webpack": "./bin/webpack.js"},

剖析 webpack 入口文件:webpack.js

文件代码并不多,总共有 171 行,次要分为 6 个局部:

  1. 失常执行返回
process.exitCode = 0;
  1. 定义了一个运行某个命令的办法 runCommand
const runCommand = (command, args) => {...};
  1. 定义了一个判断某个包是否装置的办法 isInstalled
const isInstalled = packageName => {    try {        require.resolve(packageName);        return true;    } catch (err) {        return false;    }};
  1. 定义了两个 webpack 可用的 CLI:webpack-cli 和 webpack-command。其中 installed 属性就是调用了下面的 isInstalled 办法来计算的。
const CLIs = [    {        name: "webpack-cli",        package: "webpack-cli",        binName: "webpack-cli",        alias: "cli",        installed: isInstalled("webpack-cli"),        recommended: true,        url: "https://github.com/webpack/webpack-cli",        description: "The original webpack full-featured CLI."    },    {        name: "webpack-command",        package: "webpack-command",        binName: "webpack-command",        alias: "command",        installed: isInstalled("webpack-command"),        recommended: false,        url: "https://github.com/webpack-contrib/webpack-command",        description: "A lightweight, opinionated webpack CLI."    }];
  1. 紧接着计算出曾经装置的 CLI
const installedClis = CLIs.filter(cli => cli.installed);
  1. 而后依据装置 CLI 的数量进行解决
if (installedClis.length === 0)  {...}else if(installedClis.length === 1) {...}else {...}

如果一个都没有装置,则会提醒是否要装置 webpack-cli,如果批准则主动帮你装置;如果装置了其中一个,会间接应用那个;如果装置了俩个,会提醒你删掉其中一个 CLI。

通过下面的剖析,咱们能够确认 webpack 最终会找到 webpack-cli 或 webpack-command 这个 npm 包,并执行这个 CLI。

那么咱们就去看下 webpack-cli 具体做了什么工作。

剖析 webpack-cli 运行机制

以后剖析的 webpack-cli 版本为 3.3.10,通过 webpack-cli 包的 package.json 的 bin 字段咱们能够确认 webpack-cli 的入口执行文件是 node_modules\webpack-cli\bin\cli.js

"bin": {    "webpack-cli": "./bin/cli.js"},

而后咱们持续剖析 cli.js 做了些什么,通过大抵查看能够确认的是 webpack-cli 的业务逻辑并不简单,次要文件就是 cli.js,其余都是 utils 或 config 文件,而 cli.js 的代码也只有 366 行而已。上面咱们就具体分析下 cli.js 里到底做了些什么。

首先结尾处对用户输出的参数进行了分类,判断参数中有局部在 NON_COMPILATION_ARGS 中,则调用 utils 中 prompt-command 文件默认导出的办法,并将该命令和输出的参数传入到该办法。

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);});if (NON_COMPILATION_CMD) {    return require("./utils/prompt-command")(NON_COMPILATION_CMD, ...process.argv);}

对此能够了解为,webpack-cli 局部命令是不须要进行编译的,即初始化一个 compiler 对象。那么咱们来看下不须要编译的命令都有哪些,以及它们的作用。

const NON_COMPILATION_ARGS = [  "init", // 创立一份 webpack 配置文件  "migrate",  // 进行 webpack 版本迁徙  "serve", // 运行 webpack-serve  "generate-loader", // 生成 webpack loader 代码  "generate-plugin", // 生成 webpack plugin 代码  "info" // 返回与本地环境相干的一些信息];

而后咱们再看下 ./utils/prompt-command 下的默认导出办法到底做了什么事,相干代码如下:

module.exports = function promptForInstallation(packages, ...args) {    try {        const path = require("path");        const fs = require("fs");        pathForCmd = path.resolve(process.cwd(), "node_modules", "@webpack-cli", packages);        if (!fs.existsSync(pathForCmd)) {            const globalModules = require("global-modules");            pathForCmd = globalModules + "/@webpack-cli/" + packages;            require.resolve(pathForCmd);        } else {            require.resolve(pathForCmd);        }        packageIsInstalled = true;    } catch (err) {        packageIsInstalled = false;    }    if (!packageIsInstalled) {...}}

promptForInstallation 办法接管到命令后会判断这个命令对应的包是否装置过,例如在 @webpack-cli 文件夹下是否存在 init 包,如果不存在,则提醒装置,装置后则运行。如果存在则间接运行。

接下来咱们回到主流程,看下 cli.js 前面的逻辑。

const yargs = require("yargs").usage(`webpack-cli ${require("../package.json").version}require("./config/config-yargs")(yargs);yargs.parse(process.argv.slice(2), (err, argv, output) => {...})

./config/config-yargs 文件的默认导出的办法

module.exports = function(yargs) {    yargs        .help("help")        .alias("help", "h")        .version()        .alias("version", "v")        .options({            config: {                type: "string",                describe: "Path to the config file",                group: CONFIG_GROUP,                defaultDescription: "webpack.config.js or webpackfile.js",                requiresArg: true            },            ...        );}

能够看到 webpack-cli 用到了 yargs 工具来构建交互式命令行工具,它能够提供命令和分组参数,并可能动静生成 help 帮忙信息。而 config-yargs 文件中的办法的作用就是就是对 yargs 进行配置。

接下来咱们持续返回 cli.js 看下 yargs.parse 办法内做了什么。

yargs.parse(process.argv.slice(2), (err, argv, output) => {    ...    try {        options = require("./utils/convert-argv")(argv);    } catch(e) {        ...    }    ...})

咱们能够看到接下来是调用了 ./utils/convert-argv 文件下的默认办法,并传入了输出参数。那么看看 ./utils/convert-argv 中的这个办法到底做了什么呢?

module.exports = function(...args) {    if (argv.config) {} else {        const defaultConfigFileNames = ["webpack.config", "webpackfile"].join("|");        const webpackConfigFileRegExp = `(${defaultConfigFileNames})(${extensions.join("|")})`;        const pathToWebpackConfig = findup(webpackConfigFileRegExp);        ...    }}

因为代码较多,我这里就只摘抄了局部要害代码,咱们不难发现这个办法次要作用就是生成 webpack 的配置项,例如 output 等,而起源次要有命令行的输出和 webpack.config.js 等文件。

再回到 cli.js,当生成完配置项 options,接下来又做了什么呢?

yargs.parse(process.argv.slice(2), (err, argv, output) => {    ...    function processOptions(options) {        let compiler;        try {            compiler = webpack(options);        } catch (err) {            ...        }        if (firstOptions.watch || options.watch) {            const watchOptions =                firstOptions.watchOptions || options.watchOptions || firstOptions.watch || options.watch || {};            ...            compiler.watch(watchOptions, compilerCallback);            ...        } else {            compiler.run((err, stats) => {                if (compiler.close) {                    compiler.close(err2 => {                        compilerCallback(err || err2, stats);                    });                } else {                    compilerCallback(err, stats);                }            });        }    }    processOptions(options)})

咱们能够看到,最初阶段调用了 processOptions 办法,外面则是获取到了 webpack 的一个 compiler 实例对象(后续咱们会介绍 webpack 的 Compiler),判断以后是否是 watch 形式,是的话就调用 compiler 实例上的 watch 办法来运行,否则调用 run 办法来运行,也就是开始了真正的打包构建流程。

总结下,webpack-cli 次要的作用就是:

  1. 引入 yargs,提供一个交互式命令行工具
  2. 对配置文件和命令行参数进行转换,最终生成配置选项参数;
  3. 而后依据配置实例化 webpack 对象,而后执行构建流程。

在下一篇咱们持续剖析 webpack 中的构建机制。