13-Vite热更新
# 0.前言
本节课程代码的仓库 (opens new window):
15-vite-hmr
# 1.概念梳理
# 问题 1:什么是 HMR?
HMR 的全称叫做Hot Module Replacement
,即模块热替换
或者模块热更新
。
通过 HMR
技术主要想在开发阶段实现如下以下两个效果:
- 局部刷新:服务器启动后,修改源代码,以模块为最小更新单位对页面进行无刷更新。
- 状态保存:当改变的模块涉及状态概念时,如
setTimeout
组件,每次更新时需将前一个定时器先进行销毁再创建;再如React
组件,模块更新阶段会对组件进行销毁操作,此时状态信息也会被丢失掉。
# 问题 2:Vite 在实现 HMR 时如何使用的 import.meta?
vite 主要利用的是一个现代浏览器的一个内置对象 import.meta
,在此基础上扩展了一个 hot
属性。
内置的定义如下:
interface ImportMeta {
readonly hot?: {
readonly data: any;
accept(): void;
accept(cb: (mod: any) => void): void;
accept(dep: string, cb: (mod: any) => void): void;
accept(deps: string[], cb: (mods: any[]) => void): void;
prune(cb: () => void): void;
dispose(cb: (data: any) => void): void;
decline(): void;
invalidate(): void;
on(event: string, cb: (...args: any[]) => void): void;
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
重要的有:
import.meta.hot.accept
区分热更新边界,监听模块变化import.meta.hot.data
缓存数据。import.meta.hot.dispose
销毁钩子。
# 2.HMR 实践
# 问题 3:hmr 存在几种更新情况?
自身模块更新
父模块对子模块更新
下面通过一个例子来体验下 vite 中这两种热更新模式。
存在两个模块:
renderClient
负责渲染export const render = () => { const app = document.querySelector<HTMLDivElement>('#app')! app.innerHTML = ` <h1>Hello Vite!</h1> <p target="_blank">This is hmr test</p> ` }
1
2
3
4
5
6
7
8state.js
通过操纵 dom 的方式改变count
export function initState() { let count = 0; setInterval(() => { let countEle = document.getElementById('count'); countEle!.innerText = ++count + ''; }, 1000); }
1
2
3
4
5
6
7页面结构如下:
<body> <div id="app"></div> <p>count: <span id="count">0</span></p> <script type="module" src="/src/main.ts"></script> </body>
1
2
3
4
5
当对 state.js
或是 renderClient.js
文件进行修改时,默认是没有 hmr
效果的,整个页面直接刷新。
此时可以给这两个模块添加 hmr 触发边界。
首先是 renderClient.js
,对于自身 hmr
边界非常简单,可添加如下:
/* 子页面监听监听模块变化 */
if (import.meta.hot) {
/* 回调中的 mod 即为当前模块,此时可调用当前模块暴露的 renderClient 方法 */
import.meta.hot.accept((mod) => mod.renderClient());
}
2
3
4
5
而对于 state.js
文件来说,则比较复杂,由于涉及到定时器和状态,需使用 import.meta.hot.dispose
在每次模块更新时销毁定时器,还需要对状态 count
进行缓存。
// @ts-nocheck
let timer: number | undefined;
if (import.meta.hot) {
// 当监听到当前模块变化时,需要手动清除定时器
import.meta.hot.dispose(() => {
if (timer) {
clearInterval(timer);
}
});
}
/* 子页面监听监听模块变化 */
if (import.meta.hot) {
/* 回调中的 mod 即为当前模块,此时可调用当前模块暴露的 renderClient 方法 */
import.meta.hot.accept((mod) => mod.initState());
}
/* 缓存数据 */
if (!import.meta?.hot.data.count) {
import.meta.hot.data.count = 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
还需倾入性地改造 initState
函数,使用 getAndIncCount
取代 count
如下:
export function initState() {
// 每次 initState 刷新时,需从 import.meta.hot.data 上取数据
const getAndIncCount = () => {
const data = import.meta.hot?.data || {
count: 0,
};
// 注:import.meta.hot.data 的引用地址
data.count = data.count + 1;
return data.count;
};
timer = setInterval(() => {
let countEle = document.getElementById("count");
countEle!.innerText = getAndIncCount() + "";
}, 1000);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
上述可以发现,每次模块更新了都必须要在更新的模块下写下如下代码:
/* 子页面监听监听模块变化 */
if (import.meta.hot) {
/* 回调中的 mod 即为当前模块,此时可调用当前模块暴露的 xxxMethod 方法 */
import.meta.hot.accept((mod) => mod.xxxMethod());
}
2
3
4
5
当模块越来越多时,显然是不合适的,可以使用 import.meta.hot.accept([],()=>{})
第一个参数可以改造为 数组 格式,可通过解构的方式顺序获取对应的模块。
/* 主页面监听 module 变化 */
if (import.meta.hot) {
import.meta.hot.accept(["./render.ts", "./state.ts"], (modules) => {
const [renderModule, stateModule] = modules;
if (renderModule) {
// 触发 render 模块刷新
renderModule.renderClient();
}
if (stateModule) {
// 触发 state 模块刷新
stateModule.initState();
}
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 问题 4:除了 hot.accept/dispose 还有其余的钩子吗?
import.meta.hot.decline()
:模块不可热更新。import.meta.hot.invalidate()
:强制刷新页面。import.meta.hot.on("event", (data)=>{})
:监听自定义事件。如:结合
handleHotUpdate
实现对custom
事件的监听// 插件 Hook handleHotUpdate({ server }) { server.ws.send({ type: 'custom', event: 'custom-update', data: {} }) return [] } // 前端代码 import.meta.hot.on('custom-update', (data) => { // 自定义更新逻辑 })
1
2
3
4
5
6
7
8
9
10
11
12
13
# 3. 疑问?
# 问题 5:什么时候需要手动 HMR?
此题待定,在日常开发中很少手动去编写 import.meta.hot.accept()
这部分代码,从代码可读性上以及维护性上讲,都显得比较冗余。
对于 renderCient
的改造可能还稍微轻量些,我想的是可以使用工具webpack
等工具实现自动注入代码,而对于state.ts
这种状态相关的函数,改造倾入性就很强。不仅需要收集改变的状态并挂载到 import.meta.hot.data
上,还需要构造类似 getAndIncCount()
这类辅助函数去修改状态,难度还是很大的,没有头绪。
还有一块像 React
组件的 HMR 更新,平时我们只需要用现成的插件,对于此块部分的改造还需要涉及 React
的理解。好在已有插件可用,真不知道那些人是怎么编出来的。