美文网首页
webpack5 启动流程部分源码分析

webpack5 启动流程部分源码分析

作者: 再见地平线_e930 | 来源:发表于2021-06-18 08:59 被阅读0次
    在我们执行 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
    

    相关文章

      网友评论

          本文标题:webpack5 启动流程部分源码分析

          本文链接:https://www.haomeiwen.com/subject/sajxeltx.html