HMR API及其原理

article/2025/10/7 11:56:43

很久之前,遇到一个面试题:【在代码变更之后,如何实时看到更新后的页面效果呢?】

在传统的方案中,我们可以通过 live reload 也就是自动刷新页面的方式来解决的,不过随着前端工程的日益庞大,开发场景也越来越复杂,这种live reload的方式却有很多的局限。简单来说,就是模块局部更新+状态保存的需求在live reload的方案并没有得到很好的实现,从而导致开发体验欠佳。当然,也有一些临时的解决方案,比如状态存储到浏览器的本地缓存(localStorage 对象)中,或者直接 mock 一些数据。但这些方式未免过于粗糙,无法满足通用的开发场景,且实现上也不够优雅。

那么,如何实现模块级别的代码局部更新操作呢?业界一般会使用HMR 技术来解决这个问题,像 Webpack、Parcel 这些传统的打包工具其底层都实现了一套 HMR API,而现在我们要讲的Vite也是利用HMR API来实现的。相比于传统的打包工具,Vite 的 HMR API 基于 ESM 模块规范来实现,可以达到毫秒级别的更新速度,性能非常强悍。

一、HMR 简介

HMR的全称叫Hot Module Replacement,即模块热替换或者模块热更新。在计算机领域中也有一个类似的概念叫热插拔,我们经常使用的 USB 设备就是一个典型的代表,当我们插入 U 盘的时候,系统驱动会加载在新增的 U 盘内容,不会重启系统,也不会修改系统其它模块的内容。HMR 的作用其实和U盘的插拔是一样的,就是在页面模块更新的时候,直接把页面中发生变化的模块替换为新的模块,同时不会影响其它模块的正常运作。可以看到,下面是Vite官方例子实现 HMR 的效果。

1.gif

可以看到,我们启动项目,然后对页面再次进行调整的时候,比如把最上面的 Logo 图片去掉,这个时候大家可以实时地看到图片消失了,但其他的部分并没有发生改变,这就是HMR的技术提供的局部刷新和状态保存。

二、HMR API

Vite 作为一个完整的构建工具,本身实现了一套 HMR 系统,值得注意的是,这套 HMR 系统基于原生的 ESM 模块规范来实现,在文件发生改变时 Vite 会侦测到相应 ES 模块的变化,从而触发相应的 API,实现局部的更新。

事实上,Vite 的 HMR API 也是基于一套完整的 ESM HMR 规范来实现,这个规范由同时期的 no-bundle 构建工具 Snowpack、WMR 与 Vite 一起制定,是一个比较通用的规范。下面是 HMR API 的类型定义:

interface ImportMeta {readonly hot?: {readonly data: anyaccept(): voidaccept(cb: (mod: any) => void): voidaccept(dep: string, cb: (mod: any) => void): voidaccept(deps: string[], cb: (mods: any[]) => void): voidprune(cb: () => void): voiddispose(cb: (data: any) => void): voiddecline(): voidinvalidate(): voidon(event: string, cb: (...args: any[]) => void): void}
}

解释一下,import.meta对象为现代浏览器原生的一个内置对象,Vite 所做的事情就是在这个对象上的 hot 属性中定义了一套完整的属性和方法。因此,在 Vite项目中,我们可以通过import.meta.hot来访问关于 HMR 的这些属性和方法,比如import.meta.hot.accept()。

接下来,我们就来一一熟悉这些 API 的使用方式。

3.1、hot.accept

在 import.meta.hot 对象上有一个非常关键的方法accept,因为它决定了 Vite 进行热更新的边界,那么如何来理解这个accept的含义呢?

从字面上来看,它表示接受的意思。没错,它就是用来接受模块更新的。 一旦 Vite 接受了这个更新,当前模块就会被认为是 HMR 的边界。那么,Vite 接受谁的更新呢?这里会有三种情况:

  • 接受自身模块的更新;
  • 接受某个子模块的更新;
  • 接受多个子模块的更新;

