// HMR interface
if (module.hot) {
// Capture hot update
module.hot.accept(“./component”, () => {
const nextComponent = component();
// Replace old content with the hot loaded one
document.body.replaceChild(nextComponent, demoComponent);
demoComponent = nextComponent;
});
}
模块代码的替换逻辑可能非常复杂,幸运的是我们通常不太需要对此过多关注,因为业界许多 Webpack Loader 已经提供了针对不同资源的 HMR 功能,例如:
-
style-loader
内置 Css 模块热更 -
vue-loader
内置 Vue 模块热更 -
react-hot-reload
内置 React 模块热更接口
因此,站在使用的角度,只需要针对不同资源配置对应支持 HMR 的 Loader 即可,很容易上手。
二、实现原理
======
Webpack HMR 特性的原理并不复杂,核心流程:
-
使用
webpack-dev-server
(后面简称 WDS)托管静态资源,同时以 Runtime 方式注入 HMR 客户端代码 -
浏览器加载页面后,与 WDS 建立 WebSocket 连接
-
Webpack 监听到文件变化后,增量构建发生变更的模块,并通过 WebSocket 发送
hash
事件 -
浏览器接收到
hash
事件后,请求manifest
资源文件,确认增量变更范围 -
浏览器加载发生变更的增量模块
-
Webpack 运行时触发变更模块的
module.hot.accept
回调,执行代码变更逻辑 -
done
接下来我会展开 HMR 的核心源码,详细讲解 Webpack 5 中 Hot Module Replacement 原理的关键部分,内容略微晦涩,不感兴趣的同学可以直接跳到下一章。
2.1 注入 HMR 客户端运行时
执行 npx webpack serve
命令后,WDS 调用 HotModuleReplacementPlugin
插件向应用的主 Chunk 注入一系列 HMR Runtime,包括:
-
用于建立 WebSocket 连接,处理
hash
等消息的运行时代码 -
用于加载热更新资源的
RuntimeGlobals.hmrDownloadManifest
与RuntimeGlobals.hmrDownloadUpdateHandlers
接口 -
用于处理模块更新策略的
module.hot.accept
接口 -
等等
关于 Webpack Runti me,可参考 Webpack 原理系列六:彻底理解 Webpack 运行时 。
经过 HotModuleReplacementPlugin
处理后,构建产物中即包含了所有运行 HMR 所需的客户端运行时与接口。这些 HMR 运行时会在浏览器执行一套基于 WebSocket 消息的时序框架,如图:
2.2 增量构建
除注入客户端代码外,HotModuleReplacementPlugin
插件还会借助 Webpack 的 watch
能力,在代码文件发生变化后执行增量构建,生成:
-
manifest
文件:JSON 格式文件,包含所有发生变更的模块列表,命名为[hash].hot-update.json
-
模块变更文件:js 格式,包含编译后的模块代码,命名为
[hash].hot-update.js
增量构建完毕后,Webpack 将触发 compilation.hooks.done
钩子,并传递本次构建的统计信息对象 stats
。WDS 则监听 done
钩子,在回调中通过 WebSocket 发送模块更新消息:
{“type”:“hash”,“data”:“${stats.hash}”}
实际效果:
2.3 加载更新
客户端接受到 hash
消息后,首先发出 manifest
请求获取本轮热更新涉及的 chunk,如:
注意,在 Webpack 4 及之前,热更新文件以模块为单位,即所有发生变化的模块都会生成对应的热 更新文件; Webpack 5 之后热更新文件以 chunk 为单位,如上例中,
main
chunk 下任意文件的变化都只会生成main.[hash].hot-update.js
更新文件。
manifest
请求完成后,客户端 HMR 运行时开始下载发生变化的 chunk 文件,将最新模块代码加载到本地。
2.4module.hot.accept
回调
经过上述步骤,浏览器加载完最新模块代码后,HMR 运行时会继续触发 module.hot.accept
回调,将最新代码替换到运行环境中。
module.hot.accept
是 HMR 运行时暴露给用户代码的重要接口之一,它在 Webpack HMR 体系中开了一个口子,让用户能够自定义模块热替换的逻辑。module.hot.accept
接口签名如下:
module.hot.accept(path?: string, callback?: function);
它接受两个参数:
-
path
:指定需要拦截变更行为的模块路径 -
callback
:模块更新后,将最新模块代码应用到运行环境的函数
例如,对于如下代码:
// src/bar.js
export const bar = ‘bar’
// src/index.js
import { bar } from ‘./bar’;
const node = document.createElement(‘div’)
node.innerText = bar;
document.body.appendChild(node)
module.hot.accept(‘./bar.js’, function () {
node.innerText = bar;
})
示例中,module.hot.accept
函数监听 ./bar.js
模块的变更事件,一旦代码发生变动就触发回调,将 ./bar.js
导出的值应用到页面上,从而实现热更新效果。
module.hot.accept
的作用并不复杂,但使用过程中还是有一些值得注意的点,下面细讲。
2.4.1 失败兜底
module.hot.accept
函数只接受具体路径的 path
参数,也就是说我们无法通过 glob
或类似风格的方式批量注册热更新回调。
一旦某个模块没有注册对应的 module.hot.accept
函数后,HMR 运行时会执行兜底策略,通常是刷新页面,确保页面上运行的始终是最新的代码。
2.4.2 更新事件冒泡
在 Webpack HMR 框架中,module.hot.accept
函数只能捕获当前模块对应子孙模块的更新事件,例如对于下面的模块依赖树:
示例中,更新事件会沿着模块依赖树自底向上逐级传递,从 foo
到 index
,从 bar-1
到 bar
再到 index
,但不支持反向或跨子树传递,也就是说:
-
在
foo.js
中无法捕获bar.js
及其子模块的变更事件 -
在
bar-1.js
中无法捕获bar.js
的变更事件
这一特性与 DOM 事件规范中的冒泡过程极为相似,使用时如果摸不准模块的依赖关系,建议直接在应用的入口文件中编写热更新函数。
2.4.3 无参数调用
除上述调用方式外,module.hot.accept
函数还支持无参数调用风格,作用是捕获当前文件的变更事件,并从模块第一行开始重新运行该模块的代码,例如:
// src/bar.js
console.log(‘bar’);
module.hot.accept();
示例模块发生变动之后,会从头开始重复执行 console.log
语句。
2.5 小结
最后前端到底应该怎么学才好?
如果你打算靠自己摸索自学,那么你首先要了解学习前端的基本大纲,这是你将要学习的主要内容,理解以及掌握好这些内容,便可以找到一份初级的前端开发工作。你还需要有一套完整的前端学习教程,作为初学者最好的方式就是看视频教程学习,初学者容易理解接受。
不要选择买书学习,这样的方式没有几个人能学会,基本都是看不下去书,也看不懂书。如果喜欢看书的学弟,可以买一些经典的书籍作为辅助即可,主要还是以看教程为主。每天抽出固定几个小时学习,做好长期学习的准备。学习编程并不是每天光看视频,你学习编程最重要的目的是为了编写软件产品,提供给大众使用,所以用手写出代码实现功能才是我们要做的事情。