从输入指令开始

平时使用webpack时,大致可以分为如下几种形式:
1、直接在控制台输入webpack指令,这种情况下最直接执行到的文件是./node_modules/bin/webpack.js或者/node_modules/webpack/bin/webpack.js,前者为后者的软链
2、通常写好一个类似dev-server.js/build.js的node脚本,通过npm run dev/npn run build指令去执行,这种情况通常会require(‘webpack’)直接拿到位于./node_modules/webpack/lib/webpack.js的webpack方法传入config,手动调用compiler.run执行
3、第三方库封装好的一些指令,例如create-react-app vue-cli-service,这种情况和第2种类似

接下来我们重点分析第1种情况是怎么调用到./node_modules/webpack/lib/webpack.js的:
在/node_modules/webpack/bin/webpack.js文件中,会调用runCli:

  1. const cli = {
  2. name: "webpack-cli",
  3. package: "webpack-cli",
  4. binName: "webpack-cli",
  5. installed: isInstalled("webpack-cli"),
  6. url: "https://github.com/webpack/webpack-cli"
  7. };
  8. if (!cli.installed) {
  9. // 没有安装cli的情况,可以忽略
  10. } else {
  11. runCli(cli);
  12. }
  1. const runCli = cli => {
  2. const path = require("path");
  3. const pkgPath = require.resolve(`${cli.package}/package.json`);
  4. // eslint-disable-next-line node/no-missing-require
  5. const pkg = require(pkgPath);
  6. // eslint-disable-next-line node/no-missing-require
  7. require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
  8. };

很显然,上面代码中第7行,require的路径为
./node_modules/webpack-cli/bin/cli.js
调用require解析完该文件之后,就会执行里面的runCLI方法:

  1. #!/usr/bin/env node
  2. "use strict";
  3. const importLocal = require("import-local");
  4. const runCLI = require("../lib/bootstrap");
  5. if (!process.env.WEBPACK_CLI_SKIP_IMPORT_LOCAL) {
  6. // Prefer the local installation of `webpack-cli`
  7. if (importLocal(__filename)) {
  8. return;
  9. }
  10. }
  11. process.title = "webpack";
  12. runCLI(process.argv);

可以进一步去../lib/bootstrap寻找runCLI的定义:

  1. const WebpackCLI = require("./webpack-cli");
  2. const runCLI = async (args) => {
  3. // Create a new instance of the CLI object
  4. const cli = new WebpackCLI();
  5. try {
  6. await cli.run(args);
  7. } catch (error) {
  8. cli.logger.error(error);
  9. process.exit(2);
  10. }
  11. };
  12. module.exports = runCLI;

再进一步去./webpack-cli中寻找WebpackCLI.run方法的定义,这个方法非常长,大约有750行,以下是简化后的结果:

  1. async run(args, parseOptions) {
  2. // Built-in internal commands
  3. const buildCommandOptions = {
  4. name: "build [entries...]",
  5. alias: ["bundle", "b"],
  6. description: "Run webpack (default command, can be omitted).",
  7. usage: "[entries...] [options]",
  8. dependencies: [WEBPACK_PACKAGE],
  9. };
  10. const watchCommandOptions = {
  11. name: "watch [entries...]",
  12. alias: "w",
  13. description: "Run webpack and watch for files changes.",
  14. usage: "[entries...] [options]",
  15. dependencies: [WEBPACK_PACKAGE],
  16. };
  17. const versionCommandOptions = {
  18. name: "version [commands...]",
  19. alias: "v",
  20. description:
  21. "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.",
  22. };
  23. const helpCommandOptions = {
  24. name: "help [command] [option]",
  25. alias: "h",
  26. description: "Display help for commands and options.",
  27. };
  28. // Built-in external commands
  29. const externalBuiltInCommandsInfo = [
  30. {
  31. name: "serve [entries...]",
  32. alias: ["server", "s"],
  33. pkg: "@webpack-cli/serve",
  34. },
  35. {
  36. name: "info",
  37. alias: "i",
  38. pkg: "@webpack-cli/info",
  39. },
  40. {
  41. name: "init",
  42. alias: ["create", "new", "c", "n"],
  43. pkg: "@webpack-cli/generators",
  44. },
  45. {
  46. name: "loader",
  47. alias: "l",
  48. pkg: "@webpack-cli/generators",
  49. },
  50. {
  51. name: "plugin",
  52. alias: "p",
  53. pkg: "@webpack-cli/generators",
  54. },
  55. {
  56. name: "migrate",
  57. alias: "m",
  58. pkg: "@webpack-cli/migrate",
  59. },
  60. {
  61. name: "configtest [config-path]",
  62. alias: "t",
  63. pkg: "@webpack-cli/configtest",
  64. },
  65. ];
  66. const knownCommands = [
  67. buildCommandOptions,
  68. watchCommandOptions,
  69. versionCommandOptions,
  70. helpCommandOptions,
  71. ...externalBuiltInCommandsInfo,
  72. ];
  73. const getCommandName = (name) => name.split(" ")[0];
  74. const isKnownCommand = (name) =>
  75. knownCommands.find(
  76. (command) =>
  77. getCommandName(command.name) === name ||
  78. (Array.isArray(command.alias) ? command.alias.includes(name) : command.alias === name),
  79. );
  80. const isCommand = (input, commandOptions) => {
  81. const longName = getCommandName(commandOptions.name);
  82. if (input === longName) {
  83. return true;
  84. }
  85. if (commandOptions.alias) {
  86. if (Array.isArray(commandOptions.alias)) {
  87. return commandOptions.alias.includes(input);
  88. } else {
  89. return commandOptions.alias === input;
  90. }
  91. }
  92. return false;
  93. };
  94. const findCommandByName = (name) =>
  95. this.program.commands.find(
  96. (command) => name === command.name() || command.aliases().includes(name),
  97. );
  98. const isOption = (value) => value.startsWith("-");
  99. const isGlobalOption = (value) =>
  100. value === "--color" ||
  101. value === "--no-color" ||
  102. value === "-v" ||
  103. value === "--version" ||
  104. value === "-h" ||
  105. value === "--help";
  106. const loadCommandByName = async (commandName, allowToInstall = false) => {};
  107. // Register own exit
  108. this.program.exitOverride(async (error) => {});
  109. // Default `--color` and `--no-color` options
  110. const cli = this;
  111. this.program.option("--color", "Enable colors on console.");
  112. this.program.on("option:color", function () {
  113. const { color } = this.opts();
  114. cli.isColorSupportChanged = color;
  115. cli.colors = cli.createColors(color);
  116. });
  117. this.program.option("--no-color", "Disable colors on console.");
  118. this.program.on("option:no-color", function () {
  119. const { color } = this.opts();
  120. cli.isColorSupportChanged = color;
  121. cli.colors = cli.createColors(color);
  122. });
  123. // Make `-v, --version` options
  124. // Make `version|v [commands...]` command
  125. const outputVersion = async (options) => {};
  126. this.program.option(
  127. "-v, --version",
  128. "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.",
  129. );
  130. const outputHelp = async (options, isVerbose, isHelpCommandSyntax, program) => {}
  131. this.program.helpOption(false);
  132. this.program.addHelpCommand(false);
  133. this.program.option("-h, --help [verbose]", "Display help for commands and options.");
  134. let isInternalActionCalled = false;
  135. // Default action
  136. this.program.usage("[options]");
  137. this.program.allowUnknownOption(true);
  138. this.program.action(async (options, program) => {});
  139. await this.program.parseAsync(args, parseOptions);
  140. }

