react项目热更新问题

一、前言

热更新是现代前端开发中提升开发效率的重要技术之一。它允许在不刷新整个页面的前提下,对代码模块进行动态替换,并尽可能保留应用状态,提供即时反馈。

日常开发过程中,可能很多人会发现自己项目的热更新经常失效 ,很多时候需要手动刷新页面才行 ,这里就Umi Max项目为例探讨下热更新问题及解决方案。

二、热更新的机制

1. 什么是热更新(HMR)

热更新 (Hot Module Replacement,简称 HMR)是一种由构建工具(如 Webpack、Vite、Parcel 等)提供的能力,结合运行时插件,能够动态更新单个或多个模块,而无需重新加载整个页面。

1.1. 核心目标
  • 只更新修改的部分
  • 保留应用状态(如表单输入、组件状态等)
  • 提供即时反馈,提高开发效率

2. 基本原理

2.1. 基本工作流程

(1)文件变更监听

  • 构建工具通过文件系统监听器检测项目文件的变化。

(2)增量构建

  • 工具仅对发生变化的模块进行编译和打包,生成更新补丁。

(3)推送更新

  • 开发服务器通过 fetch 或 socket 推送更新信息到浏览器。

(4)模块替换与执行

  • 浏览器端接收更新后,动态替换旧模块,并通知应用完成更新。
2.2. 核心技术机制

(1)模块系统的动态性

  • 前端框架(如 React、Vue)基于模块系统(ES Modules / CommonJS)构建。
  • 模块可以被动态加载、卸载和替换。

(2)模块热更新协议

  • 构建工具定义了一套标准化的协议来描述模块的更新过程。
  • 更新内容通常以 JSON 或 JS 文件形式传输。

(3)状态保留机制

  • 需结合运行时的热替换插件
  • 利用框架的响应式系统或 Hook 机制,在更新模块的同时保留状态。
    • React 使用 useStateuseRef 等 Hook 来保存状态;
    • Vue 使用 data()setup() 中的响应式变量。

3. react-refresh

react-refresh,是 react 官方推出的运行时热更新方案,当前广泛使用。

  • 在 react 生态下,热更新机制经历了两个重要阶段:
    • 传统模式(react hot loader)和 新模式(react fast refresh,即 react-refresh)。
    • 传统模式由于稳定性差且与函数式组件及 hooks 的兼容性不佳,在 react 16.8 后逐渐废弃。
  • umi max 项目的 mfsu/mako 也是使用的 react-refresh。
3.1. 和 HMR 的关系

HMR 是底层机制:

  • HMR 是 Webpack、Vite 等构建工具提供的模块热替换能力
  • 它监听文件变化,通过 socket/fetch 通知浏览器更新模块,但不关心具体内容

react-refresh 是运行时实现:

  • React Refresh 是 React 提供的运行时热更新方案
  • 它通过 Babel 插件 + 运行时代码,实现组件状态保留和更新逻辑。
3.2. 工作原理

(1)编译阶段(Babel 插件)

  • 使用 @babel/plugin-react-refresh 注入组件签名(Signature),并注册逻辑(injectIntoGlobalHook);
    • 每个组件都会生成一个唯一的 ID,用于运行时识别。

(2)运行时阶段(React Refresh Runtime)

  • 通过 react-refresh/runtime 跟踪组件变化;
    • 如果组件未发生结构性变化,则保留其状态,页面组件自动更新;
    • 否则,触发完整刷新,即页面自动刷新。
3.3. 使用方式
  • 方式一:使用成熟的框架,例如:create-react-app、umi max,默认自带。
  • 方式二:自行配置(webpack 为例)
    • npm install @pmmmwh/react-refresh-webpack-plugin react-refresh

      // webpack.config.js
      const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

      module.exports = {
      plugins: [
      new ReactRefreshWebpackPlugin(),
      ],
      module: {
      rules: [
      {
      test: /.[jt]sx?$/,
      use: [
      {
      loader: 'babel-loader',
      options: {
      plugins: ['react-refresh/babel'],
      },
      },
      ],
      },
      ],
      },
      };

三、ts/js 热更新

1. 失效问题

1.1. 背景

蚂蚁中后台场景下,曾推出站点应用二合一模式(极简流程),研发流程和访问速度相比以往都有较大的提升,在该模式下,以往的主应用变成了主题模式的接入,提供导航栏、侧边栏等通用 UI 和 API 的配置能力。

  • 在接入主题时通常需要 配置 externals ,在本地开发状态下 react 和 react-dom 使用了 cdn 形式的依赖资源。
  • 就是这里的 externals 配置导致了热更新失效。
    • 准确的说,是 react-refresh 失效,而 HMR 未失效,所以能看到代码变动的推送消息在浏览器端能收到,但未触发组件自动更新。
1.2. externals 导致

项目 externals 配置 react 和 react-dom 使用 cdn 资源时,react-refresh 热更新机制就会失效。

1.3. 根本原因

在开发环境中,react-refresh/runtime 提供了 injectIntoGlobalHook 方法,用于向全局注册 API,供 react 渲染器调用。

如果 react 和 react-dom 通过 CDN 加载,它会在 react-refresh 初始化之前执行,导致钩子注入失败。

2. 解决方案

2.1. chrome 扩展(推荐)

方案一:chrome 扩展

  • 安装 react 调试工具扩展:React Developer Tools,并开启,然后刷新下页面,热更新就生效了。
  • 该扩展插件是 react 官方推出的,稳定性有保障。

为什么上述扩展插件能解决?

  • 原因是 react developer tools 扩展在加载时,会在 window 上注入 __REACT_DEVTOOLS_GLOBAL_HOOK__ 对象,并添加相关 API,这个对象和 react-refresh 有深度联动以提供代码调试能力,正好补足了热更新所需的运行环境。
2.2. js 插入

方案二:js 插入

  • 基于以上扩展插件提供的__REACT_DEVTOOLS_GLOBAL_HOOK__对象能提供热更新运行环境,可以实现一段简化版的 js 代码,插入到 html 的 head 中提前执行,从而修正运行时的热更新问题。

  • js 代码:
    *

    TypeScript 复制代码
    (function (globalObject) {
      var rendererIdCounter = 0;
      var rendererMap = new Map;
      var devToolsHook = {
        renderers: rendererMap,
        supportsFiber: true,
        inject: function (renderer) {
            var id = ++rendererIdCounter;
            rendererMap.set(id, renderer);
            return id;
        },
      };
    
      Object.defineProperty(globalObject, "__REACT_DEVTOOLS_GLOBAL_HOOK__", {
        configurable: true,
        enumerable: false,
        get: function () {
          return devToolsHook;
        }
      });
    })(window);
  • umi max 下的使用方式,在 config.ts 里配置:
    *

    TypeScript 复制代码
    import { defineConfig } from 'xxx';
    
    const isLocalDev = process.env.NODE_ENV === 'development';
    
    export default defineConfig({
      ...,
      headScripts: isLocalDev
        ? [
            {
              content: `上述js代码字符串`,
            },
            // 主题等其他插入的代码放下面
            ..., 
          ]
        : [],
      externals: isLocalDev
        ? {
            react: 'React',
            'react-dom': 'ReactDOM',
          }
        : {},
    });
  • 本地重启项目,刷新页面,热更新即可生效。

四、样式 热更新

1. 失效问题

1.1. 背景
  • umi max 官方推荐的 css 样式方案是使用 styled-components,并推荐大颗粒度的方式。
  • 我们在写代码时,可能会把样式代码抽离到一个单独的文件里,例如 index.style.ts。
  • 这样写代码时会发现,修改样式,页面并不会自动更新,手动刷新页面才生效。

总结:

  • styled-components 方案下,样式文件单独抽离后,样式热更新会失效
1.2. 原因分析
  • 首先需要说明,HMR 是模块化热更新,只更新变化的模块/组件。
  • 在 styled-components 方案下,样式组件的 dom 节点除了自己添加类名外,还会多出两个动态类名:
  • 官方文档说明
    • 例如类名:xx__sc-crrtmM hEYPxh testWrap
      • 第一个类名是拼接了 hash,这个 hash 值和样式文件路径有关
      • 中间的类名是纯 hash,这个 hash 值和样式内容有关
      • 重点就是中间这个 hash:
        • 样式代码修改后,样式内容发生变化,生成了新的 hash;但由于样式文件被单独抽离,ts 组件代码并没有改动,所以 dom 节点也没触发更新,使用的还是已过期的 hash 值,导致新的样式作用不上。

css modules 样式方案不会有这个问题:

  • 虽然也会在类名上拼接一个 hash,但这个 hash 值默认是和样式文件的路径相关,修改 css 代码不会改变 hash 值,所以不存在失效问题。

2. 解决方案

2.1. 合并样式文件
  • 解决方式还是得把样式和 ts 组件合并一个文件里。
  • 考虑到代码结构清晰性、可维护性,建议使用 styled-components 时采用大颗粒度模式 (即默认只在根节点套一层样式组件),并将样式代码放到最底部 (类 Vue 文件结构,数据 + dom + 样式),这样整体结构依然清晰完整。
  • 项目的 lint 校验规则 上需要处理下,配置一下项目的 .eslintrc.js 文件,添加一条 rule:
    • js 本身有预解析能力,没必要严格控制定义顺序,用不好反而容易使结构混乱。
相关推荐
徐小夕@趣谈前端26 分钟前
NO-CRM 2.0正式上线,Vue3+Echarts+NestJS实现的全栈CRM系统,用AI重新定义和实现客户管理系统
前端·javascript·人工智能·开源·编辑器·echarts
catino32 分钟前
图片、文件上传
前端
Mr Xu_44 分钟前
Vue3 + Element Plus 实现点击导航平滑滚动到页面指定位置
前端·javascript·vue.js
小王努力学编程1 小时前
LangChain——AI应用开发框架(核心组件1)
linux·服务器·前端·数据库·c++·人工智能·langchain
pas1361 小时前
35-mini-vue 实现组件更新功能
前端·javascript·vue.js
前端达人1 小时前
为什么聪明的工程师都在用TypeScript写AI辅助代码?
前端·javascript·人工智能·typescript·ecmascript
快乐点吧1 小时前
使用 data-属性和 CSS 属性选择器实现状态样式控制
前端·css
EndingCoder2 小时前
属性和参数装饰器
java·linux·前端·ubuntu·typescript
小二·2 小时前
Python Web 开发进阶实战:量子机器学习实验平台 —— 在 Flask + Vue 中集成 Qiskit 构建混合量子-经典 AI 应用
前端·人工智能·python
TTGGGFF2 小时前
控制系统建模仿真(十):实战篇——从工具掌握到工程化落地
前端·javascript·ajax