这三种情况分别对应 accept 方法三种不同的使用方式,下面我们就一起来分析一下。

3.1.1 接受自身更新

当模块接受自身的更新时,当前模块会被认为 HMR 的边界。也就是说,除了当前模块,其他的模块均未受到任何影响。下面是我准备的一张示例图,你可以参考一下:

image.png

为了加深大家的理解,我们以一个实际的例子来操练一下,这个例子已经放到了Github 仓库中。首先,展示一下整体的目录结构:

.
├── favicon.svg
├── index.html
├── node_modules
│   └── ...
├── package.json
├── src
│   ├── main.ts
│   ├── render.ts
│   ├── state.ts
│   ├── style.css
│   └── vite-env.d.ts
└── tsconfig.json

首先,我们打开项目的index.html文件:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><link rel="icon" type="image/svg+xml" href="favicon.svg" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Vite App</title></head><body><div id="app"></div><p>count: <span id="count">0</span></p><script type="module" src="/src/main.ts"></script></body>
</html>

上面的代码比较简单,除了一个用于显示当前数量的count和引入了 /src/main.ts文件,main.ts文件的代码如下:

import { render } from './render';
import { initState } from './state';
render();
initState();

文件依赖了render.ts和state.ts两个文件,前者负责渲染文本内容,后者负责记录当前的页面状态:

// src/render.ts
// 负责渲染文本内容
import './style.css'
export const render = () => {const app = document.querySelector<HTMLDivElement>('#app')!app.innerHTML = `<h1>Hello Vite!</h1><p target="_blank">This is hmr test.123</p>`
}
// src/state.ts
// 负责记录当前的页面状态
export function initState() {let count = 0;setInterval(() => {let countEle = document.getElementById('count');countEle!.innerText =  ++count + '';}, 1000);
}

现在,可以执行npm i安装依赖,然后npm run dev启动项目,在浏览器访问就可以看到每隔一秒钟,count值会加一。

image.png

现在,我们可以试着改动一下 render.ts 的渲染内容,比如增加一些文本:

// render.ts
export const render = () => {const app = document.querySelector<HTMLDivElement>('#app')!app.innerHTML = `<h1>Hello Vite!</h1>
+   <p target="_blank">This is hmr test.123 增加的文本</p>`
}

此时,我们打开之前的页面,发现渲染内容是更新了,但不知道有没有注意到最下面的count值瞬间被置零了,并且查看控制台,也有这样的 log信息。

[vite] page reload src/render.ts

很明显,当 render.ts 模块发生变更时,Vite 发现并没有经过 HMR 相关的处理,然后就直接刷新了页面。现在,让我们在render.ts中加上如下的代码:

// 条件守卫
+ if (import.meta.hot) {
+  import.meta.hot.accept((mod) => mod.render())
+ }

加上这句代码之后,import.meta.hot对象只有在开发阶段才会被注入到全局,生产环境是访问不到的,另外增加条件守卫之后,打包时识别到 if 条件不成立,会自动把这部分代码从打包产物中移除,来优化资源体积。

此时,简单的解释一下对于 import.meta.hot.accept的使用,我们传入了一个回调函数作为参数,入参即为 Vite 给我们提供的更新后的模块内容,在浏览器中打印mod内容如下,正好是render模块最新的内容。

image.png

接着,我们在回调中调用了一下 mod.render 方法,也就是当模块变动后,每次都重新渲染一遍内容。这时你可以试着改动一下渲染的内容,然后到浏览器中注意一下count的情况,并没有被重新置零,而是保留了原有的状态。

image.png

没错,现在 render 模块更新后,只会重新渲染这个模块的内容,而对于 state 模块的内容并没有影响,并且控制台的 log 也发生了变化。

[vite] hmr update /src/render.ts