可以看到最后对我们输入的参数进行了解析,虽然我们并不能确定parseAsync这个方法干了什么,但我们可以猜测到,如果我们想要执行开发或生产环境的编译,一定要走到一个加载webpack并传入配置执行的方法里面,这个方法就是runWebpack

  async runWebpack(options, isWatchCommand) {
    // eslint-disable-next-line prefer-const
    let compiler;
    let createJsonStringifyStream;

    if (options.json) {
      const jsonExt = await this.tryRequireThenImport("@discoveryjs/json-ext");

      createJsonStringifyStream = jsonExt.stringifyStream;
    }

    const callback = (error, stats) => {
      if (error) {
        this.logger.error(error);
        process.exit(2);
      }

      if (stats.hasErrors()) {
        process.exitCode = 1;
      }

      if (!compiler) {
        return;
      }

      const statsOptions = compiler.compilers
        ? {
            children: compiler.compilers.map((compiler) =>
              compiler.options ? compiler.options.stats : undefined,
            ),
          }
        : compiler.options
        ? compiler.options.stats
        : undefined;

      // TODO webpack@4 doesn't support `{ children: [{ colors: true }, { colors: true }] }` for stats
      const statsForWebpack4 = this.webpack.Stats && this.webpack.Stats.presetToOptions;

      if (compiler.compilers && statsForWebpack4) {
        statsOptions.colors = statsOptions.children.some((child) => child.colors);
      }

      if (options.json && createJsonStringifyStream) {
        const handleWriteError = (error) => {
          this.logger.error(error);
          process.exit(2);
        };

        if (options.json === true) {
          createJsonStringifyStream(stats.toJson(statsOptions))
            .on("error", handleWriteError)
            .pipe(process.stdout)
            .on("error", handleWriteError)
            .on("close", () => process.stdout.write("\n"));
        } else {
          createJsonStringifyStream(stats.toJson(statsOptions))
            .on("error", handleWriteError)
            .pipe(fs.createWriteStream(options.json))
            .on("error", handleWriteError)
            // Use stderr to logging
            .on("close", () => {
              process.stderr.write(
                `[webpack-cli] ${this.colors.green(
                  `stats are successfully stored as json to ${options.json}`,
                )}\n`,
              );
            });
        }
      } else {
        const printedStats = stats.toString(statsOptions);

        // Avoid extra empty line when `stats: 'none'`
        if (printedStats) {
          this.logger.raw(printedStats);
        }
      }
    };

    const env =
      isWatchCommand || options.watch
        ? { WEBPACK_WATCH: true, ...options.env }
        : { WEBPACK_BUNDLE: true, WEBPACK_BUILD: true, ...options.env };

    options.argv = { ...options, env };

    if (isWatchCommand) {
      options.watch = true;
    }

    compiler = await this.createCompiler(options, callback);

    if (!compiler) {
      return;
    }

    const isWatch = (compiler) =>
      compiler.compilers
        ? compiler.compilers.some((compiler) => compiler.options.watch)
        : compiler.options.watch;

    if (isWatch(compiler) && this.needWatchStdin(compiler)) {
      process.stdin.on("end", () => {
        process.exit(0);
      });
      process.stdin.resume();
    }
  }
}