Vite HRM 简单使用示例(全)Vite HRM 简单使用示例 定义: HMR(Hot Module Replace
定义:
HMR(Hot Module Replacement): 热模块替换,即自动将页面中发生变化的模块替换为新模块,并且不影响其它模块的正常工作;
核心重要能力:
- 局部刷新(边界模块更新)
- 状态保存(不刷新以维持状态数据)
热模块边界
accept
热模块更新的模块被认为是 HRM 边界;
HMR
边界两种场景
accept
模块自身的热更新信息accept
直接依赖项的热更新信息
- hot.accept()
示例 1:
accept
模块自身的热更新信息
DEMO:
// mod1.js
export const renderHmrMod1 = () => {
const modHmr = document.querySelector('#mod-hmr')
modHmr.innerHTML = `
<h1>这是一个测试 Vite-HMR 的 demo</h1>
<p target="_blank">巴拉巴拉小魔仙-3</p>
`
}
// 1.accept 自身模块时
if (import.meta.hot) {
// 通过 accep t方法注册当前模块(模块自身为HMR边界)热更新时的回调函数,开发者每次保存对该模块文件(render.js)的修改时,所注册的回调函数自动执行
import.meta.hot.accept(newModule => newModule?.renderHmrMod1())
}
// main.js
import { renderHmrMod1 } from './mod1.js'
renderHmrMod1()
// index.html
<div id="mod-hmr"></div>
<script type="module" src="./main.js"></script>
效果:
当 mod1.js
模块发生变化后,保存之后,页面不会刷新,之后会执行 newModule.renderHmrMod1()
方法, 局部更新 Dom #mod-hmr
内容;
- hot.accept(deps, cb)
示例 2:
accept
直接依赖项的热更新信息
DEMO:
// mod2/bar.js
export const renderHmrModBar = () => {
const modHmr = document.querySelector('#mod-hmr')
modHmr.innerHTML = `
<h1>这是一个测试 Vite-HMR 的 demo</h1>
<p target="_blank">巴拉巴拉小魔仙-bar123</p>
`
}
export default {
renderHmrModBar
}
// mod2/foo.js
export const getCountFoo = () => {
let count = 11
console.log('count -> ', count)
return count
}
export default {
getCountFoo
}
// mod2/index.js
import { renderHmrModBar } from "./bar.js";
import { getCountFoo } from './foo.js'
renderHmrModBar()
getCountFoo()
// // 2. accept 单个模块时
// if (import.meta.hot) {
// import.meta.hot.accept('./bar.js', newBar => newBar?.renderHmrModBar())
// }
// 或
// if (import.meta.hot) {
// import.meta.hot.accept(['./bar.js'], ([newBar]) => newBar?.renderHmrModBar())
// }
// 3. accept 多个模块时
if (import.meta.hot) {
import.meta.hot.accept(['./bar.js', './foo.js'], ([newBar, newFoo]) => {
// 只有当所更新的模块非空时,回调函数接收一个数组
// 如果更新不成功(例如语法错误),则该数组为空
newBar?.renderHmrModBar()
newFoo?.getCountFoo()
})
}
// main.js
import './mod2/index.js'
// index.html
<div id="mod-hmr"></div>
<script type="module" src="./main.js"></script>
效果:
当 'mod2/bar.js' 和 'mod2/foo.js' 模块发生变化后, 同样不会触发页面 reload
, 而是会执行 newBar
和 newFoo
模块暴露出来的执行函数,做重新执行操作;从而达到热更新的效果;
值得注意的是,当 accept
接收的模块没有更新时,newModule
是 undefined
,所以一定要注意 newModule
为 undefined
时的判断;建议写法 newModule?.<XXX>()
;
HMR
的其它API
- hot.dispose(cb)
一个接收自身的模块或一个期望被其他模块接收的模块可以使用 hot.dispose
来清除任何由其更新副本产生的持久副作用:
值得注意的点,hot.dispose
使用的位置的定义不是 HMR边界,是被接收的模块内:
// 被 accept 的模块内 或 accept自身的模块
function setupSideEffect() {}
setupSideEffect()
if (import.meta.hot) {
import.meta.hot.dispose((data) => {
// 清理副作用
})
}
DEMO:
// mod2/foo.js
let timer = null
export const getCountFoo = () => {
let count = 0
console.log('timer 1 -> ')
timer = setInterval(() => {
console.log('count -> ', count++)
}, 1000)
return count
}
export default {
getCountFoo
}
if (import.meta.hot) {
// dispose: 接收自身的模块或一个期望被其他模块接收的模块可以使用 hot.dispose 来清除任何由其更新副本产生的持久副作用:
import.meta.hot.dispose(() => {
console.log('module:foo import.meta.host.dispose ->')
clearInterval(timer)
})
// 注册一个回调,当模块在页面上不再被导入时调用。与 hot.dispose 相比,如果源代码更新时自行清理了副作用,你只需要在模块从页面上被删除时,使用此方法进行清理。
import.meta.hot.prune(() => console.log('module:foo import.meta.host.prune -> '))
}
// mod2/bar.js
export const renderHmrModBar = () => {
const modHmr = document.querySelector('#mod-hmr')
modHmr.innerHTML = `
<h1>这是一个测试 Vite-HMR 的 demo</h1>
<p target="_blank">巴拉巴拉小魔仙-bar111</p>
`
}
export default {
renderHmrModBar
}
if (import.meta.hot) {
// dispose: 接收自身的模块或一个期望被其他模块接收的模块可以使用 hot.dispose 来清除任何由其更新副本产生的持久副作用:
import.meta.hot.dispose(() => console.log('module:bar import.meta.host.dispose ->'))
}
// mod2/index.js
import { renderHmrModBar } from "./bar.js";
import { getCountFoo } from './foo.js'
renderHmrModBar()
getCountFoo()
// // 2. accept 单个模块时
// if (import.meta.hot) {
// import.meta.hot.accept('./bar.js', newBar => newBar?.renderHmrModBar())
// }
// 或
// if (import.meta.hot) {
// import.meta.hot.accept(['./bar.js'], ([newBar]) => newBar?.renderHmrModBar())
// }
// 3. accept 多个模块时
if (import.meta.hot) {
import.meta.hot.accept(['./bar.js', './foo.js'], ([newBar, newFoo]) => {
// 只有当所更新的模块非空时,回调函数接收一个数组
// 如果更新不成功(例如语法错误),则该数组为空
newBar?.renderHmrModBar()
newFoo?.getCountFoo()
})
}
当 mod2/foo.js
发生更新之后,会先执行 mod2/foo.js
中 hot.dispose(cb)
, 然后再触发 mod2/index.js
中的模块热更新;
当 修改 console.log('timer 1 -> ')
为 console.log('timer 2 -> ')
count 计数会被清除,然后重新计算开始;
当 mod2/bar.js
发生更新之后,会先执行 mod2/bar.js
中 hot.dispose(cb)
, 然后再触发 mod2/bar.js
中的模块热更新;
[NOTE]:
模块被hot.accept
之后,在被accept
的模块中设置import.meta.hot.dispose(cb)
,hot.dispose
才会生效 。
- hot.prune(cb)
注册一个回调,当模块在页面上不再被导入时调用。与 hot.dispose
相比,如果源代码更新时自行清理了副作用,你只需要在模块从页面上被删除时,使用此方法进行清理.
DEMO:
// mod2/index.js
import './baz.js'
// 3. accept 多个模块时
if (import.meta.hot) {
import.meta.hot.accept(() => {
console.log('mod2/index.js update --> ')
})
import.meta.hot.prune(() => {
console.log('import.meta.hot.prune -> ', import.meta.hot.prune)
})
}
// mod2/baz.js
console.log('module:baz —> ')
if (import.meta.hot) {
// 注册一个回调,当模块在页面上不再被导入时调用。与 hot.dispose 相比,如果源代码更新时自行清理了副作用,你只需要在模块从页面上被删除时,使用此方法进行清理
import.meta.hot.prune(() => {
console.log('import.meta.hot.prune -> ', import.meta.hot.prune)
})
}
[NOTE]:
- 当导入
baz.js
的模块;设置了热更新(import.meta.hot.accept()
)import.meta.hot.prune(cb)
才会触发;否则会执行页面reload
。 - 在
baz.js
也就是被导入的模块中,使用import.meta.hot.prune(cb)
。
- hot.data
import.meta.hot.data
对象在同一个更新模块的不同实例之间持久化。它可以用于将信息从模块的前一个版本传递到下一个版本。
DEMO:
// mod2/count.js
// 首先从上一个实例拿 count 变量,没有的话就是 1
export let count = import.meta.hot.data?.getCount?.() || 1
let maxCount = 100
let timer = setInterval(() => {
console.log('module:count.js count -> ', count++)
if (count > 100) clearInterval(timer)
}, 1000)
if (import.meta.hot) {
// 将获取上一个实例 count 的变量封装成一个函数
import.meta.hot.data.getCount = () => {
console.log('-> import.meta.hot.data.getCount 被调用了! ')
return count
}
import.meta.hot.dispose(() => {
console.log('-> module:count.js import.meta.hot.dispose')
// 清理副作用
clearInterval(timer)
})
// 注册一个回调,当模块在页面上不再被导入时调用。与 hot.dispose 相比,如果源代码更新时自行清理了副作用,你只需要在模块从页面上被删除时,使用此方法进行清理。
import.meta.hot.prune(() => {
console.log('-> module:count.js import.meta.hot.prune')
})
}
// mod2/index.js
import './count.js'
// accept count.js 模块
import.meta.hot.accept('./count.js', newCount => {
console.log('-> module:count.js 更新了!')
})
会发现,当 count.js
发生变化后,会在全局 import.meta.hot.data
中定义了个新方法getCount
里边存储量 count
的最新值;
import.meta.hot.dispose(cb)
之后,重新执行这个这个模块,获取上一次模块执行后的 count
值来进行使用;
- hot.deline()
目前是一个空操作并暂留用于向后兼容。若有新的用途设计可能在未来会发生变更。
要指明某模块是不可热更新的,请使用 hot.invalidate()
。
- hot.invalidate(message?: string)
官网的描述比较难理解,我简单描述下这个 api 的意思:
- 模块作为被导入的模块,
accept
这个模块,根据新模块提供的条件,判断是否要重新载入刷新页面; - 注意的一个点,本模块作为 HMR 边界时,
import.meta.hot.invalidate()
时,是不起作用的; - 建议在
import.meta.hot.accept
之后使用import.meta.hot.invalidate()
示例:
import.meta.hot.accept((module) => {
// 你可以使用新的模块实例来决定是否使其失效。
if (cannotHandleUpdate(module)) {
import.meta.hot.invalidate()
}
})
DEMO:
// mod2/index.js
import './count2.js'
// import.meta.hot
if (import.meta.hot) {
import.meta.hot.accept('xx', () => {
console.log('mod2/index.js update --> ')
})
import.meta.hot.accept('./count2.js', count2Mod => {
console.log('count2Mod -> ', count2Mod)
if (count2Mod.count > 10) {
import.meta.hot.invalidate('-> import.meta.hot.invalidate')
}
console.log('-> module:count2.js 更新了!')
})
}
// mod2/count2.js
export let count = import.meta.hot.data?.getCount?.() || 1;
const timer = setInterval(() => {
console.log('count -> ', count++)
}, 1000)
if (import.meta.hot) {
import.meta.hot.accept(count2Mod => {
console.log('-> module:count2.js 更新了!')
console.log('count2Mod -> ', count2Mod)
// invalidate 在本模块中使用,自己不能让自己失效,当 count>10 时,再改变这个模块,会继续走 hot update
// if (count2Mod.count > 10) {
// import.meta.hot.invalidate('-> import.meta.hot.invalidate ')
// }
})
import.meta.hot.data.getCount = () => {
console.log('module:count2.js import.meta.hot.data.getCount() 被调用了 -> ')
return count
}
import.meta.hot.dispose(() => {
console.log('module:count2.js import.meta.hot.dispose -> ')
// 清理副作用
clearInterval(timer)
})
import.meta.hot.prune(() => {
console.log('-> module:count2.js import.meta.hot.prune')
})
}
运行 DEMO 可以看到,
当 count <= 10 时,此时改变 count2.js
这个模块,会走热更新,如图:
当 count > 10 时,再改变 count2.js
这个模块,页面会 reload()
;
- hot.on(event, cb)
监听自定义 HMR 事件。
以下 HMR 事件由 Vite 自动触发:
'vite:beforeUpdate'
当更新即将被应用时(例如,一个模块将被替换)'vite:afterUpdate'
当更新已经被应用时(例如,一个模块已被替换)'vite:beforeFullReload'
当完整的重载即将发生时'vite:beforePrune'
当不再需要的模块即将被剔除时'vite:invalidate'
当使用import.meta.hot.invalidate()
使一个模块失效时'vite:error'
当发生错误时(例如,语法错误)
上面都 vite 自带的 HMR 事件,由 Vite 自动触发;
DEMO:
// mod2/index.js
import './test-on.js'
if (import.meta.hot) {
import.meta.hot.accept(() => {
console.log('mod2/index.js update --> ')
})
}
// mod2/test-on.js
export const log = (msg = 'update') => console.log('module:testOn.js -> ' + msg)
export default {
log
}
if (import.meta.hot) {
import.meta.hot.accept((mod) => mod.log())
import.meta.hot.on('vite:beforeUpdate', () => log('vite:beforeUpdate'))
import.meta.hot.on('vite:afterUpdate', () => log('vite:afterUpdate'))
import.meta.hot.on('vite:beforeFullReload', () => log('vite:beforeFullReload'))
import.meta.hot.on('vite:beforePrune', () => log('vite:beforePrune')) // 模块作为外部依赖,当外部去掉此模块的导入时,保存时执行
import.meta.hot.on('vite:invalidate', () => log('vite:invalidate'))
import.meta.hot.on('vite:error', () => log('vite:error'))
}
自定义监听事件:
DEMO:
// custom-vite-plugin-hot-event.js : 一个自己编写vite 插件
export default (options) => {
return {
handleHotUpdate (ctx) {
// console.log('ctx -> ', ctx)
const server = ctx.server
server.ws.send({
type: 'custom',
event: 'custom-hot-plugin-test', // 定义了一个自定义监听事件的名字
data: { // 返回给监听事件的数据
msg: 'hello vite'
}
})
return []
}
}
}
// vite.config.js
import { defineConfig } from "vite";
import customVitePluginHotEvent from './plugins/custom-vite-plugin-hot-event.js'
export default defineConfig(() => {
return {
plugins: [
customVitePluginHotEvent() // 自定义热更新插件
]
}
})
// mod2/index.js
import './custom-hot-test.js'
// mod2/custom-hot-test.js
export const log = () => console.log('module:custom-hot-test 2 3 5 6 ->')
if (import.meta.hot) {
// 监听自定义的 hot hmr 插件注册的事件
import.meta.hot.on('custom-hot-plugin-test', (data) => {
console.log('data -> ', data)
})
}
当 mod2/custom-hot-test.js
发生变化后,会触发自定义的热更新事件custom-hot-plugin-test
, 如图:
- hot.send(event, data)
发送自定义事件到 Vite 开发服务器。
如果在连接前调用,数据会先被缓存、等到连接建立好后再发送。
查看官网 客户端与服务器的数据交互 一节获取更多细节。
DEMO:
// client side
if (import.meta.hot) {
import.meta.hot.send('my:from-client', { msg: 'Hey!' })
}
// vite.config.js
export default defineConfig({
plugins: [
{
// ...
configureServer(server) {
server.ws.on('my:from-client', (data, client) => {
console.log('Message from client:', data.msg) // Hey!
// reply only to the client (if needed)
client.send('my:ack', { msg: 'Hi! I got your message!' })
})
},
},
],
})
转载自:https://juejin.cn/post/7235966082032140345