Skip to content
On this page

Vite 热模块替换(HMR)

热模块替换(Hot Module Replacement,HMR)是 Vite 的核心功能之一,它允许在不刷新整个页面的情况下更新模块,从而保持应用状态并提供快速的开发反馈。

HMR 基础概念

什么是 HMR

HMR 允许在运行时更新模块,而无需完全刷新页面。这使得开发者可以在保持应用当前状态的同时看到代码更改的效果。

HMR 与传统刷新的区别

javascript
// 传统方式 - 页面完全刷新
// 修改代码 → 重新加载页面 → 丢失应用状态

// HMR 方式 - 模块热替换
// 修改代码 → 更新模块 → 保持应用状态

HMR API

基本 HMR API

在开发环境中,可以通过 import.meta.hot 访问 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 不工作

  1. 确保在开发模式下运行
  2. 检查是否有语法错误
  3. 确认模块正确导出

状态丢失

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,您可以极大地提升开发体验,实现快速反馈并保持应用状态,让开发过程更加高效。