Vite 热更新的主要流程

简介: Vite 热更新的主要流程

热更新的英文全称为Hot Module Replacement,简写为 HMR。当修改代码时,HMR 能够在不刷新页面的情况下,把页面中发生变化的模块,替换成新的模块,同时不影响其他模块的正常运作

本文讲的会讲述热更新的每个流程,主要的作用是什么,还有这些流程是怎么串起来的,目的是帮助大家对热更新的流程有个基本的了解。

由于篇幅原因,本文不会非常深入的每个流程的细节。

本文的用到的代码放在 GitHub,里边有两个项目,一个是纯 ts 的热更新项目,一个是普通的 vue 项目


热更新流程


在介绍热更新的主要流程前,我们先来看看这个问题

把一头大象装进冰箱,需要几步?

这个问题相信大家都非常的熟悉,只需要三步:

  1. 打开冰箱门
  2. 把大象装进冰箱
  3. 把冰箱门关起来

这个问题本身不是考验人的的逻辑能力,而是考验抽象解决方案关键步骤的能力

热更新的流程非常大,且很复杂,我们要把复杂问题简单化,只关注核心的流程,将次要的问题抽象化,从而对整个热更新的过程有所理解

在这个问题中,核心流程就是这三个步骤,然后我们可以进一步细化我们需要关注的步骤,其他步骤可以暂且忽略

既然只关心核心的流程,那么你觉得,热更新的有哪些核心流程?

从修改代码,到界面更新,这个过程发生了什么?

这是我在给小伙伴分享时,他们提出的:

  1. 修改代码
  2. 重新编译(怎么编译,编译产物是什么,先不管)
  3. 告诉前端要热更新了(怎么告诉,先不管)
  4. 前端执行热更新代码进行热更新(怎么更新,先不管)

实际上,也就是这么几个过程

下面是我画的热更新的主要流程的时序图,大家一开始可能是看不懂的,这不重要,后面会逐一细讲,只要大概清晰各个部分的时序关系即可

1686386868559.png

vite server:指 vite 在开发时启动的 server

vite client:vite dev server 会在 index.html 中,注入路径为 @vite/client 的脚本,这个脚本是运行在浏览器的


1686386843431.png


暂时先记住这个核心流程:

  1. 修改代码,vite server 监听到代码被修改
  2. vite 计算出热更新的边界(即受到影响,需要进行更新的模块)
  3. vite server 通过 websocket 告诉 vite client 需要进行热更新
  4. 浏览器拉取修改后的模块
  5. 执行热更新的代码

我们先从离我们最近的浏览器端,开始介绍


热更新 API 简介


该小节主要讲这两部分:


1686386822708.png

这里主要涉及到两个 API:

这两个 API 定义了拉取到新的代码之后,如何进行老代码的退出,和新代码的更新

我们先来看看,没有使用热更新 API 的代码被修改时,会发生什么?


不使用热更新 API


该小节对应的项目代码在 /package/ts-file-test,对应的文件为 no-hrm.ts

下图主要是一个 ts 文件,直接获取到一个 DOM,并替换其 innerHTML

1686386792496.png

我们可以看到,该文件没有定义热更新,当文件被修改时,整个页面都重新刷新了。因为 vite 不知道如何进行热更新,所以只能刷新页面


使用 hot.accept API


该小节对应的项目代码在 /package/ts-file-test,对应的文件为 accept.ts

import.meta.hot.accept API 用于传入一个回调函数,来定义该模块修改后,需要怎么去热更新


// src/accept.ts
export const render = () => {
  const el = document.querySelector<HTMLDivElement>('#accept')!;
  el.innerHTML = `
    <h1>Project: ts-file-test</h1>
    <h2>File: accept.ts</h2>
    <p>accept test</p>
  `;
};
if (import.meta.hot) {
  // 调用的时候,调用的是老的模块的 accept 回调
  import.meta.hot.accept((mod) => {
    // 老的模块的 accept 回调拿到的是新的模块
    console.log('mod', mod);
    console.log('mod.render', mod.render);
    mod.render();
  });
}

