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
上一篇下一篇

猜你喜欢

热点阅读