调试工具
首先利用 Chrome 的 dev-tools 中的 network 观察,代码改变的时候,页面与后端之间发生了什么?示例中的项目代码地址已上传 Github。
页面初始加载
运行 dev 模式,本地打开页面并开启 dev-tools,我们看到除了加载页面所依赖的文件外,多了一个连接__webpack_hmr,这是一个叫做 Server-sent Events 的长连接,主要用于后端不断的向前端推送数据,其原理图如下所示,相关的介绍可以参考这篇文章。
后端每次推送的数据内容主要在 EventStream 中的 Data 字段中,如下图所示:
Data 中存储的是一个对象,对象有 action、hash、modules 等字段。
action:sync 操作;
hash:0c0d327c2abaa1fd4b88,是 bundle 的 hash,因为和产出文件 app.0c0d327c2abaa1fd4b88.js 的内容 hash 值相同;
modules:产出 bundle 中的 module id 和对应的文件地址。
修改代码
修改一处代码,webpack 自动编译后,发现 network 中发生了几处变化,首先是客户端收到后端发出的若干事件。
action:built 操作,通知浏览器 webpack 完成了编译;
hash:最新产出 bundle 的内容 hash 值为215d3b813666fbaea5a3;
modules:bundle 中的模块id 和对应模块的文件地址。
在前端收到 built 事件之后,前端向后端发起了两个请求,请求了 0c0d327c2abaa1fd4b88.hot-update.json 和 0.0c0d327c2abaa1fd4b88.hot-update.js 两个文件,文件的 hash 值正好是未发生修改之前后端发送前端的 bundle hash 值。
json文件的内容:
h:215d3b813666fbaea5a3,bundle 内容的最新 hash 值;
c:"0": true, 表示 bundle id 为 0 的文件被修改了;
js文件的内容:
内容是一个函数,类似 jsonp 的返回形式,也就是页面收到请求后执行了 webpackHotUpdate 函数,对 bundle id 为 0 的文件中的 moudle id 为 11 的模块进行修改。
推测结论
根据上面 network 中的信息,我们可以推测出这个交互过程:
webpack 首次编译时,为前端页面注入后端推送事件监听(event-source)和拉取、更新模块的方法(update-method)的代码,并打包到 bundle 之中;
webpack 进入 watch 模式,在项目代码发生变化的时候重新编译,并将编辑的进展实时通知前端;
将编译产出存放在 dev-server,此处的编译只针对变动的模块,产出应该包含上文中提到的 oldbundlehash.hot-update.json 和 oldbundlehash.hot-update.js 文件;
dev-server 中使用 hot-middleware 中间件向前端发送 built 事件;
前端收到通知后,向后端请求最新的变动文件,请求到的 js 文件通过 script 标签加载后执行(类似 Jsonp),其实就是执行已经预埋到 bundle 中的函数(update-method),从而修改 bundle 文件。
以上过程可以用下图表示:
配置文件
接下来我们从项目的配置文件来验证一下,配置文件主要参考 vue-cli 中的webapck 项目(1.1.2),不同的版本会存在差异。
涉及到 Hot Module Replacement 的地方主要有两处:
1. entry 的配置:
在每个入口 bundle 开头引入了 event-source,即在页面中接收后端发送的事件
2. 插件的配置:
引入 HotModuleReplacementPlugin 插件,将 update-method 的代码打入 bundle
涉及到 Hot Module Replacement 的地方主要有两处:
1. 将 compiler 挂载在 devMiddleware 上:
对编译产出提供静态文件服务
2. 将 compiler 挂载在 hotMiddleware 上:
通知前端 event-source 对象发生了 rebuilt
由配置文件可以基本验证之前通过 network debug 得到的推论,接下来去看一
下官方文档验证一下。
官方文档
官方文档中先是总体介绍了一下 Hot Module Replacement 的基本原理,然后将原理中涉及到几个知识点进行了介绍。
1. 基本原理
webapck 在编译的过程中,将 HMR Runtime 嵌入到 bundle 中;编译结束后,webpack 对项目代码文件进行监视,发现文件变动重新编译变动的模块,同时通知 HMR Runtime,然后 HMR Runtime 加载变动的模块文件,尝试执行热更新操作。更新的逻辑是:先检查模块是否能支持 accept 方法,不支持的话,则冒泡查找模块树的父节点,直到入口模块,accept 方法也就是模块 hot-replace 的 handler。
2. 知识点
(1)compiler
这里的 compiler 也就是指 webapck,主要提供 update 的信息,也就是 update menifest(json 文件格式)和 update chunks(js文件格式);
(2)app
app 也就是指前端页面,app 中的代码主要调用 HMR Runtime 下载最新的模块代码,然后调用 HMR Runtime 执行 update 操作;
(3)HMR Runtime
HMR Runtime 是 webapck 内嵌到前端页面的代码,主要提供来能给个职能 check 和 apply。check 用来下载最新模块代码,runtime 能够接收后端发送的事件和发送请求;apply 用于更新模块,主要将要更新的模块打上 tag,然后调用模块的(也有可能是父模块)的更新 handler 执行更新。
(4)module
HRM是一个可插拔的工具,只能影响包含HMR code的模块。通常情况下,没有必要为每个模块写入HMR code,更新的时候会进行冒泡检查HMR code的是否存在。
根据官方文档的介绍,基本和我们的推论吻合,区别在于官方文档引入了 HMR Runtime 的概念,这个可以看作是推论中的 event-source 和update-method的结合体。
现在大家应该清楚了 webpack的Hot Module Replacement 的基本原理了,官方文档中提到了如何根据最新的模块替换旧模块的方法,这个知识点暂不在本文进行介绍。
如果你对本文有疑问,
或者想和作者进行技术交流,
领取专属 10元无门槛券
私享最新 技术干货