相比直接刷新浏览器,通过 webpack 的 HMR 模式更能对开发效率有显著提升。
试想下:开发时,你对客户端的 js、css 做了小改动,浏览器没有再次向服务端发起请求,页面的修改区域就更新了代码,那多美好。
本篇先从 webpack-dev-server 着手,探索 webpack HMR 更新机制,以及结合 webpack 在服务端的 SSR 构建做一些实践。(篇幅过长,另开一篇说明:开发中如何接入 HRM 到服务端)
webpack-dev-server 自带的 HMR
可以借助 webpack-dev-server 来开启一个服务,它具备代理、静态文件等功能,当然还有本篇的重点功能 ——
Hot Module Reload(HMR)
零障碍开启 HMR
我已经参照 webpack 官网的 Guide 写了个 Demo :
点击访问 https://gitee.com/eminoda/ssr-learn/tree/webpack-hmr-practice
webpack-dev-server 如何实现 HMR
精力有限,webpack 相关的不做涉及,围绕 webpack-dev-server 对 HMR 的实现做说明。
server 服务的创建
webpack-dev-server 提供一个服务功能,首先找到 Server 类:
1 | // node_modules\webpack-dev-server\lib\Server.js |
在里面会创建一个 express 服务,在其中开启一个 socket 套接字服务。HMR 就以此为桥梁互相通讯。
socket 怎么通讯?
不清楚 socket 的同学可以看下这个 Demo,体验 socket 怎么桥接服务端和客户端的通讯问题:
注入客户端 HMR 代码
添加 entry 文件
更新 compiler 时,在原有配置上会插入额外的 entry 文件。
1 | // node_modules\webpack-dev-server\lib\utils\updateCompiler.js |
check entry
entry 由 wepback 提供,根据配置的 hot、hotOnly 选项,加载指定的 entry 文件:
1 | // node_modules\webpack-dev-server\lib\utils\addEntries.js |
only-dev-server 和 dev-server 这两者最大的不同,就是在异常情况下,对浏览器是否进行强刷:window.location.reload();
client entry
由 webpack-dev-server 提供,包括 socket 、reloadApp 等代码:
1 | const additionalEntries = checkInject(options.injectClient, config, webTarget) ? [clientEntry] : []; |
最后统统加到我们自己的 wepack 中:
1 | additionalEntries.push(hotEntry); |
HotModuleReplacementPlugin
可以看到原始的 webpack 的 plugins 配置自动添加了 HotModuleReplacementPlugin:
1 | // node_modules\webpack-dev-server\lib\utils\addEntries.js |
通过 HotModuleReplacementPlugin 给客户端添加向服务端请求用来获取 hot-update.json 和 hot-update.js 文件的代码,来实现 HMR 功能。
hot-update.json
1 | // node_modules\webpack\lib\web\JsonpMainTemplate.runtime.js |
hot-update.js
1 | // node_modules\webpack\lib\web\JsonpMainTemplate.runtime.js |
当然这两段预先埋到客户端中的代码何时触发,之后再看。
服务端与客户端的交互
如何通过 socket 通讯
借助封装的 sockWrite 方法,服务端将发送 hot、liveReload、invalid、progress、ok 等关键词以及对应的 data 信息 send 给客户端。
像是这样:{type:’ok’,msg:’foo’}
1 | // node_modules\webpack-dev-server\lib\Server.js |
客户端拿到这些关键词匹配对应的执行逻辑
1 | // node_modules\webpack-dev-server\client\socket.js |
socket 建立后的准备
socket 服务创建连接后,随着 webpack 的构建,会同步向客户端发送 webpack 的构建进度信息
1 | this.socketServer.onConnection((connection, headers) => { |
客户端就会接收到这些信息,并打印输出:
1 | [HMR] Waiting for update signal from WDS... |
监听文件修改
webpack-dev-server 会监听本地文件的修改保存,每当 webpack 编译完成后就发送 socket ,通知客户端重载更新代码,触发 reloadApp 。
1 | // node_modules\webpack-dev-server\lib\Server.js |
1 | this.sockWrite(sockets, "hash", stats.hash); // 关键依据 |
注意,服务端发送 ok 标识后,视线就该转到客户端,因为 reloadApp 的代码在客户端中。
reloadApp()
注意上述的 hash ,每次构建后 webpack 会将最新的 currentHash 通过 socket 告诉客户端,这是 HMR 是否执行的依据。
通知 webpackHotUpdate 事件,并传递 currentHash :
1 | function reloadApp(_ref, _ref2) { |
webpackHotUpdate 收到 currentHash ,调用 check 方法:
1 | // node_modules\webpack\hot\only-dev-server.js |
check()
check 内部会执行 module.hot.check(),根据 upToDate 更新情况判断是否继续 check
1 | // node_modules\webpack\hot\only-dev-server.js |
module.hot.check 是 hotCheck 的赋值变量。代码在开始时已经通过 HotModuleReplacementPlugin 被打包到客户端
1 | // node_modules\webpack\lib\HotModuleReplacement.runtime.js |
hotDownloadManifest 就是生成拉取 hot-update.json 的代码,参照:注入客户端 HMR 代码
获取到的 hot-update.json 就像这样:
1 | {"h":"ba41c82ba5dfb16b4a29","c":{"app":true}} |
这样的文件是通过 webpack 输出在内存中:
1 | // node_modules\webpack\lib\HotModuleReplacementPlugin.js |
之后再执行 hotEnsureUpdateChunk (内部调用 hotDownloadUpdateChunk )向服务端获取对应的 js 的代码,参照:注入客户端 HMR 代码
同样看下这代码如何生成:
在热更新中会执行 HotModuleReplacementPlugin 和 JsonpMainTemplatePlugin
1 | // node_modules\webpack\lib\HotModuleReplacementPlugin.js |
1 | // node_modules\webpack\lib\web\JsonpMainTemplatePlugin.js |
这个 JsonpMainTemplate.runtime 有往页面 Head 中添加标签的逻辑。
最后交给 HotModuleReplacementPlugin 中的 source 输出到客户端代码中,这样就能在 html 看到新增的代码:
1 | <script charset="utf-8" src="app.a924ad7db1acc3cd4b8e.hot-update.js"></script> |
在 Network 中看到这样的请求:
同时完毕后,控制台会有如下打印:
1 | [WDS] App hot update... |
全览整个 HMR 过程(图)
- 设置 hot ,开启 HMR 功能(会对 webpack 的构建配置做修改,添加 entry 和 plugin)
- webpack 监听、构建本地代码
- 将构建结果推送到内存中(client 的请求将从这里获取)
- webpack-dev-server 开启 express 服务,并创建 stock 连接
- 实时对客户端发送 server 端的进度状态
- 如有代码更新,执行 check 进行检查
- 做是否需要更新 client 代码的判断
- 拉取 hot-update.json ,获取更新 chunk,判断 chunk 是否有不同
- 若有差异,拉取 hot-update.js ,热替换 client 代码
参考
我只是知识点的“加工者”, 更多内容请查阅原文链接 :thought_balloon: , 同时感谢原作者的付出: