vite项目 - hmr失效问题排查思路分享

2,165 阅读8分钟

项目由webpack转vite后,hmr一直存在一些问题,即变更文件都会导致页面重新加载。最近解决了此问题,写下此文,简单介绍一下hmr的概念、比较webpack和vite中的hmr共同点和差异,并讲述vite项目中hmr失效的排查思路和解决措施

一. hmr 介绍

热更新(Hot Module Replacement,简称 HMR)是一种前端开发中的技术,允许开发者在应用运行时替换、添加或删除模块,而无需完全刷新页面。这极大地提升了开发效率和调试速度,因为开发者可以即时看到修改效果,而不需要重启整个应用或重载整个页面。

二. vite 中的 hmr 与 webpack 中的 hmr

image.png vite 和 webpack 都是现代前端构建工具,它们都支持热模块替换(HMR)。然而,它们在实现 HMR 的方式上有所不同。 Webpack 和 Vite 都支持热模块替换(HMR),这是一项使得开发者可以在应用运行时更新代码而无需完全刷新页面的技术。尽管两者都提供了这种能力,但它们实现 HMR 的细节和方法有所不同,反映了各自的架构特性和设计理念。

共同点:

  1. 实时更新: 无论是 Webpack 还是 Vite,在检测到文件变化后都能实时更新改变的模块,而不需要重新加载整个页面。
  2. 开发效率: 两者的 HMR 功能都极大地提升了开发效率,使得开发者可以即刻看到代码变更的效果。
  3. 状态管理: 在不同程度上,两个系统都需要开发者在某些情况下手动管理和保持应用状态,以便在模块更新时避免状态丢失。
  4. 服务器与客户端通信:开发服务器和客户端(浏览器)通过 WebSocket 通信。

不同点:

  1. 实现机制:
    • Webpack: 依赖于其复杂的构建系统和 HotModuleReplacementPlugin。Webpack 会重新编译变更的模块以及其依赖链上的所有模块,并通过 WebSocket 将更新推送到客户端。

image.png * Vite: 利用现代浏览器支持的原生 ES 模块动态导入特性。文件更改仅触发所需模块的重新请求,Vite 开发服务器对变更文件作快速转换,然后直接推送至客户端。

  1. 配置复杂度:

    • Webpack: 需要在配置中显式启用 HMR,并可能需要编写更多的样板代码和特定配置,尤其是在复杂的项目中。
    • Vite: 简化了配置流程,HMR 功能默认内置,开箱即用,反映了 Vite 主张的“开发简化”。
  2. 基于构建系统的差异:

    • Webpack: 所有文件都需要经过构建过程,即使是开发环境。
    • Vite: 在开发时不进行打包,仅通过服务器实时提供转换后的代码,利用浏览器的模块缓存提高性能。
  3. accept 代码的差异: Vite 使用 import.meta.hot 来访问热模块替换 API,而 Webpack 使用 module.hot

  • Webpack:

    if (module.hot) {
      module.hot.accept('./editor', () => {
        console.log('Accepting the updated editor module!');
      });
    }
    
  • Vite:

    if (import.meta.hot) {
      import.meta.hot.accept('./editor', () => {
        console.log('Accepting the updated editor module!');
      });
    }
    

三. vite hmr 原理

由于本文重点不是原理解析,而是问题排查。网上已经有很多不错的文章详细介绍vite中hmr的实现,本文主要介绍两篇相关阅读~ 😁 请自行查阅啦

相关阅读1:彻底理解 Vite 的热更新主要流程

image.png

文章总结:

  • (1)当文件没有定义热更新,当文件被修改时,整个页面都重新刷新了。因为 Vite 不知道如何进行热更新,所以只能刷新页面;
  • (2)Vite 本身只提供热更新 API,不提供具体的热更新逻辑,具体的热更新行为,由 Vue、React 这些框架提供;
  • (3)更新步骤:
  1. 监听到文件修改
  2. 遍历文件对应的模块,分别计算热更新边界(即找到可以接受热更新的模块)
  3. 用 websocket 通知客户端,需要热更新哪些模块
  4. 客户端对老模块进行 dispose 失活处理
  5. 客户端动态 import,请求需要热更新的模块
  6. Vite Server 编译这些重新请求的模块,并响应请求
  7. 客户端执行热更新

相关阅读2: vite——hot-module-replacement-is-easy

延伸阅读笔记:

    1. In essence, HMR is about replacing modules on the fly while your app is running. Most bundlers use ECMAScript modules (ESM) as the module because it's easier to analyze the imports and exports, which helps to inform how a replacement in one module will affect other related modules.
    1. Fundamentally, HMR propagation is about finding the HMR boundaries, using the updated modules as the starting point. If all of the updated modules are within a boundary, the Vite dev server will inform the HMR client to inform the accepted modules to perform HMR. If some are not within a boundary, then a full page reload will be triggered.
    1. In a Vite app, you might notice a special script added in the HTML that requests /@vite/client. This contains the HMR client!

