Webpack 构建流程简介

webpack的核心概念

  • Entry: 入口,webpack执行构建的第一入口,可以抽象理解为input
  • Module: 模块, 在webpack里面一切皆是模块,一个模块对应一个文件,webpack会从配置的Entry开始,递归找到所有依赖的模块。
  • Chunk:代码块, 一个chunk是由多个模块组合而成,用于代码合并与分割。
  • Loader: 模块转换器,用于将模块的元内容按照需求转换成新内容。
  • Plugin: 扩展插件,在webpack构建流程中的特定时机会广播对应的事件,插件可以监听这些事件,在特定的时机做对应的事情。

webpack 中,module,chunk 和 bundle 的区别是什么?

先上一篇参考文章中的图片

对于一份同逻辑的代码,当我们手写下一个一个的文件,它们无论是 ESM 还是 commonJS 或是 AMD,他们都是 module

当我们写的 module 源文件传到 webpack 进行打包时,webpack 会根据文件引用关系生成 chunk 文件,webpack 会对这个 chunk 文件进行一些操作;

webpack 处理好 chunk 文件后,最后会输出 bundle 文件,这个 bundle 文件包含了经过加载和编译的最终源文件,所以它可以直接在浏览器中运行。

一般来说一个 chunk 对应一个 bundle,比如上图中的 utils.js -> chunks 1 -> utils.bundle.js;但也有例外,比如说上图中,我就用 MiniCssExtractPlugin 从 chunks 0 中抽离出了 index.bundle.css 文件。

webpack构建流程分析

Webpack 源码是一个插件的架构,很多功能都是通过诸多的内置插件实现的。Webpack为此专门自己写一个插件系统,叫 Tapable 主要提供了注册和调用插件的功能。

webpack的流程

webpack是一个串行的过程,从启动到结束会依次执行以下流程

  • 初始化参数: 从shell参数和配置文件合并参数,得出最终的参数
  • 开始编译:从上一步获得的参数初始化compiler对象,加载所有的插件,通过run方法执行编译。
  • 确定入口:根据配置文件的entry找出所有入口文件。
  • 编译模块:从入口文件开始,调用所有配置的loader对模块进行翻译成compliation,然后递归所有依赖的模块,然后重复编译。得到每个模块翻译后的最终内容以及它们之间的依赖关系。
  • 输出资源:根据入口和模块的依赖关系,组装成一个个包含多个模块的chunk,然后将chunk转换成一个单独的文件加入输出列表,这是可以修改输出内容的最后机会
  • 输出完成: 在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内容写入文件系统上。

img

WebPack 编译流程图 原图出自:blog.didiyun.com/index.php/2…

在以上过程中,webpack会在特定的时间点广播特定的事件,插件通过监听到感兴趣的事件后执行特定的逻辑,并且改变webpack的运行结果。

这篇文章会从源码级别讲解流程,有兴趣或者有需要可以回头看。

理解事件流机制 Tabable

webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable。

WebpackTapable 事件流机制保证了插件的有序性,将各个插件串联起来, Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条webapck机制中,去改变webapck的运作,使得整个系统扩展性良好。

Tapable也是一个小型的 library,是Webpack的一个核心工具。类似于node中的events库,核心原理就是一个订阅发布模式。作用是提供类似的插件接口。

webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例,可以直接在 CompilerCompilation 对象上广播和监听事件,方法如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* 广播事件
* event-name 为事件名称,注意不要和现有的事件重名
*/
compiler.apply('event-name',params);
compilation.apply('event-name',params);
/**
* 监听事件
*/
compiler.plugin('event-name',function(params){});
compilation.plugin('event-name', function(params){});

Tapable类暴露了taptapAsynctapPromise方法,可以根据钩子的同步/异步方式来选择一个函数注入逻辑。

tap 同步钩子

1
2
3
compiler.hooks.compile.tap('MyPlugin', params => {
console.log('以同步方式触及 compile 钩子。')
}

tapAsync 异步钩子,通过callback回调告诉Webpack异步执行完毕 tapPromise 异步钩子,返回一个Promise告诉Webpack异步执行完毕

1
2
3
4
5
6
7
8
9
10
compiler.hooks.run.tapAsync('MyPlugin', (compiler, callback) => {
console.log('以异步方式触及 run 钩子。')
callback()
})

compiler.hooks.run.tapPromise('MyPlugin', compiler => {
return new Promise(resolve => setTimeout(resolve, 1000)).then(() => {
console.log('以具有延迟的异步方式触及 run 钩子')
})
})

Tapable用法

1
2
3
4
5
6
7
8
9
10
11
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");

tapable

简单实现一个 SyncHook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Hook{
constructor(args){
this.taps = []
this.interceptors = [] // 这个放在后面用
this._args = args
}
tap(name,fn){
this.taps.push({name,fn})
}
}
class SyncHook extends Hook{
call(name,fn){
try {
this.taps.forEach(tap => tap.fn(name))
fn(null,name)
} catch (error) {
fn(error)
}

}
}
复制代码

tapable是如何将webapck/webpack插件关联的?

Compiler.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const { AsyncSeriesHook ,SyncHook } = require("tapable");
//创建类
class Compiler {
constructor() {
this.hooks = {
run: new AsyncSeriesHook(["compiler"]), //异步钩子
compile: new SyncHook(["params"]),//同步钩子
};
},
run(){
//执行异步钩子
this.hooks.run.callAsync(this, err => {
this.compile(onCompiled);
});
},
compile(){
//执行同步钩子 并传参
this.hooks.compile.call(params);
}
}
module.exports = Compiler

MyPlugin.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const Compiler = require('./Compiler')

class MyPlugin{
apply(compiler){//接受 compiler参数
compiler.hooks.run.tap("MyPlugin", () => console.log('开始编译...'));
// 在run阶段添加方法
compiler.hooks.complier.tapAsync('MyPlugin', (name, age) => {
setTimeout(() => {
console.log('编译中...')
}, 1000)
});
// 在compier阶段添加方法
}
}

//这里类似于webpack.config.js的plugins配置
//向 plugins 属性传入 new 实例

const myPlugin = new MyPlugin();

const options = {
plugins: [myPlugin]
}
let compiler = new Compiler(options)
compiler.run()

想要深入了解tapable的文章可以看看这篇文章:

webpack4核心模块tapable源码解析: https://www.cnblogs.com/tugenhua0707/p/11317557.html

总结

总的来说,webpack的打包分为几个阶段,类似于生命周期,每个生命周期开始和结束都可以发出事件,loader是编译阶段用于编译我们指定类型文件的。

所以就像前面所说,webpack的构建流程是基于事件的,对于webpack而言,它的事件发出和订阅都是依赖于Tapable类,我们的Plugin其实就是 一个个的Tapable类,而我们用到的compiler或者compilation就是内置的Plugin。

我么可以利用Tapable类的apply和plugin方法发出和订阅事件,由于complier本身是一个Tapable,所以它自身可以apply和plugin事件,而我们在complier的hooks上通过类似tap的方法添加函数,就相当于监听了该hooks发出的事件。

参考文章:

webpack构建流程分析

webpack 中那些最易混淆的 5 个知识点

揭秘webpack插件工作流程和原理

webpack插件编写及原理解析