现在我们算是实现了初步的 HMR,也在实际的代码中体会到了 accept 方法的用途。当然,在这个例子中我们传入了一个回调函数来手动调用 render 逻辑,但事实上你也可以什么参数都不传,这样 Vite 只会把 render模块的最新内容执行一遍,但 render 模块内部只声明了一个函数,因此直接调用import.meta.hot.accept()并不会重新渲染页面。

3.1.2 接受依赖模块的更新

接下来,我们来分析一下接受依赖模块更新是如何做到的。首先,我们来看一张原理图。

1 (1).gif

还是拿示例项目来举例,main模块依赖render 模块,也就是说,main模块是render父模块,那么我们也可以在 main 模块中接受render模块的更新,此时 HMR 边界就是main模块了。首先,我们将前面render模块的添加的accept 相关代码先删除:

// render.ts
- if (import.meta.hot) {
-   import.meta.hot.accept((mod) => mod.render())
- }

然后,在main模块增加如下代码:

// main.ts
import { render } from './render';
import './state';
render();
+if (import.meta.hot) {
+  import.meta.hot.accept('./render.ts', (newModule) => {
+    newModule.render();
+  })
+}

与之前不同的是,第一个参数传入一个依赖的路径,也就是render模块的路径,这就相当于告诉 Vite: 我监听了 render 模块的更新,当它的内容更新的时候,请把最新的内容传给我。同样的,第二个参数中定义了模块变化后的回调函数,这里拿到了 render 模块最新的内容,然后执行其中的渲染逻辑,让页面展示最新的内容。

3.1.3 多个子模块的更新

接下来是最后一种 accept 的情况——接受多个子模块的更新。有了上面两种情况的铺垫,这里再来理解第三种情况就容易多了,下面是原理示意图。

1 (2).gif

这里的父模块可以接受多个子模块的更新指的是,当其中任何一个子模块更新之后,父模块会成为 HMR 边界。还是拿之前的例子来演示,现在我们更改main模块代码。

// main.ts
import { render } from './render';
import { initState } from './state';
render();
initState();
+if (import.meta.hot) {
+  import.meta.hot.accept(['./render.ts', './state.ts'], (modules) => {
+    console.log(modules);
+  })
+}

在代码中我们通过 accept 方法接受了render和state两个模块的更新,接着让我们手动改动一下某一个模块的代码,观察一下回调中modules的打印内容。例如当我改动 state模块的内容时,回调中拿到的 modules 如下所示。

image.png

可以看到,Vite 给我们的回调传来的参数modules其实是一个数组,和我们第一个参数声明的子模块数组一一对应。因此modules数组第一个元素是 undefined,表示render模块并没有发生变化,第二个元素为一个 Module 对象,也就是经过变动后state模块的最新内容。

于是,我们根据 modules 进行自定义的更新,修改 main.ts:

