前端工程化9:Webpack构建流程分析,Webpack5源码解读

复用君
• 阅读 4655

Webpack构建流程

webpack 核心构建流程总结

webpack5 和 webpack4 源码不同,本文参考 webpack5.38.1 源码
1. 核心构建流程总结

1.1 启动 webpack

1.1.1 执行 node_modules/.bin/webpack.cmd
1.1.2 执行 node_modules/webpack/bin/webpack.js

1.2 启动 webpack-cli

1.2.1 执行 node_modules/webpack-cli/bin/cli.js
1.2.2 检查 webpack 是否安装,没有则提示安装
1.2.3 调用 runCLI() 解析命令行参数,创建编译对象;执行顺序:runCLI() => cli.run() => cli.buildCommand()

1.3 创建编译对象 compiler

1.3.1 调用 cli.buildCommand() => cli.createCompiler() => webpack()

1.4 实例化编译对象 compiler,预埋核心钩子

1.4.1 挂载Node文件读写能力到 compiler
1.4.2 挂载所有插件到 compiler
1.4.3 挂载默认配置到 compiler
1.4.4 启动核心钩子的监听:compilation、make

1.5 执行方法 compiler.run(),启动编译

1.5.1 触发 beforeRun 钩子
1.5.2 触发 run 钩子
- 1.5.2.1 主要功能是 触发 compile、make 钩子,make 完成后处理回调
- 1.5.2.2 make 结束后,通过 onCompiled 写入代码到文件输出到 dist 目录

1.6 执行方法 compiler.compile(),完成编译,输出文件

1.6.1 触发钩子 beforeCompile
1.6.2 触发钩子 compile
1.6.3 触发钩子 thisCompilation、compilation
1.6.4 触发钩子 make(核心钩子)
- 1.6.4.1 根据入口等配置,创建模块
- 1.6.4.2 实现编译功能:转换代码为ast语法树,再将其转换回code代码
- 1.6.4.3 对chunk进行处理
1.6.5 触发钩子 finishMake                    
1.6.6 触发钩子 afterCompile
2. 核心钩子的监听
- beforeRun       =>   未知 
- run             =>   未知
- beforeCompile   =>   未知
- compile         =>   未知 
- thisCompilation =>   未知
- compilation     =>   new WebpackOptionsApply() 中启动监听  => 作用:让 compilation 具备创建模块的能力
- make            =>   new WebpackOptionsApply() 中启动监听  => 作用:打包入口
- finishMake      =>   未知
- afterCompile    =>   未知
3. 核心钩子触发顺序
- beforeRun       =>   compiler.run()中触发
- run             =>   compiler.run()中触发
- beforeCompile   =>   compiler.compile()中触发
- compile         =>   compiler.compile()中触发
- thisCompilation =>   compiler.compile()中触发 => compiler.newCompilation()中触发
- compilation     =>   compiler.compile()中触发 => compiler.newCompilation()中触发
- make            =>   compiler.compile()中触发
- finishMake      =>   compiler.compile()中触发
- afterCompile    =>   compiler.compile()中触发
4. 核心代码文件路径
// 基于版本:
"webpack": "^5.38.1", 
"webpack-cli": "^4.7.0"

// 文件路径:
node_modules/.bin/webpack.cmd
node_modules/webpack/bin/webpack.js
node_modules/webpack-cli/bin/cli.js
node_modules/webpack/lib/webpack.js
node_modules/webpack/lib/EntryPlugin.js
node_modules/webpack/lib/Compiler.js
node_modules/webpack/lib/Compilation.js

webpack 构建流程详细分析

1. 启动 webpack(命令行执行 webpack)
  • 1.1 执行脚本:node_modules/.bin/webpack.cmd
  • 1.2 webpack.cmd 中先根据环境变量找到 node.exe
  • 1.3 webpack.cmd 中再使用 node.exe 执行js脚本:node node_modules/webpack/bin/webpack.js
