# 运行webpack发生了什么?

# 查找webpack入口文件

通过npm scripts运行webpack时候,npm其实是通过让命令行工具进入node_modules/.bin目录 查找是否存在webpack.cmd或者webpack.sh文件,如果存在就执行,不存在则抛出错误。
node_modules/bin目录下有这个命令,则如果局部安装依赖,则需要在依赖配置package.json中指定bin字段, 所以实际查找的入口文件就是 node_modules/webpack/bin/webpack.js。

# 分析webpack入口文件

/node_modules/webpack/bin/webpack.js

#!/usr/bin/env node

// @ts-ignore
process.exitCode = 0;

/**
 * @param {string} command process to run
 * @param {string[]} args commandline arguments
 * @returns {Promise<void>} promise
 */
const runCommand = (command, args) => {
	const cp = require("child_process");
	return new Promise((resolve, reject) => {
		const executedCommand = cp.spawn(command, args, {
			stdio: "inherit",
			shell: true
		});

		executedCommand.on("error", error => {
			reject(error);
		});

		executedCommand.on("exit", code => {
			if (code === 0) {
				resolve();
			} else {
				reject();
			}
		});
	});
};

/**
 * @param {string} packageName name of the package
 * @returns {boolean} is the package installed?
 */
const isInstalled = packageName => {
	try {
		require.resolve(packageName);

		return true;
	} catch (err) {
		return false;
	}
};

/**
 * @typedef {Object} CliOption
 * @property {string} name display name
 * @property {string} package npm package name
 * @property {string} binName name of the executable file
 * @property {string} alias shortcut for choice
 * @property {boolean} installed currently installed?
 * @property {boolean} recommended is recommended
 * @property {string} url homepage
 * @property {string} description description
 */

/** @type {CliOption[]} */
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."
	}
];

const installedClis = CLIs.filter(cli => cli.installed);

if (installedClis.length === 0) {
	const path = require("path");
	const fs = require("fs");
	const readLine = require("readline");

	let notify =
		"One CLI for webpack must be installed. These are recommended choices, delivered as separate packages:";

	for (const item of CLIs) {
		if (item.recommended) {
			notify += `\n - ${item.name} (${item.url})\n   ${item.description}`;
		}
	}

	console.error(notify);

	const isYarn = fs.existsSync(path.resolve(process.cwd(), "yarn.lock"));

	const packageManager = isYarn ? "yarn" : "npm";
	const installOptions = [isYarn ? "add" : "install", "-D"];

	console.error(
		`We will use "${packageManager}" to install the CLI via "${packageManager} ${installOptions.join(
			" "
		)}".`
	);

	const question = `Do you want to install 'webpack-cli' (yes/no): `;

	const questionInterface = readLine.createInterface({
		input: process.stdin,
		output: process.stderr
	});
	questionInterface.question(question, answer => {
		questionInterface.close();

		const normalizedAnswer = answer.toLowerCase().startsWith("y");

		if (!normalizedAnswer) {
			console.error(
				"You need to install 'webpack-cli' to use webpack via CLI.\n" +
					"You can also install the CLI manually."
			);
			process.exitCode = 1;

			return;
		}

		const packageName = "webpack-cli";

		console.log(
			`Installing '${packageName}' (running '${packageManager} ${installOptions.join(
				" "
			)} ${packageName}')...`
		);

		runCommand(packageManager, installOptions.concat(packageName))
			.then(() => {
				require(packageName); 
			})
			.catch(error => {
				console.error(error);
				process.exitCode = 1;
			});
	});
} else if (installedClis.length === 1) {
	const path = require("path");
	const pkgPath = require.resolve(`${installedClis[0].package}/package.json`);
	const pkg = require(pkgPath);
	require(path.resolve(
		path.dirname(pkgPath),
		pkg.bin[installedClis[0].binName]
	));
} else {
	console.warn(
		`You have installed ${installedClis
			.map(item => item.name)
			.join(
				" and "
			)} together. To work with the "webpack" command you need only one CLI package, please remove one of them or use them directly via their binary.`
	);

	process.exitCode = 1;
}

分析webpack.js,可以了解到主要有几大部分:

  • process.exitCode = 0
    正常执行返回,如果exitCode!=0,则为抛出对应错误;
  • const runCommand = (command, args) => {...}
    运行某个命令,并以promise形式返回;
  • const isInstall = packageName => {...}
    判断某个包是否安装
  • const CLIs = [...]
    webpack可用的脚手架:webpack-cli和webpack-command
  • const installedClis = CLIs.filter(cli => cli.installed)
    判断脚手架是否安装
  • if (installedClis.length === 0) {...} else if (installedClis.length === 1) {...} else {...}
    根据安装脚手架数量进行判断,如果没有安装则提示报错并提示安装流程;如果安装了一个运行安装的脚手架CLI; 如果安装了两个就抛出错误

所以,npm scripts运行命令行,最终会通过webpack查找webpack-cli/webpack-command,并且执行CLI。

# webpack-cli工作流程

分析不需要编译的命令,例如init、info等命令并不会实例化webpack对象, webpack不需要经过构建编译的过程。

/node_modules/webpack-cli/bin/cli.js

