What(HMR 是什么?)

我们下面讨论的HMR都是基于vite自身实现的一套HMR系统。 vite实现的HMR是根据 ESM HMR 规范 来实现的。

HMRHot Module Reload模块热更新。 之前当我们在编辑器中更新代码时,会触发浏览器的页面刷新,但是这个刷新是全量刷新,相当于CMD+R。这时页面的状态会被重置掉,总之体验不好。 而模块热更新就是为了解决这样的问题,只是刷新我们编辑的代码所对应的模块,并且能保持页面的状态。Oct-11-2022 10-06-42.gif

可以看到这里我们在编辑代码时,下面count的状态是保存了的。只是热更新了上面的文字部分的模块。

Why(为什么需要 HMR?)

其实每个技术的诞生,都是为了解决之前所凸显出来的问题。HMR 也是如此,其实在上面也已经说了原因。 这里再来总结一下:为什么需要 HMR?

  1. 解决修改代码后页面全量更新,体验不好的问题
  2. 解决全量更新导致的状态丢失问题

How(怎么使用 HMR?)

vite中实现的HMR系统其实是对ESM HMR规范中的API进行了一层封装。vite会主动监听文件的变化,然后触发对应的API,来实现模块的热更新。 所以首先我们来简单了解一下这套规范中的API

API

hmrAPI都注入到了import.metahot中。 我们访问的时候只需要false.[name]即可

import.meta是浏览器中内置的一个对象。【MDN

ts
interface ImportMeta {
  readonly hot?: {
    readonly data: any
    // ======触发更新=====
    accept(): void //
    accept(cb: (mod: any) => void): void
    accept(dep: string, cb: (mod: any) => void): void
    accept(deps: string[], cb: (mods: any[]) => void): void
    // ==================
    prune(cb: () => void): void
    dispose(cb: (data: any) => void): void
    decline(): void
    invalidate(): void
    // =====监听hmr事件====
    on(event: string, cb: (...args: any[]) => void): void
    // ===================
  }
}

accept(cb)

accept翻译过来就是接受。而在hmr中他也是这个意思:接受此次热更新,而接受热更新的模块被称为HMR边界 当我们在文件中加入这行代码的时候,就是手动开启该文件模块的热更新。 当这个文件中的代码产生更新时,就会接收此次热更新的结果。

ts
if (import.meta.hot) {
  import.meta.hot.accept((mod) => {
    console.log(mod, '==')
  })
}

DANGER

accept中的mod就是更新之后的模块中所导出的内容。

比如我们的文件是下面这样,导出了renderother

ts
export const render = () => {
  // ...
}

export const other = () => {
  //...
}

if (import.meta.hot) {
  import.meta.hot.accept((mod) => {
    console.log(mod, '==')
  })
}

那么当我们在这个文件中更新代码,接受热更新时此时mod中就是: image.png 如果我们需要接受其中一个导出模块的更新,那么直接调用mod.render()或者mod.other()即可在页面上更新到最新的内容。

如果你的文件中导出方式是默认导出export default xxx,那么mod中就是mod.default

在上面的代码中,我们是向accept中传递了一个回调函数来主动触发热更新模块中的函数。因为我们这个文件中只是声明了renderother函数,并没有执行,所以需要在accpet的回调中手动触发才可以 其实有些情况下也不用传回调函数。accept会把当前变更的文件中的最新内容执行一遍。就比如我们这个文件就是一个可执行文件(类似自执行函数),当我们import这个文件的时候,文件里的代码就会执行,例如下面的情况:

ts
// render.ts
const render = () => {
  const app = document.querySelector<HTMLDivElement>('#app')!
  app.innerHTML = `
    <h1>Hello Vite12</h1>
    <p id="p">是是是</p>
  `
}


render()

if (import.meta.hot) {
  import.meta.hot.accept()
}

// main.ts
import './render.tx'

render文件执行执行了render函数,这时accept就会重新执行这个文件,也就理所当然的触发了render函数。这时就不需要我们向accpet传递回调函数了。

accept(dep, cb)

accept方法中也可以接收一个dep参数,也就是当前页面热更新时所依赖的子模块的路径。 这个dep参数,可以是一个单独字符串,也可以是一个字符串数组,当是数组时说明依赖多个子模块

ts
//main.ts
import { render } from './render'
import { initsate } from './state'

render()
initsate()

if (import.meta.hot) {
  import.meta.hot.accept('./render.ts', (mod) => {
    console.log(mod, '==')
    mod?.render()
  })
}

main模块依赖render文件 当render文件变更时,会接收热更新 因为此时没有依赖state文件,所以当state文件发生变更时会**reload page**,而不会热更新。 因为此时热更新的边界仅仅是render模块,只有render模块中的变更才会触发main的热更新

ts
//main.ts
import { render } from './render'
import { initsate } from './state'

render()
initsate()

if (import.meta.hot) {
  import.meta.hot.accept(['./render.ts', './state.ts'], ([mod1, mod2]) => {
    console.log(mod1, mod2, '==')
    mod1?.render()
    mod2?.initsate()
  })
}

这时,当state模块中的文件发生变化时,就也会触发main的热更新了。 此时,回调函数中的mod为:(因为仅仅变更了state模块,所以mod1undefined,也就说明render模块没有更新,符合预期。 image.png

dispose()

这个函数就是比较简单。就是在新模块更新前 旧模块销毁时的钩子。用来清理掉旧模块中的一些副作用。

ts
const timerId = setInterval(() => {
  countEle.innerText = Number(countEle.innerText) + 1 + ''
}, 1000)

if (import.meta.hot) {
  import.meta.hot.dispose((data) => {
    // 清理副作用
    clearInterval(timerId)
  })
}

在我们需要 hmr 的模块中如果有定时器之类的操作,我们热更新后如果不提前销毁定时器,就会重复执行定时,那么可能会出现意想不到效果。

on(event,cb)

监听自定义 HMR 事件。 自定义 HMR 事件,是在服务端定义发送的。在 vite 中,我们可以在插件中完成这件事。 vite插件中提供了handleHotUpdate

ts
// vite-plugin.tx
// 省略其他代码
handleHotUpdate({ server }) {
  server.ws.send({
    type: 'custom',
    event: 'xxx-file-change', // 自定义事件名称
    data: {} // 携带的信息
  })
  return []
}

// client
 import.meta.hot.on('xxx-file-change', (payload) => {
    console.log(payload)
})

https://github.com/sanyuan0704/island.js/pull/79 有时自定义 hmr 事件,没有触发页面更新。我们可以利用监听自定义事件,来主动触发页面的rerender

data

该属性用来共享同一个模块中更新前后的数据。 在这里面绑定的数据,不会被hmr影响或重置。

ts
import.meta.hot.data.count = 1

decline()

表示此模块不可热更新,如果在传播 HMR 更新时遇到此模块,浏览器应该执行完全重新加载

invalidate()

重新加载页面。

下回书

TIP

请看下一篇文章