# node_modules/.bin/webpack.cmd
@ECHO off
SETLOCAL
CALL :find_dp0
# 1.2. 先根据环境变量找到 node.exe
IF EXIST "%dp0%\node.exe" (
  SET "_prog=%dp0%\node.exe"
) ELSE (
  SET "_prog=node"
  SET PATHEXT=%PATHEXT:;.JS;=;%
)
# 1.3. 再使用 node.exe 执行js脚本
"%_prog%"  "%dp0%\..\webpack\bin\webpack.js" %*
ENDLOCAL
EXIT /b %errorlevel%
:find_dp0
SET dp0=%~dp0
EXIT /b
  • 1.4 webpack.js 中判断是否安装了 webpack-cli
> 1.4.1 没有安装webpack-cli => 则提示安装
> 1.4.2 已经安装webpack-cli => 执行 runCli,require 了 node_modules/webpack-cli/bin/cli.js
// node_modules/webpack/bin/webpack.js
#!/usr/bin/env node
// ...
const runCli = cli => {
    const path = require("path");
    const pkgPath = require.resolve(`${cli.package}/package.json`);
    const pkg = require(pkgPath);
    require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
};

// 没有安装 webpack-cli,则提示安装
if (!cli.installed) {
    // ...
} else {
    runCli(cli);
}
2. 启动 webpack-cli
  • 2.1 启动 webpack-cli:webpack.js 中导入了 node_modules/webpack-cli/bin/cli.js
  • 2.2 cli.js 中检查 webpack 是否安装,没有则提示安装
  • 2.3 cli.js 中执行 runCLI(),解析命令行参数,创建编译对象。(例如:读取配置文件,合并默认配置项等。)
> 2.3.1 runCLI() 中执行 cli.run()           => 解析命令行参数  => .../lib/webpack-cli.js
> 2.3.2 cli.run() 中执行 cli.buildCommand() => 创建编译对象    => .../lib/webpack-cli.js
// node_modules/webpack-cli/bin/cli.js
#!/usr/bin/env node
// ...
// 2.2 检查 webpack 是否安装
if (utils.packageExists('webpack')) {
    // 参数1:process.argv => 命令行参数<br>
    // 参数2:Module.prototype._compile => nodejs底层的编译方法<br>
    // 2.3 执行runCLI
    runCLI(process.argv, originalModuleCompile);
} 
else{
    // ...
}
3. 创建编译对象 compiler
  • 3.1 调用 cli.createCompiler() 生成一个 compiler
// cli.buildCommand() 中调用 cli.createCompiler() 生成一个 compiler;
// cli.buildCommand() 定义在:node_modules/webpack-cli/lib/webpack-cli.js
compiler = await this.createCompiler(options, callback);

// cli.createCompiler() 中调用 webpack(options) 返回一个 compiler
// webpack(options) 定义在:node_modules/webpack/lib/webpack.js
compiler = this.webpack(
// 合并options
// ...
)
  • 3.2 根据参数判断是否要结束命令行,是否watch
if (isWatch(compiler) && this.needWatchStdin(compiler)) {
    process.stdin.on('end', () => {
        process.exit(0);
    });
    process.stdin.resume();
}
4. 实例化编译对象 compiler,预埋核心钩子
  • 4.1 webpack() 中调用 createCompiler(),定义在:node_modules/webpack/lib/webpack.js
  • 4.2 createCompiler() 中创建一个 comipler 实例
> 4.2.1 createCompiler() 中使用 new Compiler() 实例化一个 compiler,定义在:.../lib/Compiler.js
> 4.2.2 compiler 继承了 tapable,因此它具备钩子的操作能力(监听事件,触发事件,webpack是一个事件流)
  • 4.3 在 compiler 对象身上挂载文件读写的能力:
