一、webpack 核心模块
Webpack
工程相当庞大,但Webpack
本质上是一种事件流机制。通过事件流将各种插件串联起来,最终完成Webpack
的全流程,而实现事件流机制的核心是Tapable
模块。Webpack
负责编译的Compiler
和创建Bundle
的Compilation
都是继承自Tapable
。Webpack
核心库Tapable
的原理和EventEmitter
类似,但是功能更强大,包括多种类型,通过事件的注册和监听,触发Webpack
生命周期中的函数方法。在Webpack
中,tapable
都是放到对象的hooks
上,所以我们叫它们钩子Tapable
的原理解析,Tapable
的执行流程可以分为四步:
- 使用
tap*
对事件进行注册绑定。根据类型不同,提供三种绑定的方式:tap、tapPromise、tapAsync
,其中tapPromise、tapAsync
为异步类Hook
的绑定方法; - 使用
call*
对事件进行触发,根据类型不同,也提供了三种触发的方式:call、promise、callAsync;
- 生成对应类型的代码片段(要执行的代码实际是拼字符串拼出来的);
- 生成第三步生成的代码片段。
- 总结:
Tapable
是Webpack
的核心模块,Webpack
的所有工作流程都是通过Tapable
来实现的。Tapable
本质上是提供了多种类型的事件绑定机制,根据不同的流程特点可以选择不同类型的Hook
来使用。Tapable
的核心实现在绑定事件阶段跟我们平时的自定义JavaScript
事件绑定(例如EventEmitter
)没有太大区别,但是在事件触发执行的时候,会临时生成可以执行的函数代码片段。通过这种实现方式,Tapable
实现了强大的事件流程控制能力,也增加了如waterfall/parallel
系列方法,实现了异步/并行等事件流的控制能力。
二、Webpack 的 Compiler 和 Compilation
- 在
Webpack
工作流程中,Compiler
和Compilation
都是继承自Tapable
,不同点是Compiler
是每个Webpack
的配置,对应一个Compiler
对象,记录着整个Webpack
的生命周期;在构建的过程中,每次构建都会产生一次Compilation,Compilation
则是构建周期的产物。 - 总结:
Webpack
中两个核心的类Compiler
和Compilation
。Compiler
是每次Webpack
全部生命周期的对象,而Compilation
是Webpack
中每次构建过程的生命周期对象,Compilation
是通过Compiler
创建的实例。两个类都有自己生命周期,即有自己不同的Hook
,通过添加对应Hook
事件,可以拿到各自生命周期关键数据和对象。Compilation
有个很重要的对象是Stats
对象,通过这个对象可以得到Webpack
打包后的所有module、chunk 和 assets
信息,通过分析Stats
对象可以得到很多有用的信息,比如webpack-bundle-analyzer
这类分析打包结果的插件都是通过分析Stats
对象来得到分析报告的。
三、Webpack 的基本流程
Webpack
的基本流程可以分为三个阶段,如下所示:
- 准备阶段:主要任务是创建
Compiler
和Compilation
对象; - 编译阶段:这个阶段任务是完成
modules
解析,并且生成chunks
; module
解析:包含了三个主要步骤,创建实例、loaders
应用和依赖收集;chunks
生成,主要步骤是找到每个chunk
所需要包含的modules
。- 产出阶段:这个阶段的主要任务是根据
chunks
生成最终文件,
- 在产出阶段中,主要有三个步骤:模板
Hash
更新,模板渲染chunk
,生成文件。细化到具体的代码层次,大概可以分为:
- 初始化参数:包括从配置文件和
shell
中读取和合并参数,然后得出最终参数; shell
中的参数要优于配置文件的;- 使用上一步得到的参数实例化一个
Compiler
类,注册所有的插件,给对应的Webpack
构建生命周期绑定Hook
; - 开始编译:执行
Compiler
类的run
方法开始执行编译; compiler.run
方法调用compiler.compile
,在compile
内实例化一个Compilation
类。
Compilation
是做构建打包的事情,主要事情包括:
- 查找入口:根据
entry
配置,找出全部的入口文件; - 编译模块:根据文件类型和
loader
配置,使用对应loader
对文件进行转换处理; - 解析文件的
AST
语法树; - 找出文件依赖关系;
- 递归编译依赖的模块。
- 递归完后得到每个文件的最终结果,根据
entry
配置生成代码块chunk
;
输出所有chunk
到对应的output
路径。 shell
中的参数要优于配置文件。举例说明:配置文件指定了mode
是development
,而shell
中传入了--mode production
,则最终mode
值为production
。- 在
Webpack
工作流程里,Tapable
始终贯穿其中,Tapable
各种Hook
(钩子)组成了Webpack
的生命周期。Tapable Hook
和生命周期的关系为:
Hook
:钩子,对应Tapable
的Hook
;- 生命周期:
Webpack
的执行流程,钩子实际就是生命周期,一般类似entryOption
的Hook
,在生命周期中entry-option
。 - 参与
Webpack
流程的两个重要模块是:Compiler和Compilation
。
-
总结:
Webpack
打包流程从配置文件的读取开始,分别经过了准备阶段、modules
产出阶段、chunks
产出阶段和bundle
产出物产出阶段。在各自阶段,分别有不同的「角色」参与,整个Webpack
的打包流程是通过Compiler
来控制的,而每次打包的过程是通过Compilation
来控制的。在普通打包模式下,webpack
的Compiler
和Compilation
是一一对应的关系;watch
模式下,Webpack
的Compiler
会因为文件变化而产生多次打包流程,所以Compiler
和Compilation
是一对多关系,通过Hook Compiler
的流程,可以得到每次打包过程的回调。 -
Webpack
的工作流程中的类,如下所示:
Tapbale
:Webpack
事件流程核心类;Compiler
:Webpack
工作流程中最高层的对象,初始化配置,提供Webpack
流程的全局钩子,比如done、compilation
这类;Compilation
:由Compiler
来创建的实例对象,是每次打包流程最核心的流程,该对象内进行模块依赖解析、优化资源、渲染runtime
代码等事情,下面在Compilation
中还有用到的一些对象:Resolver
:解析模块(module)、loader
等路径,帮助查找对应的位置;ModuleFactory
:负责构造模块的实例,将Resolver
解析成功的组件中把源码从文件中读取出来,然后创建模块对象;Template
:主要是来生成runtime
代码,将解析后的代码按照依赖顺序处理之后,套上Template
就是我们最终打包出来的代码。
四、 webpack 中的 HMR
HMR
的一个完整周期,整个周期分为两部分:启动阶段和文件监控更新流程。- 在启动阶段,
Webpack
和webpack-dev-server
进行交互。Webpack
和webpack-dev-server
主要是通过Express
的中间件webpack-dev-middleware
进行交互,这个阶段可以细分为以下几个步骤:
webpack-dev-server
启动Webpack
打包的watch
模式,在这种模式下Webpack
会监听文件的变化,一旦有文件发生变化,则会重新进行打包,watch
模式下Webpack
打包的结果不会落盘(保存到硬盘上);webpack-dev-server
通过webpack-dev-middleware
与Webpack
进行交互,Webpack-dev-middleware
初始化会接收Webpack
的Compiler
对象,通过Compiler
的钩子可以监听Webpack
的打包过程;- 如果
devServer.watchContentBase=true
,则webpack-dev-server
监听文件夹中静态文件的变化,发生变化则通知浏览器刷新页面重新请求新的文件; - 打开浏览器之后,
webpack-dev-server
会利用sockjs
在浏览器和Server
之间创建一个WebSocket
长连接,这个长连接是浏览器和webpack-dev-server
的通信桥梁,它们之间的通信内容主要是传递编译模块的文件信息(hash
值),这时候如果Webpack
监控的文件发生了修改,webpack/hot/dev-server
来实现HMR
更新还是刷新页面。
- 注意的是,如下所示:
webpack-dev-server
的contentBase
可以理解为静态资源服务器的目录文件夹,启动server
之后,可以通过网址+电脑中文件路径的方式访问到具体文件,这个文件跟Webpack
打包出来的路径并不一样;- 这里有两个文件变化的监控,第一步中
Webpack
监控整个依赖模块的文件变化,发生变化则重新出发Webpack
编译;第三步中webpack-dev-server
自己监控contentBase
的文件变化,文件发生变化则通知浏览器刷新页面,这里是刷新页面并不是HMR
,这是因为contentBase
内容是非Webpack
打包的依赖文件。 WebSocket
需要服务端和浏览器端都有对应的创建连接代码(new WebSocket),webpack-dev-server
在浏览器中通过在chunks
中插入webpack-dev-server/client
这个文件来创建WebSocket
通信。
- 到此启动阶段结束,当
Webpack
监控的文件发生变化之后,这时候就进入了文件监控更新流程,当Webpack
监控的依赖图中的某个文件修改之后:
Webpack
会重新编译文件,这时候我们在webpack.config.js
中添加的插件HotModuleReplacementPlugin
会生成两次编译之间差异文件列表(manifest)
文件[hash].hot-update.json
,这个manifest JSON
文件包含了变化文件的Update
内容,即[id].[hash].hot-update.js
。webpack-dev-server
中的webpack-dev-middler
会通过Webpack
的Compiler
钩子监听打包进程,然后通知webpack-dev-server
使用WebSocket
长连接推送编译之后的hash
值;- 除了发送编译后
Hash
值之外,webpack-dev-server
还会通过长连接告诉浏览器当前的页面代码是invalid
状态的,需要更新新的代码; - 浏览器拿到
Hash
之后,会首先发起一个Ajax
请求manifest
文件[hash].hot-update.json
文件内容; manifest
列表文件内容拿到之后,会告诉HMR
的Runtime
请求那些变化的JavaScript
文件,这时候会Runtime
会按照清单列表发起JSONP
请求,将两次编译的差异文件[id].[hash].hot-update.js
获取下来,插到页面head
标签的script
中执行,最终完成了更新的全流程。
- 总结:
webpack-dev-server
虽然可以直接来启动HMR
,但是真正核心的是webpack-dev-middleware
。webpack-dev-server
除了这个中间件之外主要功能就是个静态服务器。