最接地气的webpack源码解析(一)

1,770 阅读4分钟

记2021年底-2022年初各互联网大厂裁员风波内的个人感受,也是计划写这个专题的初衷。笔者作为一名前端开发,也担心哪天突然收到离职大礼包,上午收到通知,下午就要走人的尴尬境遇。作为底层的开发人员,我深切感受到前路的迷茫和不确定,但是与其惶惶度日,不如静下心来沉淀自己的技术,有技术在身总会让人觉得心安。

准备工作

开始进入正题,本次的webpack分享不会停留在webpack配置层,将会从执行入口、构建前准备阶段、编译构建阶段、构建后优化阶段、文件输出阶段这五部分进行介绍。这里特别感谢“极客时间”的 《玩转webpack》 作者 程柳锋,笔者是在借鉴视频课程的基础上学习webpack4.0源码进行的总结分享,另外也想借此机会安利一波该课程,视频内容从浅入深,很适合入门学习。

执行入口

通常我们构建打包过程可以简化为以下三步:

  1. 在项目根目录配置webpack.config.js文件,文件内对webpack进行各种配置(entry output module plugins devtool env等等)
  2. 在package.json中添加脚本执行指令
"scripts": {
    "build": "webpack"
}
  1. 在终端运行npm run build即可开始打包构建

但是黑盒内到底隐藏着怎样的真相?

当运行npm run build指令时,npm会让命令行工具进入node_modules目录下的.bin目录查找是否存在webpack.sh或者webpack.cmd,所以实际执行的文件是node_modules/.bin/webpack

本着求真的原则,这里稍微扩展一下node_modules的这个.bin目录是怎么生成的,其实npm这个包管理工具,会根据每个依赖包中json对象的bin字段,在.bin目录下创建该字段指向文件的镜像。所以可以看看webpack依赖包里的package.json文件,发现"bin": {"webpack": "bin/webpack.js"}这块代码。

到这一步,我们就找到了webpack运行时的真正入口:node_modules/webpack/bin/webpack.js

后面就开始针对实际代码进行解析分享,这部分建议大家对照着webpack4.x的源码来看,文章中会对主流程代码进行提取讲解,不过具体的代码处理过程需要大家观看源码。

个人读 “这部分源码” 时感觉受益匪浅,主流程脉络清晰,即使没有注释可以知道每个代码块负责怎样的功能,和webpack相关的其他代码文件不同,webpack/bin/webpack.js作为入口文件相当有含金量!

webpack/bin/webpack.js

process.exitCode = 0;

const runCommand = (command, args) => {};

const isInstalled = packageName => {};

const CLIs = [];

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

if (installedClis.length === 0) {}
else if (installedClis.length === 1) {}
else {}
  1. 设置进程退出时的状态码为0,表示构建进程正常结束;
  2. 分别定义runCommand——运行指定命令的函数、isInstalled——判断某个依赖是否被引入的函数、CLIs——webpack命令行工具的枚举值;
  3. 获取用户已安装的命令行工具数组installedClis
  4. 如果用户未安装webpack命令行工具则提示用户安装,如果用户安装了一个命令行工具则正常运行require('webpack-cli/bin/cli.js'),如果用户安装了两个命令行工具则提示用户删掉一个webpack命令行工具或者直接用指定的命令行工具去运行

看到这里,其实可以发现webpack/bin/webpack.js这个入口文件的作用就是为了引入webpack-cli这个命令行工具。如果用户安装了webpack-cli,最后会执行require('webpack-cli/bin/cli.js')这行代码,引入cli.js这个文件进行后续处理。

webpack-cli/bin/cli.js

const { NON_COMPILATION_ARGS } = require("./utils/constants");

(function() {
    const NON_COMPILATION_CMD = process.argv.find();
    if (NON_COMPILATION_CMD) {
        return require("./utils/prompt-command")(NON_COMPILATION_CMD, ...process.argv);
    }
    
    const yargs = require("yargs").usage(`webpack-cli ${require("../package.json").version};
    yargs.parse(process.argv.slice(2), (err, argv, output) => {
        options = require("./utils/convert-argv")(argv);
        
        const webpack = require("webpack");
        compiler = webpack(options);
        
        if (firstOptions.watch || options.watch) {
            compiler.watch(watchOptions, compilerCallback);
        } else {
            compiler.run();
        }
    });
})()

这部分处理流程稍微多一点,不过关键节点比较清晰,可以先将主流程一分为二,一个是用户运行的指令不需要编译;另一个是需要编译的指令。

如何判断用户输入的指令是否需要编译?webpack-cli事先定义了不需要编译指令的枚举值数组,在./utils/constants.js文件中的NON_COMPILATION_ARGS数组,const NON_COMPILATION_ARGS = ["init", "migrate", "serve", "generate-loader", "generate-plugin", "info"],对于不需要编译的指令直接调用promptForInstallation(packages, ...args)方法去运行。

对于需要进行编译构建的指令,首先引入yarg对“npm run”后面的指令进行解析,然后根据解析后的argv去生成webpack配制项参数options,再使用配制项参数实例化webpack,然后判断构建模式是watch还是run并开始运行。

本专题的主线是webpack构建打包的源码分析,后续将主要讲解compiler.run()对应的源码处理。