Webpack5源码解读 - 模块热更新原理
HMR介绍 & 使用
介绍
Hot Module Replacement (HMR) exchanges, adds, or removes modules while an application is running, without a full reload. This can significantly speed up development in a few ways:
- Retain application state which is lost during a full reload.
- Save valuable development time by only updating what's changed.
- Instantly update the browser when modifications are made to CSS/JS in the source code, which is almost comparable to changing styles directly in the browser's dev tools.
上面来源于webpack官方文档介绍,总结起来是webpack热更新模式能够做到当应用代码发生变更、增删时,无需刷新应用,而是以模块为最小颗粒度进行模块替换或修改的能力,从而提高开发效率。
HMR使用
webpack配置
webpack配置文件中devServer.hot
为true
时,即可开启热更模式:
module.exports = {
entry: {
app: './src/index.js',
},
devtool: 'inline-source-map',
devServer: {
static: './dist',
hot: true,
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
模块自定义热更能力
HMR能力提供了以模块为最小颗粒度更新能力,每个模块都可以自主添加本模块或子模块热更逻辑。
模块热更API
webpack内置提供JavaScript模块处理能力,每个模块都能够编写当前模块或者子模块发生模块热更新时的处理逻辑,webpack提供了热更新API,列举如下:
API | 含义 |
---|---|
module.hot.accept | 提供子模块或者当前模块发生变化时,接收新模块代码时的处理逻辑 |
module.hot.dispose | 提供子模块或者当前模块发生变化时,旧模块卸载逻辑 |
module.hot.removeDisposeHandler | 移除dispose回调方法 |
module.hot.decline | 提供模块热更新失败时处理逻辑 |
module.hot.invalidate | 将当前模块失效,执行该方法会调用模块的dispose方法并且重新创建当前模块 |
module.hot.status | 查询当前状态机状态,具体的状态如下:![]() |
module.hot.check | 主动检查是否存在需要更新的模块 |
module.hot.apply | 主动应用更新的模块 |
module.hot.addStatusHandler | 添加状态变更监听事件 |
module.hot.removeStatusHandler | 移除状态变更监听事件 |
热更新处理流程
当模块发生热更新时,HMR能力会从本模块开始寻找模块更新处理逻辑,如果本模块找不到,那么会冒泡往上模块寻找,直至找到处理逻辑,如果到根模块后也无法找到对应处理逻辑,那么会重新刷新页面。
架构设计
Webpack官方文档分别从应用、编译器、模块、浏览器Runtime角度解释原理:
角度 | 原理 |
---|---|
模块层面 | 模块是热更新变更的最小颗粒度,可以通过模块定义发生变更时的处理逻辑,应用模块变更时会调用对应代码。Api介绍具体可见webpack提供了热更新API。 |
应用层面 | 应用可以通过4个步骤完成模块的替换1. 模块通过Api调用Runtime 检查更新代码(check方法); |
- Runtime模块下载变更模块,并通知到应用
- 应用调用Runtime 进行模块更新(apply方法)
- 同步完成模块热更新 |
| Runtime层面 | Runtime提供两个Api:
check
和apply
1.check
:发起Http请求获取更新描述文件manifest,如果请求成功了,那么会按照manifest逐个下载更新的Chunk文件并转入ready状态; apply
:在ready
状态时,会将下载的Chunk中逐个模块进行替换,替换逻辑由Module层面定义的逻辑决定,如果当前模块不能够完成替换,那么会逐级往上冒泡。 | | 编译器层面 | 当文件发生变更时,会触发compiler编译并提交一个更新,更新包括两方面内容:1. manifest描述文件(JSON形式)- 更新Chunk,包含本次更新的所有模块compiler会将变更信息存储在内存中(webpack-dev-server),也可能会存储到文件中。如果使用webpack-dev-server,那么会通过WebSocket主动向浏览器发起通知文件变更。 |
官方文档从多个角度描述了HMR的工作原理,窥一斑而知全豹,根据经验我们可以在脑海中能够清晰地刻画出HMR的设计架构图。能够初步得到下面架构设计图:
上述HMR架构图能够清晰描绘出每一层级关注的内容,下面逐一讲解每一层级的作用:
Module层
:关注单个模块以及子模块产生变更时具体处理逻辑;HMR Runtime
:从应用层面提供变更获取、代码下载、统一的模块替换应用逻辑等等;Webpack Dev Server
:提供B/S沟通服务器,用于HMR Runtime使用;Compiler
:当文件发生变更时,提供编译功能以及模块变更描述文件;
有了上述的架构图,我们能够对HMR的模块设计以及模块之间的关联关系有了一丝印象,接下来讲解模块原理。
模块原理解析
从源码上来看,HMR
设计上是层成四层,但由于模块处理和编译器处理存在一些关联,所以这里将编译器和模块放一起看待,所以热更能力可以分为三大模块:热更模块编译处理、Webpack Dev Server、HMR Runtime。
热更新模块编译处理
开启热更新后,会往Compiler
注入HotModuleReplacementPlugin
插件,插件内部做了三方面处理:
- 语法转译:模块代码允许使用
module.hot.xxx
进行个性化处理,需要将这些语法处理为符合运行时能力的代码; HMR Runtime Module
:在Webpack5源码解读系列5 - 运行时、代码生成原理 文章中我们了解到编译时Webpack会按需将使用到的运行时能力注入,HMR模式下也不例外会将HMR Runtime模块进行注入操作。- 产物生成:
HMR
模式下每次变更都会产生manifest和update chunks文件,编译器需要额外处理这些产生生成逻辑。
语法转义
模块可以通过使用module.hot.xxx
表达式使用热更api,如下面例子:
HotModuleReplacementPlugin
提供了语法转义能力,主要替换语法有module.hot
、module.hot.accept
、module.hot.decline
语法:
转译前:
转译后:
Runtime Module注入
HMR
在运行时需要相应基础能力才能够运行,在HotModuleReplacementPlugin
内部会往编译器注入HotModuleReplacementRuntimeModule
保证应用能够正常提供运行时能力。
manifest及chunk生成
Webpack
在设计上通过Tapable
实现事件机制,除了核心流程之外,各种新增功能均以插件形式,通过事件注册完成解耦。HotModuleReplacementPlugin
往Compiler
注册事件实现热更能力。
Compiler
每次编译之后都会得到每个模块、Chunk
的哈希值,Compiler
通过判断本次编译的产物的哈希和上次编译记录的Hash
得出模块变更信息,HotModuleReplacementPlugin
根据模块变更信息生成两份数据:
-
更新描述
manifest
:以JSON形式数据:updateChunkIds
:更新的Chunk Id
removedChunkIds
:移除的Chunk Id
removedModuleIds
:移除的Module Id
-
模块Chunk产物:只包含更新模块代码
具体计算流程如下:
运行时逻辑
HMR与其他Webpack特性一样,需要提供全局运行时代码才可完成模块热更新管理。Webpack提供热更运行时能力管理模块更新通知,并提供了针对JavaScript模块的热更新运行时能力。
HotModuleReplacement.Runtime(HMR Runtime)
热更新运行时(HMR Runtime
)能力内部维护一个状态机负责管理所有热更任务,状态及其含义如下表:
状态 | 含义 |
---|---|
idle | 空闲 |
check | 检查是否存在更新请求阶段 |
prepare | 进入准备更新阶段,开始获取更新信息 |
ready | 获取到所有更新任务,准备执行任务 |
abort | 更新前发生错误时,终止任务 |
dispose | 执行模块卸载逻辑 |
apply | 执行模块加载逻辑 |
fail | 更新任务失败 |
下图是状态机状态运转逻辑,看状态流转图的时候思考一个问题:为什么要使用状态机维护状态流转呢?
如果把状态机换成『生命周期』就能够很好地理解上述问题,总体来说主要有两个原因:
- 注册钩子函数:上层通过api注册每个生命周期的钩子执行逻辑;
- 任务分批次处理:每次ready之后都会获取所有待执行任务,之后产生的更新信息需要等到下一次状态流转时执行;
开启HMR后,每个模块初始化时都会注册一个热更新描述对象(ModuleHotObject),描述当前模块对于本模块或者子模块的热更新处理信息,同时提供主动触发热更新的api,主要属性和方法如下:
每个模块执行的时候会创建Module Hot Object,如果模块内部可以通过module.hot.xxx
访问属性和方法,如下图的处理转换:
JavaScriptHotModuleReplacement.Runtime(JSHMR Runtime)
HMR Runtime
只处理了状态机状态流转以及流转时注册的钩子函数,并没有处理具体的处理逻辑。Webpack基于HMR Runtime
提供了专门处理JavaScript的模块热更新运行时(JSHMR Runtime
),用于处理JavaScript模块发生热更新时的逻辑,主要处理两种场景:
- 当模块被删除时,调用dispose方法消除副作用;
- 当模块更新时,冒泡寻求accept方法执行模块更新逻辑并重新加载模块;
Webpack-Dev-Server
「热更新模块编译处理」和「运行时逻辑」讲解了编译器处理模块热更语法以及模块热更运行时,但是缺乏两者之间的沟通逻辑:
应用在本地开发的时候,一般情况下会启用DevServer作为开发时静态资源服务器并开启LiveReload,LiveReload是当应用文件发生变更后,主动推送到Web应用要求应用刷新页面。相比于LiveReload,HMR提供了文件级别的刷新能力,它们都是基于DevServer能力开发而成。
DevServer作为编译器和Web应用的中间层,当本地文件发生变更时会主动通知Web应用变更信息,并提供静态资源服务器。所以从设计上看DevServer分为两大模块:
-
应用沟通模块
- 当本地文件发生变更时,DevServer主动推送更新消息到浏览器。一般情况下使用WebSocket作为实现技术,同时也提供长轮询方式作为兜底实现。
-
静态资源服务器
- 使用express框架提供静态资源服务器功能。
应用沟通模块需要由服务端(server)、浏览器端(client)约定接口协议,并且在两侧需提供相应能力才可完成对应能力。具体的协议及其含义如下表:
signal | 含义 |
---|---|
hot | 开启热更新 |
liveReload | 开启实时刷新 |
invalid | 使应用失效 |
overlay | 展示提示面板 |
reconnect | 重连 |
progress | 是否展示进度 |
progress-update | 更新进度 |
data | HMR更新的数据 |
整体工作时序图如下:
小结
HMR是一个涉及多个模块的能力,横跨了编译器、Web应用、DevServer模块的能力。上述三个模块分别提供了不同的能力支持:
-
编译器层面:Webpack编译器层面将热更语法进行转义,并按需注入热更运行时代码
-
Web应用层面:
- 通过HMR Runtime提供热更任务处理逻辑
- 通过JSHMR Runtime提供针对JS语法的模块热更替换逻辑
-
DevServer:DevServer作为编译器和Web应用的中间层,提供了双向沟通能力以及本地静态资源服务能力
最后给出了HMR所涉及到的模块能力RoadMap方便大家记忆~
转载自:https://juejin.cn/post/7231809738203021370