webpack5 启动流程部分源码分析
2021-06-18 本文已影响0人
再见地平线_e930
在我们执行 npm run build 命令来对我们的项目进行打包时,实际上是执行 package.json 文件中的 build 命令,如:
"build": "webpack --config ./config/webpack.common.js --env production",
该命令实际上等于下面这条命令:
npx webpack-------
实际上执行的是我们 node_modules/.bin 文件下的 webpack.js 文件,(但在新版本的 webpack 中该文件好像移动到了 node_modules/webpack/bin/webpack.js)
1. 接下来是对 webpack.js 源码的解读:
具体请看我在源码中的注释
#!/usr/bin/env node
/**
* @param {string} command process to run
* @param {string[]} args command line arguments
* @returns {Promise<void>} promise
*/
// 安装 webpack-cli 的函数
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;
}
};
/**
* @param {CliOption} cli options
* @returns {void}
*/
// 判断是否安装 webpack-cli 的函数
const runCli = cli => {
const path = require("path");
const pkgPath = require.resolve(`${cli.package}/package.json`); // 拿到 webpack-cli/package.json 文件的路径
const pkg = require(pkgPath); // 拿到 package.json 文件
require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName])); // 通过 package.json 中 bin 命令,导入 bin/cli.js 这个 js 文件,相当于执行 bin/cli.js 文件
};
/**
* @typedef {Object} CliOption
* @property {string} name display name
* @property {string} package npm package name
* @property {string} binName name of the executable file
* @property {boolean} installed currently installed?
* @property {string} url homepage
*/
/** @type {CliOption} */
// 1. 从这里开始读,定义了 cli 对象
const cli = {
name: "webpack-cli",
package: "webpack-cli",
binName: "webpack-cli",
installed: isInstalled("webpack-cli"), // 调用 isInstalled 函数判断是否安装 cli
url: "https://github.com/webpack/webpack-cli"
};
// 如果 webpack cli 没有安装,则会报错并提示
if (!cli.installed) {
const path = require("path");
const fs = require("graceful-fs");
const readLine = require("readline");
const notify =
"CLI for webpack must be installed.\n" + ` ${cli.name} (${cli.url})\n`;
console.error(notify);
let packageManager;
if (fs.existsSync(path.resolve(process.cwd(), "yarn.lock"))) {
packageManager = "yarn"; // 如果你用 yarn,则提示你用 yarn 安装
} else if (fs.existsSync(path.resolve(process.cwd(), "pnpm-lock.yaml"))) {
packageManager = "pnpm";
} else {
packageManager = "npm"; // 如果你用 npm,则提示你用 npm 安装
}
const installOptions = [packageManager === "yarn" ? "add" : "install", "-D"];
console.error(
`We will use "${packageManager}" to install the CLI via "${packageManager} ${installOptions.join(
" "
)} ${cli.package}".`
);
const question = `Do you want to install 'webpack-cli' (yes/no): `;
const questionInterface = readLine.createInterface({ // 通过 readLine 接受用户的输入和输出
input: process.stdin,
output: process.stderr
});
// In certain scenarios (e.g. when STDIN is not in terminal mode), the callback function will not be
// executed. Setting the exit code here to ensure the script exits correctly in those cases. The callback
// function is responsible for clearing the exit code if the user wishes to install webpack-cli.
process.exitCode = 1;
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."
);
return;
}
process.exitCode = 0;
console.log( // 提示正在安装 webpack-cli
`Installing '${
cli.package
}' (running '${packageManager} ${installOptions.join(" ")} ${
cli.package
}')...`
);
runCommand(packageManager, installOptions.concat(cli.package)) // 执行 runCommand 函数来安装 webpack-cli
.then(() => {
runCli(cli); // 成功安装 webpack-cli 后将运行脚手架
})
.catch(error => {
console.error(error);
process.exitCode = 1;
});
});
} else {
runCli(cli); // 如果我们在打包之前已安装 webpack-cli,则会直接调用 runCli(cli) 来运行 webpack 脚手架
}
2. 执行完 webpack.js 文件后,紧接着会执行 node_modules/webpack-cli/bin/cli.js 文件:
#!/usr/bin/env node
'use strict';
const Module = require('module');
const originalModuleCompile = Module.prototype._compile;
require('v8-compile-cache');
const importLocal = require('import-local');
const runCLI = require('../lib/bootstrap');
const utils = require('../lib/utils');
if (!process.env.WEBPACK_CLI_SKIP_IMPORT_LOCAL) {
// Prefer the local installation of `webpack-cli`
if (importLocal(__filename)) {
return;
}
}
process.title = 'webpack';
if (utils.packageExists('webpack')) { // 使用 utils 工具文件中的 packageExists 方法判断 webpack 包是否存在
runCLI(process.argv, originalModuleCompile); // 最终会执行 runCLI 方法
} else {
const { promptInstallation, logger, colors } = utils;
promptInstallation('webpack', () => { // 提示用户需要安装 webpack
utils.logger.error(`It looks like ${colors.bold('webpack')} is not installed.`);
})
.then(() => { // 会自动帮我们安装 webpack 并提示安装成功(具体安装步骤在 utils 中)
logger.success(`${colors.bold('webpack')} was installed successfully.`);
runCLI(process.argv, originalModuleCompile);
})
.catch(() => {
logger.error(`Action Interrupted, Please try once again or install ${colors.bold('webpack')} manually.`); // 安装失败的提示
process.exit(2);
});
}
cli.js 文件最终会执行 runCLI 方法
3. 我们进入 runCLI 方法所在的 bootstrap.js 文件(runCLI 方法主要定义了一个 webpack 对象,并执行了该对象的 run 方法):
const runCLI = async (args, originalModuleCompile) => {
try {
// Create a new instance of the CLI object
// 1. 创建 webpack-cli 对象
const cli = new WebpackCLI();
cli._originalModuleCompile = originalModuleCompile;
// 2. 执行 webpack-cli 对象的 run 方法
await cli.run(args);
} catch (error) {
utils.logger.error(error);
process.exit(2);
}
};
4. 我们点击 WebpackCLI 进入 webpack-cli.js 文件:
该文件主要创建了一个 Webpack-CLI 类,在该类的构造函数中导入了 webpack (本质上,webpack 导出的就是一个 webpack 函数)
class WebpackCLI {
constructor() {
// Global
// 这里的 this.webpack 实际上是一个函数
this.webpack = require(process.env.WEBPACK_PACKAGE || 'webpack'); // 导入 webpack
this.logger = utils.logger;
this.utils = utils;
// Initialize program
this.program = program;
this.program.name('webpack');
this.program.configureOutput({
writeErr: this.logger.error,
outputError: (str, write) => write(`Error: ${this.utils.capitalizeFirstLetter(str.replace(/^error:/, '').trim())}`),
});
}
在 run 方法里面 => loadCommendByName => this.makeCommend(检测一些包是否安装) => this.buildCommend => this.createCompiler => compiler => this.webpack
这里的核心就是 compiler 函数,它将我们的命令和 webpack 配置相合并
try {
// webpack(config, callback)
// callback: 会自动调用 run 方法(在 runCLI 方法中有提到,在文章标题 3)
// 没有传 callback,要手动通过 compiler 调用 run 方法 run((err, status) => {})
compiler = this.webpack( // 执行该函数
config.options, // 传入我们自己的 webpack.config.js 文件中的配置和 package.json 文件中的命令的结合
callback
? (error, stats) => {
if (error && this.isValidationError(error)) {
this.logger.error(error.message);
process.exit(2);
}
callback(error, stats);
}
: callback,
);
} catch (error) {
if (this.isValidationError(error)) {
this.logger.error(error.message);
} else {
this.logger.error(error);
}
process.exit(2);
}
上面提到的 this.webpack() 方法很重要,传入我们的命令和配置,就能实现 webpack-cli 的功能
我们也可以自己实现一个类似于 webpack-cli 的功能:
const webpack = require('webpack');
const config = require('./config/webpack.common')({ // 导入 webpack 配置函数(自带 webpack 配置),参数为 webpack 的命令
production: true // 参数为 webpack 的命令
});
const compiler = webpack(config);
compiler.run((err, status) => {
if(err) {
console.log(err)
} else {
console.log(status)
}
})
执行:
node ./build.js