03-webpack热更新原理

  • 课时 3: 热更新

    • 热启动

    • 热更新原理

webpack热启动原理

热启动配置

在webpack的dev模式下,devserver这样配置就可以启动热更新。

const webpack = require('webpack');
devServer: {
    hot: true,
    open: false,
    port: 8080,
},
plugins: [
    new webpack.HotModuleReplacementPlugin(),
],

注意,必须有 webpack.HotModuleReplacementPlugin 才能完全启用 HMR。如果 webpackwebpack-dev-server 是通过 --hot 选项启动的,那么这个插件会被自动添加,所以你可能不需要把它添加到 webpack.config.js 中.

HMR原理

为什么要用HMR

在 Webpack HMR 功能之前,已经有很多 live reload 的工具或库,比如 live-server,这些库监控文件的变化,然后通知浏览器端刷新页面,那么我们为什么还需要 HMR 呢?

  • live reload 工具并不能够保存应用的状态(states),当刷新页面后,应用之前状态丢失,还是上文中的例子,点击按钮出现弹窗,当浏览器刷新后,弹窗也随即消失,要恢复到之前状态,还需再次点击按钮。而 Webapck HMR 则不会刷新浏览器,而是运行时对模块进行热替换,保证了应用状态不会丢失,提升了开发效率。

  • 在古老的开发流程中,我们可能需要手动运行命令对代码进行打包,并且打包后再手动刷新浏览器页面,而这一系列重复的工作都可以通过 HMR 工作流来自动化完成,让更多的精力投入到业务中,而不是把时间浪费在重复的工作上。

  • HMR 兼容市面上大多前端框架或库,比如React Hot Loader,Vue-loader,能够监听 React 或者 Vue 组件的变化,实时将最新的组件更新到浏览器端。Elm Hot Loader 支持通过 webpack 对 Elm 语言代码进行转译并打包,当然它也实现了 HMR 功能。

放上参考文章的一张HMR过程图解

HMR原理

  1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。

  2. 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。

  3. 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。

  4. 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。

  5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。

  6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。

  7. 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。

  8. 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

以上内容都是摘录于Webpack Hot Module Replacement 的原理解析, 下面我们按照上述步骤,从源码中看webapck HMR的实现过程。

这里我使用的是自己手搭的基于webpack的vue工程,方便以后使用vue开发项目开箱即用,在工程中webpack该有的配置都有, 目录如下,详细工程请到xxx下载。

在webpack dev配置中,配置了devServer的hot模式,由于没有在package.json中的script脚本命令中配置--hot,所有这里手动添加webpack.HotModuleReplacementPlugin()插件。

下面我们对文件进行修改,来看webpackHMR的过程。

第一步:webpack watch 打包到内存中

webpack-dev-middleware 调用 webpack 的 api 对文件系统 watch, 当文件变化时,webpack重新编译文件

在webpack中,dev环境下webpack打包编译的输出结果是内存文件,没有将文件写道output.path目录下,这样的原因是因为节省了代码写和读的开销,在内存中访问代码比在文件中访问代码更快。

说到这里,我们顺带说一下memory-fs,就是这个东西将webpack的FileSystem替换为MemoryFileSystem,让代码输入到内存里面。接上面讲到,watch到文件变化之后编译出的结果context,下一步就是转化文件,

第二步:devServer 通知浏览器端文件发生变化

在通知文件之前,webpack已经通过sockjs建立了于浏览器之间的websocket通信,这块相关内容可以参考webpack-dev-serber/lib/Server.jscreateServer方法。这稍微说下,socket连接创建之前是根据支持协议https/spdy创建的http连接,之后在listen中创建的socket连接。

第三步:Client接收服务端消息做出相应

可能你又会有疑问,我并没有在业务代码里面添加接收 websocket 消息的代码,也没有在 webpack.config.js 中的 entry 属性中添加新的入口文件,那么 bundle.js 中接收 websocket 消息的代码从哪来的呢?原来是 webpack-dev-server 修改了webpack 配置中的 entry 属性,在里面添加了 webpack-dev-client 的代码,这样在最后的 bundle.js 文件中就会有接收 websocket 消息的代码了

在client端,接收到type=hash的消息会将hash暂存起来,当接收到type=ok的消息时,执行接收ok的消息之后执行reloadApp操作。在reloadApp中,如果配置了模块热更新,就调用 webpack/hot/emitter emit一个webpackHotUpdate事件,将最新 hash 值发送给 webpack,然后将控制权交给 webpack 客户端代码。如果没有配置模块热更新,就直接调用 location.reload 方法刷新页面。