new NodeEnvironmentPlugin({
    infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
  • 4.4 在 compiler 对象身上挂载所有 plugins 插件
  • 4.5 在 compiler 对象身上挂载默认配置项:
applyWebpackOptionsDefaults(options);
  • 4.6 启动 compilation、make 钩子的监听,具体流程如下:
> 4.6.1 createCompiler() 方法中调用 new WebpackOptionsApply().process(options, compiler);
> 4.6.2 然后启动钩子 entryOption 的监听;(注意hooks都是new Compiler时定义)
> 4.6.3 触发钩子 entryOption 的执行回调函数:启动了 compilation 钩子和 make 钩子的监听
// 4.6.2 然后启动钩子 entryOption 的监听,在 EntryOptionPlugin 中定义的。(注意hooks都在new Compiler时定义)
new EntryOptionPlugin().apply(compiler);

// 4.6.3 触发钩子 entryOption 的执行回调函数:启动了 compilation 钩子和 make 钩子的监听;回调在EntryOptionPlugin中定义的。
compiler.hooks.entryOption.call(options.context, options.entry);

// 4.6.3 触发钩子 entryOption 的执行回调时,执行如下代码;启动钩子 make 的监听。
compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {
  const { context, entry, name } = this
  console.log("make 钩子监听执行了~~~~~~")
  // compilation.addEntry(context, entry, name, callback)
})
5. webpack() 中执行 compiler.run() 触发一系列钩子
webpack() 定义在:node_modules/webpack/lib/Compiler.js
  • 5.1 触发 beforeRun 钩子
  • 5.2 触发 run 钩子
  • 5.3 执行 compile 方法(该方法里面继续触发了一堆钩子,参考第6点)
  • 5.4 执行 compile 方法成功后,执行 onCompiled 将模块代码写入文件:writeFile()
// run() 里就是一堆钩子按着顺序触发
const run = () => {
    // 5.1 触发 beforeRun 钩子
    this.hooks.beforeRun.callAsync(this, err => {
        if (err) return finalCallback(err);
        // 5.2 触发 run 钩子
        this.hooks.run.callAsync(this, err => {
            if (err) return finalCallback(err);
            this.readRecords(err => {
                if (err) return finalCallback(err);
                // 5.3 执行 compile 方法
                this.compile(onCompiled);
            });
        });
    });
};
6. run() 中执行 compile()
run() 定义在:node_modules/webpack/lib/Compiler.js
  • 6.1 触发钩子 beforeCompile
  • 6.2 触发钩子 compile
  • 6.3 触发钩子 thisCompilation、compilation (让 compilation 具备创建模块的能力)
  • 6.4 触发钩子 make (重要:make 里面的 compilation.addEntry 根据入口等一些配置项创建模块,执行编译等操作)
  • 6.5 触发钩子 finishMake
const params = this.newCompilationParams();

this.hooks.beforeCompile.callAsync(params, err => {
    // ...
    this.hooks.compile.call(params);
    // 在newCompilation() 里面触发钩子 thisCompilation、compilation
    const compilation = this.newCompilation(params);
    // ...
    this.hooks.make.callAsync(compilation, err => {
        // ...
        this.hooks.finishMake.callAsync(compilation, err => {
            // ...
        });
    });
});
7. 关于 addEntry 的分析(编译入口的重点)
  • 7.1 make 钩子触发时执行 compilation.addEntry(),调用前的几个参数说明:见以下代码注释
  • 7.2 make 钩子触发时接受一个 compilation 传递给 addEntry(),addEntry() 是在new compilation()里面定义
// entry 当前打包模块的相对路径(/src/index.js)
// options 里面包括 name、filename、publicPath 等配置
// context 当前项目根路径
// dep 是对当前入口模块依赖关系的处理 
const { entry, options, context } = this;
const dep = EntryPlugin.createDependency(entry, options);
// 这里是 make 钩子的监听
// compilation 是 make 钩子触发的时候传给回调函数的值,addEntry 定义在 compilation 实例化的类里面
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
    compilation.addEntry(context, dep, options, err => {
        callback(err);
    });
});
// 这里是触发 make 钩子
this.hooks.make.callAsync(compilation, err => { // ... })
  • 7.3 进入入口函数过后按照以下顺序调用 => Compilation.js
addEntry() => _addEntryItem() => addModuleTree() => handleModuleCreation()
  • 7.4 执行 addModuleTree(),在 compilation 当中我们可以通过 NormalModuleFactory 来创建一个普通模块对象;然后通过 dependencyFactories 获取它
// addModuleTree() 中 moduleFactory 是 NormalModuleFactory 创建的普通模块对象
const moduleFactory = this.dependencyFactories.get(Dep);
  • 7.5 handleModuleCreation() 方法中按照以下顺序调用 => Compilation.js
// 注意:factory 就是上面的参数 moduleFactory 
factorizeModule() => factorizeQueue() => _factorizeModule() => factory.create()
  • 7.6 factory.create() 创建一个模块
> 7.6.1 factory.create() 是定义在 NormalModuleFactory 中的一个成员方法(NormalModuleFactory .js)
> 7.6.2 this.hooks.beforeResolve.callAsync 处理 loader 的路径编译工作,在回调里面触发钩子:factorize
> 7.6.3 this.hooks.factorize.callAsync 处理 loader,然后将 factoryResult 传给 callback()
  • 7.7 factory.create() 执行完成,将 factoryResult 放到 callback() 中执行
> 7.7.1 factory.create 的 callback() 拿到 result(实际就是 factoryResult )
const newModule = result.module;
> 7.7.2 newModule 传递给上面的 factorizeModule() 的 callback();

// 回调 callback 拿到 newModule,然后执行addModule
this.factorizeModule({
    // ...
},(err, newModule){// 回调 callback 拿到 newModule 
    // ...
    this.addModule()
})
  • 7.8 factory.create() 的回调中调用 addModule(newModule) 去创建 module
this.factorizeModule => ... => factory.create() => this.addModule()=> this.buildModule()
  • 7.9 this.buildModule() 编译模块,最后将 doBuild 的执行结果传递给 callback;
> 7.9.1 doBuild() 编译完成的结果存在 this.modules 中
> 7.9.2 doBuild() 方法中实现了核心的编译功能:将 js 代码转为 ast 语法树 => 然后再转换成 code 代码
> 7.9.3 build 在 NormalModule.js 文件中,this.buildModule() => build => doBuild
其中一些知识点:
  1. 什么是取值函数?参考:https://es6.ruanyifeng.com/?s...
const source = {
  get foo() { return 1 }
};
class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'
  1. AST 语法树,参考:https://segmentfault.com/a/11...

tapabel库与钩子

tapabel是什么?

tapable 是一个类似于 Node.js 中的 EventEmitter的库,但更专注于自定义事件的触发和处理。webpack 通过 tapable 将实现与流程解耦,所有具体实现通过插件的形式存在。

tapabel的分类

同步钩子(syncHook)
  1. 熔断钩子(syncBialHook)
  2. 瀑布钩子(syncWaterfallHook)
  3. 循环钩子(syncLoopHook)
异步钩子(asyncHook)

对于异步钩子的使用,在添加事件监听时会存在三种方式: tap tapAsync tapPromise

  1. 异步串行钩子(asyncSeriesHook)
  2. 异步并行钩子(asyncParallelHook)
  3. 异步并行熔断钩子(asyncParallelBailHook)
钩子的监听与触发
const { SyncHook } = require('tapable')
// 钩子的定义
let hook = new SyncHook(['name', 'age'])
// 钩子的监听
hook.tap('fn1', function (name, age) {
  console.log('fn1--->', name, age)
})
// 钩子的触发
hook.tap('fn2', function (name, age) {
  console.log('fn2--->', name, age)
})

hook.call('zoe', 18)
代码示例,参考:本文 Webpack 相关源码
相关知识点:

1. 发布订阅模式和观察者模式
2. NodeJs 中的 EventEmitter
3. Tapable 的相关知识点

特别鸣谢:拉勾教育前端高薪训练营

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
可莉 可莉
3年前
18个常用 webpack插件,总会有适合你的!
!(https://oscimg.oschina.net/oscnet/71317da0c57a8e8cf5011c00e302a914609.jpg)来源| https://github.com/Michaellzg/myarticle/blob/master/webpack/Plugin何为插
Wesley13 Wesley13
3年前
VBox 启动虚拟机失败
在Vbox(5.0.8版本)启动Ubuntu的虚拟机时,遇到错误信息:NtCreateFile(\\Device\\VBoxDrvStub)failed:0xc000000034STATUS\_OBJECT\_NAME\_NOT\_FOUND(0retries) (rc101)Makesurethekern
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Easter79 Easter79
3年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
美凌格栋栋酱 美凌格栋栋酱
5个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(