当我们将修改该文件时(将 <p>accept test</p> 改成 <p>accept test2</p> ),之前老的模块注册的 accept 的回调就会被执行

mod 就是修改后的模块对象,在该文件中,mod 就是一个导出了 render 函数的对象


1686386755288.png


当模块被修改时,重新执行 render 函数,设置 innerHTML 更新界面。

这时候我们定义了如何进行热更新,vite 就不会刷新页面了(刷新页面会清空所有请求,而下图没有清空请求)

1686386737908.png

dispose 类似 hot,只是 dispose 定义的是老模块如何退出,而 hot 定义的是新模块如何更新

什么时候老模块需要退出?

假如你的页面有个定时器,就要在老模块退出时,将定时器清除,否则每次修改,页面会新增一个定时器,页面上的定时器会越来越多,造成内存泄露

dispose 主要用来做一些模块的退出工作

写热更新代码非常麻烦,应该没有人会在业务中写?

热更新代码的确很麻烦,业务中基本上也不会有人写,但我们在写 vue 代码时,确实有热更新的。

那是因为, vite 的 vite-plugin 插件,在编译模块时加入了 vue 热更新的代码

vite 本身只提供热更新 API,不提供具体的热更新逻辑,具体的热更新行为,由 vue、react 这些框架提供


热更新边界


该小节主要讲这一部分

1686386709710.png

什么是热更新边界?作用是什么?

假设有两个文件,关系如下


1686386687723.png


从上一小节,我们可以知道,vue 自带了热更新逻辑,而我们写的 ts 文件,没有热更新逻辑

useData.ts 被修改时,这时候是会刷新页面吗?

答案是不会的。vue 组件依赖的 ts 文件被修改,可以对这个 vue 文件进行热更新,重新加载组件。如果刷新页面,那开发体验就不太好了。

这时候,index.vue 就被称为热更新边界——最近的可接受热更新的模块

沿着依赖树,往上找到最近的一个可以热更新的模块,即热更新边界,对其进行热更新即可

为什么有时候修改代码可以热更新,有时候却是刷新页面?例如在 vue 项目中修改 main.ts

修改 main.ts 时,因为往上找不到可以热更新的模块了,vite 不知道如何进行热更新,因此只能刷新页面

如果其他 ts 文件,能找到热更新边界,就可以直接进行热更新

文件跟模块不是一一对应的吗?为什么需要遍历文件对应的模块?

在 vite 中,文件跟模块不是一一对应

因为 vite 可以加入查询参数,可查看 vite 文档【更改资源被引入的方式

// 显式加载资源为一个 URL
import assetAsURL from './asset.js?url'
// 以字符串形式加载资源
import assetAsString from './shader.glsl?raw'
// 加载为 Web Worker
import Worker from './worker.js?worker'
// 在构建时 Web Worker 内联为 base64 字符串
import InlineWorker from './worker.js?worker&inline'

同一个文件,可能作为多个模块,例如 raw 时的编译产出的模块跟 worker 时编译产出的模块就是两个不同的模块

因为,一个文件,是对应多个模块的。这些模块都需要找到他们的热更新边界,并进行热更新


浏览器接收热更新信号


该小节主要讲这一部分


1686386581719.png


websocket 是什么创建的?

vite dev server 会在 index.html 中,注入路径为 @vite/client 的脚本,当访问 index.html 时,就会拉取该脚本

client.ts 在加载时,会创建 websocket 并监听 message 事件


1686386555259.png

handleMessage 负责处理各种信号,由于篇幅有限,我们不会展开讲细节


async function handleMessage(payload: HMRPayload) {
  switch (payload.type) {
    case 'connected':
      // 连接信号
      console.log(`[vite] connected.`)
      setInterval(() => socket.send('ping'), __HMR_TIMEOUT__)
      break
    case 'update':
      // 模块更新信号
      break
    case 'custom': {
      // 自定义信号
      break
    }
    case 'full-reload':
      // 页面刷新信号
      break
    case 'prune':
      // 模块删除信号
      break
    case 'error': {
      // 错误信号
      break
    }
  }
}