这里我们看到一次更新websocket对应发送的内容

websocket

第四步:webpack 接收到最新 hash 值验证并请求模块代码

接下来就是webpack的操作了,首先是webpack/hot/dev-server.js中监听上一步中webpack-dev-server/client-src/default/index.js中emit出来的webpackHotUpdate事件,然后触发check方法。

在check过程中,调用挂载在module.hot.checkout方法,会用到webpack/lib/hmr/HotModuleReplacement.runtime.js中的hmrDownloadManifesthmrDownloadUpdateHandlers两个核心方法,这两个方法是被注入到webpack全局变量RuntimeGlobals中的。

hotDownloadManifest方法是调用AJAX向服务端请求是否有更新的文件,如果有将发更新的文件列表返回浏览器端,hmrDownloadUpdateHandlers方法是通过 jsonp 请求最新的模块代码,然后将代码返回给 HMR runtime,HMR runtime 会根据返回的新模块代码做进一步处理,可能是刷新页面,也可能是对模块进行热更新。

由于没有在源码中定位到对应方法的定义,直接在devtools下找的编译后的代码

下面是从服务端获取变化文件

hotDownloadManifest获取的文件变化名称

hmrDownloadUpdateHandlers将上面的文件名称拼接,生成一个script标签,插入到dom中,然后发起请求,获取到对应的js代码块,然后将新的代码块返回给 HMR runtime,进行模块热更新。

还记得 HMR 的工作原理图解 中的问题 3 吗?为什么更新模块的代码不直接在第三步通过 websocket 发送到浏览器端,而是通过 jsonp 来获取呢?我的理解是,功能块的解耦,各个模块各司其职,dev-server/client 只负责消息的传递而不负责新模块的获取,而这些工作应该有 HMR runtime 来完成,HMR runtime 才应该是获取新代码的地方。再就是因为不使用 webpack-dev-server 的前提,使用 webpack-hot-middleware 和 webpack 配合也可以完成模块热更新流程,在使用 webpack-hot-middleware 中有件有意思的事,它没有使用 websocket,而是使用的长轮询。综上所述,HMR 的工作流中,不应该把新模块代码放在 websocket 消息中。

第五步:HotModuleReplacement.runtime 对模块进行热更新

在HotModuleReplacement.runtime 中,我们看到了有这样的一个方法,createModuleHotObject,该方法create之后的赋给了全局的module.hot,并且使用interceptModuleExecution注入到全局(==具体怎么注入的没有细看, 留到后面有时间再回过来看==),在createModuleHotObject方法中我们看到,定义了接下来我们要用到和hot相关的moduleAPI方法。

在这里我们先看几个状态,后面看runtime代码的时候可以方便理解:

status

description

idle

该进程正在等待调用 check(见下文)

check

该进程正在检查以更新

prepare

该进程正在准备更新(例如,下载已更新的模块)

ready

此更新已准备并可用

dispose

该进程正在调用将被替换模块的 dispose 处理函数

apply

该进程正在调用 accept 处理函数,并重新执行自我接受(self-accepted)的模块

abort

更新已中止,但系统仍处于之前的状态

fail

更新已抛出异常,系统状态已被破坏

在上一步,我们使用check方法的时候调用了module.hot.check,在这里我们看到了实际上调用了hotApply方法。我们来看一下hotCheck方法,

下面我们来看下hotAddUpdateChunk方法,

上面的一大堆操作其实就是为了做一件事,拿到代码段,执行hotApply代码。webpack4的hotApply写的替换过程根本没看懂。直接放别人的成果。

从上面 hotApply 方法可以看出,模块热替换主要分三个阶段,第一个阶段是找出 outdatedModules 和 outdatedDependencies,这儿我没有贴这部分代码,有兴趣可以自己阅读源码。第二个阶段从缓存中删除过期的模块和依赖,如下:

delete installedModules[moduleId];

delete outdatedDependencies[moduleId];

第三个阶段是将新的模块添加到 modules 中,当下次调用 webpack_require (webpack 重写的 require 方法)方法的时候,就是获取到了新的模块代码了。

以上就是webpack HMR的主要过程。

附:

webpack5中hotApply的核心方法

参考:

Last updated