/** ./utils/constants ,webpack-cli提供的不需要编译的命令
* NON_COMPILATION_ARGS = 
*  ["init",  创建一份webpack配置文件
*  "migrate", 进行webpack版本迁移
*  "serve",   运行webpack-serve
*  "generate-loader",  生成webpack loader代码
*  "generate-plugin",  生成webpack plugin代码
*  "info"   返回与本地环境相关的信息
];**/
const { NON_COMPILATION_ARGS } = require("./utils/constants");
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) {
/**
* // ./utils/prompt-command
* module.exports = function promptForInstallation(packages, ...args) {
* 	// 查找node_modules下是否有@webpack-cli下的包名
* 	const nameOfPackage = "@webpack-cli/" + packages;
* 	let packageIsInstalled = false;
* 	let pathForCmd;
* 	// 如果存在
* 	try {
* 		...
*         // 如果不存在当前项目下,则通过全局的依赖包查找返回完整路径
* 		if (!fs.existsSync(pathForCmd)) {...} else {
* 			// 否则在当前依赖包查找返回完整路径
* 			...
* 		}
* 		packageIsInstalled = true;
* 	} catch (err) {
* 		packageIsInstalled = false;
* 	}
* 	// 如果不存在则提示并提供选择安装
* 	if (!packageIsInstalled) {...} else {
* 		return runWhenInstalled(packages, pathForCmd, ...args);
* 	}
* };
* 
* */    
		return require("./utils/prompt-command")(NON_COMPILATION_CMD, ...process.argv);
	}

引入yargs,对命令行进行定制

设置参数分组,将命令划分为九类:

  • Config options:配置相关参数(文件名称、运行环境等)
  • Basic options:基础参数(entry设置、debug模式设置、watch监听设置、devtool设置)
  • Module options:模块参数,给loader设置扩展
  • Output options:输出参数(输出路径、输出文件名称)
  • Advanced options:高级用法(记录设置、缓存设置、监听频率、bail等)
  • Resolving options:解析参数(alias和解析的文件后缀设置)
  • Optimizing options:优化参数
  • Stats options:统计参数
  • options:通用参数(帮助命令、版本信息等)

yargs

// /node_modules/webpack-cli/bin/cli.js 
	const yargs = require("yargs").usage(`webpack-cli ${require("../package.json").version}

// 配置提示信息
Usage: webpack-cli [options]
       webpack-cli [options] --entry <entry> --output <output>
       webpack-cli [options] <entries...> --output <output>
       webpack-cli <command> [options]

For more information, see https://webpack.js.org/api/cli/.`);

/**
* const {
* 	CONFIG_GROUP, Config options:配置相关参数(文件名称、运行环境等)
* 	BASIC_GROUP, Basic options:基础参数(entry设置、debug模式设置、watch监听设置、devtool设置)
* 	MODULE_GROUP, Module options:模块参数,给loader设置扩展
* 	OUTPUT_GROUP, Output options:输出参数(输出路径、输出文件名称)
* 	ADVANCED_GROUP, Advanced options:高级用法(记录设置、缓存设置、监听频率、bail等)
* 	RESOLVE_GROUP, Resolving options:解析参数(alias和解析的文件后缀设置)
* 	OPTIMIZE_GROUP, Optimizing options:优化参数
* 	DISPLAY_GROUP Stats options:统计参数
* } = GROUPS;
*  ./config/config-yargs ,定义了一些基本信息help、version和options输入的一些命令
*  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
*  			},
*  			...
*  		});
*  };
*  
*  
* */
	require("./config/config-yargs")(yargs);

分析命令行参数,对各个参数进行转换,组成编译配置项
引用webpack,根据配置项进行编译和构建

// /node_modules/webpack-cli/bin/cli.js 
// 对命令行参数进行解析
yargs.parse(process.argv.slice(2), (err, argv, output) => {
        ...
		// 定义options输入参数,传递给webpack,webpack-cli会从两处修改options
		// 第一处是webpack.config.js配置文件
		// 第二处是命令行中设置的参数
		let options;
		try {
			// 组装options,转换为webpack可识别的参数
			options = require("./utils/convert-argv")(argv);
		} catch (err) {
		    // 如果报错则抛出错误
			...
			return;
		}

		/**
		 * When --silent flag is present, an object with a no-op write method is
		 * used in place of process.stout
		 */
        ...
		// 判断配置参数在对象上是否存在
		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);
			}
		}

		// 将组装的options传递进去
		function processOptions(options) {
			...
			// 定义输出对象,webpack在输出时候就会读取outputOptions进而做相应的处理
			let outputOptions = options.stats;
			...

			ifArg("display", function(preset) {
				outputOptions = statsPresetToOptions(preset);
			});
			...
			// 引入webpack
			const webpack = require("webpack");
			let lastHash = null;
			let compiler;
			try {
				// 将组装完成的options传递给webpack,返回compiler对象
				compiler = webpack(options);
			} catch (err) {
				//错误抛出
                ...
				throw err;
			}
			...
			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");
				});
			}

			//完成后,compiler回调
			function compilerCallback(err, stats) {
			    //日志打印/错误抛出
			    ...
			}
			// 如果设置watch,则以watch方式运行compiler.watch
			if (firstOptions.watch || options.watch) {
				const watchOptions =
					firstOptions.watchOptions || options.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);
				if (outputOptions.infoVerbosity !== "none") console.error("\nwebpack is watching the files…\n");
			} else {
			    // 否则以compiler.run运行构建
				compiler.run((err, stats) => {
					if (compiler.close) {
						compiler.close(err2 => {
							compilerCallback(err || err2, stats);
						});
					} else {
						compilerCallback(err, stats);
					}
				});
			}
		}
		processOptions(options);
	});

所以,webpack-cli执行的结果就是对配置文件和命令行参数进行转换,最终生成配置选项参数options。 然后根据配置参数实例化webpack对象,执行构建流程。

# 推荐阅读

npm scripts使用指南:http://www.ruanyifeng.com/blog/2016/10/npm_scripts.html