乐趣区

关于webpack:Webpack-源码分析1-Webpack-启动过程分析

文章首发于我的博客 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 中的构建机制。

退出移动版