Appearance
Vite 热模块替换(HMR)
热模块替换(Hot Module Replacement,HMR)是 Vite 的核心功能之一,它允许在不刷新整个页面的情况下更新模块,从而保持应用状态并提供快速的开发反馈。
HMR 基础概念
什么是 HMR
HMR 允许在运行时更新模块,而无需完全刷新页面。这使得开发者可以在保持应用当前状态的同时看到代码更改的效果。
HMR 与传统刷新的区别
javascript
// 传统方式 - 页面完全刷新
// 修改代码 → 重新加载页面 → 丢失应用状态
// HMR 方式 - 模块热替换
// 修改代码 → 更新模块 → 保持应用状态
HMR API
基本 HMR API
在开发环境中,可以通过 i 访问 HMR API:
javascript
// 检查 HMR 是否可用
if (import.meta.hot) {
// HMR 逻辑
}
接受自身更新
javascript
// 接受自身模块的更新
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
// 当模块自身更新时执行的回调
console.log('模块已更新:', newModule)
// 可以在这里更新应用状态
if (newModule && newModule.render) {
newModule.render()
}
})
}
接受依赖更新
javascript
// 接受特定依赖的更新
import { count } from './counter.js'
if (import.meta.hot) {
import.meta.hot.accept('./counter.js', (newCounter) => {
// 当 counter.js 更新时执行
console.log('Counter 模块已更新')
// 更新相关的 UI 或状态
updateCounterDisplay(newCounter.count)
})
}
拒绝更新
javascript
// 拒绝更新(不接受父模块的更新)
if (import.meta.hot) {
import.meta.hot.decline() // 模块不再接受更新
}
模块卸载处理
javascript
// 模块被卸载时的清理工作
if (import.meta.hot) {
import.meta.hot.dispose(() => {
// 清理定时器、事件监听器等
clearInterval(timer)
document.removeEventListener('click', handleClick)
})
}
框架集成
Vue 中的 HMR
在 Vue 项目中,HMR 是自动处理的:
vue
<template>
<div>{{ message }}</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello Vite!'
}
},
created() {
console.log('组件已创建')
}
}
</script>
Vue 组件会自动更新而无需手动 HMR 配置。
React 中的 HMR
React 项目使用 Fast Refresh:
jsx
import React, { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
)
}
export default Counter
普通 JavaScript 中的 HMR
对于普通 JavaScript 模块,需要手动实现 HMR:
javascript
// counter.js
let count = 0
function increment() {
count++
updateDisplay()
}
function updateDisplay() {
const element = document.getElementById('counter')
if (element) {
element.textContent = `Count: ${count}`
}
}
// 导出函数供外部使用
export { increment, count as getCount }
// HMR 处理
if (import.meta.hot) {
// 接受更新并保持状态
import.meta.hot.accept((newModule) => {
// 更新导出的函数引用
increment = newModule.increment
count = newModule.getCount
updateDisplay()
})
// 模块卸载时的清理
import.meta.hot.dispose(() => {
console.log('Counter 模块被卸载')
})
}
高级 HMR 用法
验证更新
javascript
// 验证更新是否兼容
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
if (!newModule.isVersionSupported) {
// 如果不兼容,触发完整页面重载
import.meta.hot.invalidate()
} else {
// 执行更新逻辑
applyUpdates(newModule)
}
})
}
自定义更新处理
javascript
// 自定义更新处理逻辑
if (import.meta.hot) {
import.meta.hot.accept(['./dependency1.js', './dependency2.js'],
([newDep1, newDep2]) => {
// 同时处理多个依赖的更新
if (newDep1) {
updateDependency1(newDep1)
}
if (newDep2) {
updateDependency2(newDep2)
}
}
)
}
错误处理
javascript
// 处理 HMR 过程中的错误
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
try {
applyUpdates(newModule)
} catch (error) {
console.error('HMR 更新失败:', error)
// 可以选择重新加载页面
location.reload()
}
})
}
HMR 配置
服务器配置
javascript
// vite.config.js
export default {
server: {
hmr: {
// 启用/禁用 HMR
overlay: true, // 显示编译错误
protocol: 'ws', // HMR 连接协议
host: 'localhost', // HMR 连接主机
port: 24678, // HMR 连接端口
clientPort: 5173 // 客户端连接端口
}
}
}
禁用 HMR
javascript
// vite.config.js
export default {
server: {
hmr: false // 完全禁用 HMR
}
}
实际应用示例
状态管理中的 HMR
javascript
// store.js
let state = {
count: 0,
name: 'Vite App'
}
function setState(newState) {
state = { ...state, ...newState }
notifySubscribers()
}
const subscribers = []
function subscribe(fn) {
subscribers.push(fn)
return () => {
const index = subscribers.indexOf(fn)
if (index > -1) {
subscribers.splice(index, 1)
}
}
}
function notifySubscribers() {
subscribers.forEach(fn => fn(state))
}
// HMR 处理
if (import.meta.hot) {
// 保持状态
if (import.meta.hot.data?.state) {
state = import.meta.hot.data.state
}
// 模块卸载时保存状态
import.meta.hot.dispose(data => {
data.state = state
})
// 接受更新
import.meta.hot.accept(newModule => {
// 更新函数引用
setState = newModule.setState
notifySubscribers = newModule.notifySubscribers
})
}
export { state, setState, subscribe }
组件库中的 HMR
javascript
// component.js
class MyComponent {
constructor(element, options = {}) {
this.element = element
this.options = options
this.init()
}
init() {
this.render()
this.bindEvents()
}
render() {
this.element.innerHTML = `
<div class="my-component">
<h3>${this.options.title || 'Default Title'}</h3>
<p>${this.options.content || 'Default content'}</p>
</div>
`
}
bindEvents() {
this.handleClick = this.handleClick.bind(this)
this.element.addEventListener('click', this.handleClick)
}
handleClick() {
console.log('Component clicked')
}
destroy() {
this.element.removeEventListener('click', this.handleClick)
}
}
// HMR 处理
if (import.meta.hot) {
// 保存现有实例
if (import.meta.hot.data.instances) {
// 重新初始化所有实例以应用更新
import.meta.hot.data.instances.forEach(instance => {
instance.destroy()
instance.init()
})
}
import.meta.hot.dispose(data => {
// 保存实例以便下次使用
data.instances = window.__MY_COMPONENTS__ || []
})
import.meta.hot.accept()
}
// 导出构造函数
export default MyComponent
HMR 最佳实践
1. 状态持久化
javascript
// 保持模块状态
if (import.meta.hot) {
// 从之前的模块实例恢复状态
if (import.meta.hot.data.previousState) {
state = import.meta.hot.data.previousState
}
// 保存状态供下次使用
import.meta.hot.dispose(data => {
data.previousState = state
})
}
2. 资源清理
javascript
// 确保清理资源
if (import.meta.hot) {
import.meta.hot.dispose(() => {
// 清理定时器
if (timer) clearInterval(timer)
// 清理事件监听器
window.removeEventListener('resize', handleResize)
// 清理 WebSocket 连接
if (socket) socket.close()
})
}
3. 错误边界
javascript
// 实现错误边界
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
try {
// 应用更新
newModule.render()
} catch (error) {
console.error('HMR 更新失败:', error)
// 回退到安全状态或重新加载
}
})
}
常见问题
HMR 不工作
- 确保在开发模式下运行
- 检查是否有语法错误
- 确认模块正确导出
状态丢失
javascript
// 正确保存状态
if (import.meta.hot) {
if (import.meta.hot.data.savedState) {
// 恢复之前的状态
restoreState(import.meta.hot.data.savedState)
}
import.meta.hot.dispose(data => {
// 保存当前状态
data.savedState = getCurrentState()
})
}
多次绑定事件
javascript
// 避免重复绑定事件监听器
if (import.meta.hot) {
import.meta.hot.dispose(() => {
// 清理之前绑定的事件
element.removeEventListener('click', boundHandler)
})
// 每次更新时重新绑定
element.addEventListener('click', boundHandler)
}
通过合理使用 HMR,您可以极大地提升开发体验,实现快速反馈并保持应用状态,让开发过程更加高效。