// main.ts
import { render } from './render';
import { initState } from './state';
render();
initState();
if (import.meta.hot) {import.meta.hot.accept(['./render.ts', './state.ts'], (modules) => {// 自定义更新const [renderModule, stateModule] = modules;if (renderModule) {renderModule.render();}if (stateModule) {stateModule.initState();}})
}

现在,你可以改动两个模块的内容,可以发现,页面的相应模块会更新,并且对其它的模块没有影响。但不过带来了另一个问题,当改动了state模块的内容之后,页面的内容会变得错乱。这是为什么呢?

我们快速回顾一下 state模块的内容:

// state.ts
export function initState() {let count = 0;setInterval(() => {let countEle = document.getElementById('count');countEle!.innerText =  ++count + '';}, 1000);
}

其中设置了一个定时器,但当模块更改之后,这个定时器并没有被销毁,紧接着我们在 accept 方法调用 initState 方法又创建了一个新的定时器,导致 count 的值错乱。那如何来解决这个问题呢?这就涉及到新的 HMR 方法——dispose方法了。

3.2 hot.dispose

此模块主要用于处理在模块更新、旧模块需要销毁时事情。比如上面的问题,我们可以通过在state模块中调用 dispose 方法来轻松解决定时器共存的问题,代码改动如下:

// state.ts
let timer: number | undefined;
if (import.meta.hot) {import.meta.hot.dispose(() => {if (timer) {clearInterval(timer);}})
}
export function initState() {let count = 0;timer = setInterval(() => {let countEle = document.getElementById('count');countEle!.innerText =  ++count + '';}, 1000);
}

此时,我们再来观察一下浏览器 HMR 的效果。可以看到,当我稍稍改动一下state模块的内容(比如加个空格),页面确实会更新,而且也没有状态错乱的问题,说明我们在模块销毁前清除定时器的操作是生效的。

此时,让我们来重新梳理一遍热更新的逻辑:

image.png

当我们改动了state模块的代码,main模块接受更新,执行 accept 方法中的回调,接着会执行 state 模块的initState方法。注意了,此时新建的 initState 方法的确会初始化定时器,但同时也会初始化 count 变量,也就是count从 0 开始计数了!

这显然是不符合预期的,我们期望的是每次改动state模块,之前的状态都保存下来。怎么来实现呢?

3.3 hot.data

hot.data属性主要用来在不同的模块实例间共享一些数据。使用上也非常简单,让我们来重构一下 state 模块。

let timer: number | undefined;
if (import.meta.hot) {
+  // 初始化 count
+  if (!import.meta.hot.data.count) {
+    import.meta.hot.data.count = 0;
+  }import.meta.hot.dispose(() => {if (timer) {clearInterval(timer);}})
}
export function initState() {
+  const getAndIncCount = () => {
+    const data = import.meta.hot?.data || {
+      count: 0
+    };
+    data.count = data.count + 1;
+    return data.count;
+  };timer = setInterval(() => {let countEle = document.getElementById('count');
+    countEle!.innerText =  getAndIncCount() + '';}, 1000);
}

我们在 import.meta.hot.data 对象上挂载了一个count 属性,在二次执行initState的时候便会复用 import.meta.hot.data 上记录的 count 值,从而实现状态的保存。

到此,基本实现了这个示例应用的 HMR 的功能。在这个过程中,我们用到了核心的accept、dispose 和data属性和方法。除此之外,常见的属性和方法还有如下一些:

3.4 其他方法

import.meta.hot.decline()

这个方法调用之后,相当于表示此模块不可热更新,当模块更新时会强制进行页面刷新。

import.meta.hot.invalidate()

这个方法就更简单了,只是用来强制刷新页面。

自定义事件

我们还可以可以通过 import.meta.hot.on 来监听 HMR 的自定义事件,内部有这么几个事件会自动触发。

  • vite:beforeUpdate:当模块更新时触发;
  • vite:beforeFullReload:当即将重新刷新页面时触发;
  • vite:beforePrune:当不再需要的模块即将被剔除时触发;
  • vite:error:当发生错误时(例如,语法错误)触发。

三、总结

在本小节中,我们认识了 HMR 这个概念,了解它相比于传统的 live reload 所解决的问题:模块局部更新和状态保存。

然后,我带你熟悉了 Vite HMR 中的各种 API,尤其是 accept 方法,根据 accept 的不同用法,我们分了三种情况来讨论 Vite 接受更新的策略: 接受自身更新、接受依赖模块的更新和接受多个子模块的更新,并通过具体的示例演示了这三种情况的代码。可以看到,在代码发生变动的时候,Vite 会定位到发生变化的局部模块,也就是找到对应的 HMR 边界,然后基于这个边界进行更新,其他的模块并没有受到影响,这也是 Vite 中热更新的时间能达到毫秒级别的重要原因。


http://chatgpt.dhexx.cn/article/y04gh4R4.shtml

相关文章

webpack实践之路(七):模块热替换HMR

HMR 模块热替换(Hot Module Replacement 或 HMR)允许在运行时更新各种模块&#xff0c;而无需进行完全刷新。 HMR主要是通过以下几种方式&#xff0c;来显著加快开发速度&#xff1a; 保留在完全重新加载页面时丢失的应用程序状态。只更新变更内容&#xff0c;以节省宝贵的开…

【Webpack 性能优化系列(1) - HMR 热模块替换】

webpack系列文章&#xff1a; 【Webpack 性能优化系列(9) - 多进程打包】极大的提升项目打包构建速度&#xff01;&#xff01;&#xff01;【Webpack 性能优化系列(8) - PWA】使用渐进式网络应用程序为我们的项目添加离线体验【Webpack 性能优化系列(7) - 懒加载和预加载】【…

hmr webpack 不编译_Webpack HMR 热更新实现原理深入分析

概述 在使用 Webpack 构建开发期时&#xff0c;Webpack 提供热更新功能为开发带来良好的体验和开发效率&#xff0c;那热更新机制是怎么实现的呢&#xff1f; 代码实现 Webpack 配置添加 HotModuleReplacementPlugin 插件 new webpack.HotModuleReplacementPlugin({ // Options…

webpack4.0核心概念(十)—— HMR(热模块替换-局部刷新)

HMR&#xff1a;当修改一个js或者css的时候&#xff0c;只刷新修改的内容&#xff0c;不进行整个页面的刷新。 css的HMR——只支持开发环境 不能使用mini-css-extract-plugin需要使用style-loader,因为它不支持抽离出的css,需要用style-loader ① 在webpack.config.js中配置 …

前端工程化——Livereload和HMR、本地开发服务器

目录 本地开发服务器解决的问题 动态构建 Mock服务 动态构建 源码改动之后&#xff0c;浏览器应该在何时获取重新编译后的资源&#xff1f; Livereload和HMR 有了构建系统的支持&#xff0c;前端开发人员可以使用诸多有利于开发和维护的技术进行源代码编写。然而如果在开…

hmr webpack 不编译_webpack hmr

参考&#xff1a; hmr技术支持程序运行时的模块(amd、commonJS等)的修改、添加和删除&#xff0c;而不用整个程序重新加载&#xff0c;这可以提升开发的效率&#xff1a; hmr后程序的状态可以得到保存 仅仅改变变化的部分&#xff0c;其余不变 调样式更加快捷&#xff0c;基本比…

18.webpack4之HMR

1.HMR&#xff08;Hot Module Replacement&#xff09;热模块替换 在开发环境&#xff0c;可以使用热模块替换&#xff08;HMR&#xff09;去实现如果一个模块发生变化&#xff0c;只会重新打包这一个模块&#xff08;而不是所有模块都进行打包&#xff09;&#xff0c;而无需重…

webpack5之HMR原理探究

一、概念介绍 模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块&#xff0c;而无需完全刷新。 主要是通过以下几种方式&#xff0c;来显著加快开发速度&#xff1a; 保留在完全重新加载页面期间丢失的应用程…

hmr webpack 不编译_一文搞懂 webpack HMR 原理

关注「前端向后」微信公众号&#xff0c;你将收获一系列「用心原创」的高质量技术文章&#xff0c;主题包括但不限于前端、Node.js以及服务端技术 一.HMR Hot Module Replacement(HMR)特性最早由 webpack 提供&#xff0c;能够对运行时的 JavaScript 模块进行热更新(无需重刷&a…

Webpack HMR 原理全解析

执行 npx webpack serve 命令后&#xff0c;WDS 调用 HotModuleReplacementPlugin 插件向应用的主 Chunk 注入一系列 HMR Runtime&#xff0c;包括&#xff1a; 用于建立 WebSocket 连接&#xff0c;处理 hash 等消息的运行时代码 用于加载热更新资源的 RuntimeGlobals.hmrDow…

vite1.x 热更新(HMR)的实现原理

前言 将近一年前自己尝试阅读vite源码&#xff08;2.x&#xff09;&#xff0c;虽然也有些收获但整体并没有到达我的预期&#xff0c;对于vite也是停留在一知半解的程度上。最近想重新开始学习vite&#xff0c;但回顾之前的学习历程&#xff0c;感觉不太想继续之前的方式&…

Webpack HMR 原理解析

Hot Module Replacement&#xff08;以下简称 HMR&#xff09;是 webpack 发展至今引入的最令人兴奋的特性之一 &#xff0c;当你对代码进行修改并保存后&#xff0c;webpack 将对代码重新打包&#xff0c;并将新的模块发送到浏览器端&#xff0c;浏览器通过新的模块替换老的模…

Webpack的HMR原理解析

Hot Module Replacement&#xff08;以下简称 HMR&#xff09;是 webpack 发展至今引入的最令人兴奋的特性之一 &#xff0c;当你对代码进行修改并保存后&#xff0c;webpack 将对代码重新打包&#xff0c;并将新的模块发送到浏览器端&#xff0c;浏览器通过新的模块替换老的模…

Esbuild Bundler HMR

Esbuild 虽然 bundler 非常快&#xff0c;但是其没有提供 HMR 的能力&#xff0c;在开发过程中只能采用 live-reload 的方案&#xff0c;一有代码改动&#xff0c;页面就需要全量 reload &#xff0c;这极大降低开发体验。为此添加 HMR 功能至关重要。 经过调研&#xff0c;社…

Vite HMR

传统webpack的hmr是使用webpack的HotModuleReplacementPlugin&#xff0c;而vite则是采用native ES Module的devServer。 初始化本地服务器加载并运行对应的plugin 最重要的一件事就是运行plugin&#xff0c;目前vite支持的plugin大体如下图所示 1、建立ViteDevServer服务器…

webpack HMR

HMR或者hot模式下&#xff0c;启动webpack会在浏览器与服务器之间会建立一个websocket连接&#xff0c;使得浏览器可以和服务端建立全双工通信&#xff1b;当应用程序的代码更新时&#xff0c;会要求HMR runtime检查更新&#xff0c;有更新时&#xff0c;在websoket连接中会返回…

webpack中的HMR(热更新)原理剖析

简介 Hot Module Replacement&#xff08;以下简称 HMR&#xff09;是 webpack 发展至今引入的最令人兴奋的特性之一 &#xff0c;当你对代码进行修改并保存后&#xff0c;webpack 将对代码重新打包&#xff0c;并将新的模块发送到浏览器端&#xff0c;浏览器通过新的模块替换…

HMR(热替换)

HMR 即模块热替换&#xff08;hot module replacement&#xff09;的简称&#xff0c;它可以在应用运行的时候&#xff0c;不需要刷新页面&#xff0c;就可以直接替换、增删模块。webpack 可以通过配置 webpack.HotModuleReplacementPlugin 插件来开启全局的 HMR 能力&#xff…

面试官:说说Webpack的热更新是如何做到的?原理是什么?

一、是什么 HMR全称 Hot Module Replacement&#xff0c;可以理解为模块热替换&#xff0c;指在应用程序运行过程中&#xff0c;替换、添加、删除模块&#xff0c;而无需重新刷新整个应用 例如&#xff0c;我们在应用运行过程中修改了某个模块&#xff0c;通过自动刷新会导致整…

Webpack 热更新HMR 原理全解析

一、什么是 HMR HMR 全称 Hot Module Replacement&#xff0c;中文语境通常翻译为模块热更新&#xff0c;它能够在保持页面状态的情况下动态替换资源模块&#xff0c;提供丝滑顺畅的 Web 页面开发体验。 HMR 最初由 Webpack 设计实现&#xff0c;至今已几乎成为现代工程化工具…