The HMR client is responsible for:

  1. Establishing a WebSocket connection with the Vite dev server.
  2. Listening to HMR payloads from the server.
  3. Providing and triggering the HMR APIs in the runtime.
  4. Send back any events to the Vite dev server.

四、 vite 项目中 hmr 失效问题修复过程记录

本文重点内容来啦~~~

1. 问题描述

修改项目中的任意文件(包括注释)都会导致页面重新刷新,开发体验不好。

2. 问题排查过程

① 打开浏览器开发者工具中的 preserve log 查看报错信息 img_v3_02d4_88f63494-3e07-4d16-9435-04e634d42f5g.jpg

② 可结合vite --debug hmr命令进一步 debug。

[hmr] /src/router/routeData.tsx failed to apply HMR as it's within a circular import. Reloading page to reset the execution order. To debug and break the circular import, you can run vite --debug hmr to log the circular dependency path if a file change triggered it.

img_v3_02d4_0d3c04bc-3592-4ed5-a137-908ebc9a343g.jpg

3. 原因解析:

  • 1. 项目中的存在很多循环引用,导致full reload。

循环引用指的是两个或多个模块相互依赖,形成了闭环。假设有两个模块 A 和 B,A 引用了 B,B 又引用了 A,就 形成了一种循环引用。如此一来,在解析模块时可能会造成复杂性和不可预见性。 当产生循环依赖(circular imports)时,但它可能会导致模块的加载和初始化顺序变得复杂,影响模块的热更新,无法正确地应用更新。

打开开发者工具-网络-ws, 查看 websocket 通信信息,可见isWithinCircularImport为 true 的信息。

image-3.png

查阅github上相关issue:github.com/vitejs/vite…

(When propagating HMR updates, we only propagate until we reach HMR-accepted modules, or the root (which we do a full reload.

A HMR-accepted module doesn't need a full reload as it can update itself. However, if the accepted module is within an import loop, updating itself can break the execution order of the import loop. This PR detects this and forces a full reload instead.)

  • 2. react 组件导出方式不符合规范,导致 react fast refresh 没有正常工作。

查看 vite-plugin-react 的报错信息 image-4.png

consistent-components-exports

For React refresh to work correctly, your file should only export React components. You can find a good explanation in the Gatsby docs. If an incompatible change in exports is found, the module will be invalidated and HMR will propagate. To make it easier to export simple constants alongside your component, the module is only invalidated when their value changes. You can catch mistakes and get more detailed warning with this eslint rule.

4. 解决方案

  • 增加相关eslint 规则,通过eslint . --fix检测项目中所有的循环依赖,通过拆分模块、优化模块引用等方式解决循环依赖问题。
// .eslintrc.js
module.exports = {
  plugins: ['react-refresh'],
  rules: {
  // 检测循环引用
'import/no-cycle': 'error',
 // Validate that your components can safely be updated with fast refresh
'react-refresh/only-export-components': 'warn',
  },
}

延伸阅读: 关于 react-refresh

GitHub - ArnaudBarre/eslint-plugin-react-refresh: Validate that your components can safely be update

fast-refresh/#how-it-works

Fast Refresh 试图在你编辑的组件上保留本地 React 状态,但仅在安全的情况下才会这么做。在文件编辑中每次更新都伴随局部状态重置的原因可能有以下几点:

  • 在类组件上,它无法保持本地状态的持久化。只有函数组件和 hooks 能够保留状态。
  • 你当前编辑的文件可能除了 React 组件之外还有其他导出。Gatsby 默认会包含一个 ESLint 规则警告此类型的设计在页面中:页面模板必须只有一个默认导出(即页面)以 及以一个命名导出的查询。
  • 匿名箭头函数,例如export default () => <div/> ,会导致 Fast Refresh 无法保持本地组件状态的持久化。Gatsby 默认会包含一个 ESLint 规则以警告这种情况。
  • 有时,一个文件会导出一个像如 HOC(WrappedComponent)这样的结果。如果返回的组件是类或小写,状态将被重置。
  • 小写的组件,如 function example() {},会导致 Fast Refresh 无法保留本地组件状态。组件和 HOCs 必须使用 PascalCase 命名。

img_v3_02d4_b4c8d39b-4fb9-4a0b-82c1-4e9a1dac6d6g-1.jpg 全部问题解决后,修改文件后,可见websocket中的循环依赖为false, 项目中的热更新功能恢复正常。

image.png

五、小结

本文简单介绍了hmr的概念、比较webpack和vite中的hmr共同点和差异,并讲述vite项目中hmr失效的排查思路和解决措施。从中,我体会到,遇到问题时多通过控制台观察报错信息,查看相应的链接资料,找到问题所在,并尝试解决。