我们可以通过抓包的方式,看到 vite dev server 跟 client 之前的通信

1686386530317.png

server 模块转换


该小节主要讲这一部分


1686386513751.png

模块代码转换 vite 的核心,这部分足以开一个大的主题去讲,同样的,本文只会介绍个大概,只需要知道 vite 会转换代码即可,转换细节暂时可以不关注,把 vite server 当做一个黑箱

之前说的到,vite 的 plugin-vue 插件,将热更新代码注入到模块中,就是在编译转换模块的过程中处理


1686386499324.png


从图中可以看出,index.vue 经过编译后,内容是 js 代码,其中还能看到 import.meta.hot.accept 定义热更新的回调

时序图中,有个循环条件,直到动态 import 的模块没有模块依赖,是什么意思?

假如有以下两个文件:


index.vue
  - useData.ts

index.vue 依赖(import)了 useData.ts

当修改 useData.ts 时,会执行以下的步骤:

  1. vite 沿着依赖树,往上找到 index.vue,作为热更新边界
  2. server 将热更新边界信息,通过 websocket 传递到 client
  3. client 执行老的 index.vueimport.meta.hot.dispose 回调
  4. client 动态 import(index.vue),vite 会重新编译 index.vue
  5. 执行 index.vue 的代码(此时请求到 index.vue 虽然是 vue 后缀,但是它的内容经过编译后,是 js 代码),执行过程中遇到 import useData.ts

1686386472927.png


  1. 动态拉取 useData.ts 模块,vite 会重新编译 useData.ts
  2. 执行 useData.ts 的代码
  3. client 执行新的 index.vueimport.meta.hot.accept 回调

因为热更新边界的模块,可能会存在依赖,import 了其他模块,这些模块都需要 import 拉取,直到动态 import 的模块没有模块依赖


参考资料


目录
相关文章
|
前端开发 JavaScript 开发者
Vite前端构建工具详解
Vite 是一款新兴的前端构建工具,它的出现带来了前端开发体验的革命性变化。本文将介绍 Vite 的基本概念和核心特性,并通过代码实例来演示其强大功能。
187 0
|
1月前
|
开发框架 移动开发 前端开发
除了 HMR 插件,还有哪些技术可以实现热更新?
【10月更文挑战第23天】不同的热更新技术都有其特点和适用场景。开发者需要根据项目的具体需求和技术架构,选择合适的热更新技术来提高开发效率和用户体验。同时,随着技术的不断发展,热更新技术也在不断创新和完善,未来可能会出现更多更先进的热更新技术和方法。
|
2月前
|
资源调度 JavaScript 前端开发
如何实现一个类似 vite 的脚手架并发布 npm
本文介绍了如何实现一个类似 Vite 的脚手架工具。通过详细解析和实践,文章分享了从零开始构建脚手架的过程,包括技术选型、开发步骤及发布 NPM 包的完整流程。最终目标是让用户能够通过 `yarn create electron-prokit myapp` 快速搭建 Electron 项目。项目源码可在 GitHub 上获取。
33 5
|
4月前
|
JavaScript 前端开发
什么是前端构建工具?vite和vite脚手架的关系!
【8月更文挑战第1天】前端构建工具简析
88 4
|
5月前
Vite 项目中如何去集成 Mock 环境 (插件:vite-plugin-mock)
Vite 项目中如何去集成 Mock 环境 (插件:vite-plugin-mock)
194 0
|
JavaScript
为老的vueCli项目添加vite支持
为老的vueCli项目添加vite支持
143 0
为老的vueCli项目添加vite支持
|
JavaScript Android开发
Vue 接口联调以及打包上线
Vue 接口联调以及打包上线
91 0
|
资源调度 JavaScript 前端开发
nuxt2-storybook-vite:环境搭建、基础使用 / nuxt项目组件库
nuxt2-storybook-vite:环境搭建、基础使用 / nuxt项目组件库
342 0
|
缓存 JavaScript CDN
关于vite打包优化,你了解多少
关于vite打包优化,你了解多少
|
Web App开发 缓存 前端开发