vite HMR api

vite 启动热更新,dev server的信息存储在内置变量hot属性里。

hot的定义参照声明文件:

// hot.d.ts
export interface ViteHotContext {
  readonly data: any

  accept(): void
  accept(cb: (mod: ModuleNamespace | undefined) => void): void
  accept(dep: string, cb: (mod: ModuleNamespace | undefined) => void): void
  accept(
    deps: readonly string[],
    cb: (mods: Array) => void
  ): void

  acceptExports(exportNames: string | readonly string[]): void
  acceptExports(
    exportNames: string | readonly string[],
    cb: (mod: ModuleNamespace | undefined) => void
  ): void

  dispose(cb: (data: any) => void): void
  decline(): void
  invalidate(): void

  on(
    event: T,
    cb: (payload: InferCustomEventPayload) => void
  ): void
  send(event: T, data?: InferCustomEventPayload): void
}

accept

用户可以调用accept,人工介入热更新的过程。

  • accept()
// main.js
if (import.meta.hot) {
  import.meta.hot.accept()
}

当我们以不传递deps参数的方式调用accept函数,我们修改文件代码后发现页面能正常热更新,并且没有刷新页面,符合预期。

不传deps入参vite会默认分析页面整体的模块依赖情况,有代码更新,会去通知依赖方。接收到通知的依赖方重新引入更新后的代码进行热更新,所以不传deps当模块代码发生变化也是能被正常进行热更新。

  • accpet(deps)
// main.js
if (import.meta.hot) {
  import.meta.hot.accept(['./style.css'])
}

我们尝试改动非style.css的其它文件,使网页展示发生改变,页面也会自动更新,但和不传参数时候的更新方式不太一样。

当我们传入deps,尝试去修改非deps外的文件代码,触发热更新是以刷新页面的方式进行。

以上面代码为例,传入deps的时候,main.js只accept deps里的更新,不关心其它地方的更新。对于整个应用来说,除了style.css文件以外的文件更新时,没有模块接受它的热更新,没有模块去处理它热更新的结果,所以它只能刷新页面重新加载更新的文件达到代码改动后的效果。

尝试传入需要改动代码的deps,再试试看:

// hmr-test.js
export function render () {
  document.querySelector('#app').innerHTML = `
    

Hello HMR!

` } // main.js import { render } from './hmr-test' render() if (import.meta.hot) { import.meta.hot.accept(['./hmr-test']) }

️ 尝试改变hmr-test.js的代码,发现页面没有更新,这又是为什么呢?

没错,这是因为main.js只accept了hmr-test.js文件的更新,没有接受main.js自己的更新,就是说它只关心hmr-test.js的代码,可render是在main.js里面执行的,自然不会重新执行render方法。

解决办法是自己手动处理重新执行render函数:

// main.js
import { render } from './hmr-test'

render()

if (import.meta.hot) {
  import.meta.hot.accept(['./hmr-test'], ([newHmrTest]) => {
    newHmrTest.render()
  })
}

再尝试修改hmr-test.js的代码就能正常热更新啦

与webpack热更新的区别

webpack

webpack设计了整套模块代理功能,以上述离得最近的代码块为例,在main.js里import了hmr-test.js,webpack会将hmr-test.js导出的内容加工成Proxy对象,赋值到以_webpack_module_hmr-test命名的变量上,可以理解为一个代理模块。

render函数会以_webpack_module_hmr-test.render() 的方式调用,会触发代理的get方法,收集代理模块的依赖。这时当render方法代码改变webpack会将旧的代理对象替换成新的代理对象,这种替换机制是webpack模块管理的基础。

模块代理功能使得webpack HMR的能力更强大,只需更新依赖代码不需要关注引入方的代码就能完成热更新。

vite

vite最大的优势是启动本地服务速度快,所以vite选择基于es module的加载方式,它没实现模块管理的功能,也不推荐使用模块代理的方式实现HMR api,因为模块代理要给每个模块生成proxy,是个额外的开销。

vite的做法是有模块更新代码,模块的引用方会重新执行更新后的代码类似上面介绍accept使用的做法。

⚠️ 这样做会有一些问题,比如残留的老版本代码执行的结果还会在,可能会对新代码造成污染,影响页面展示。

比如在模块里写定时器的代码,修改模块的代码,触发热更新过程中会执行模块代码,定时器代码也会被重新执行一次,页面上就会新增一个定时器,而旧的定时器未被清除达不到期望的预期。

vite怎么可能容忍这样的问题发生!

插播一则介绍,模块分为两类,一类是执行代码后不会有其它影响或后续称之为pure型模块,如果有像定时器这样的代码执行完后续还有其它行为称之为side effect型模块。
上面的定时器就是所在模块的side effect。

dispose

回到正题,vite怎么可能容忍这样的问题发生!它给我们提供了dispose api,具体用法:

// hmr-test.js 的部分代码
let i = 0
const timer = setInterval(() => {
  console.log(++i)
}, 1000)

if (import.meta.hot) {
  import.meta.hot.dispose(() => {
    if (timer) clearInterval(timer)
  })
}

当前模块更新代码,执行新模块代码前会触发dispose的回调,可以在里面清除当前模块的side effect。

这样每次更新当前模块代码,都会清除旧的定时器创建新的定时器,i 也会初始化

data

️ 细心的同学会提出疑问: “诶不对啊,变量 i 明明没有改变为啥重置了”。

有时候我们确实不希望i重置,我们可以缓存在data里。

⚠️ 声明文件里介绍data属性是 readonly类型的属性,我们只能在上面修改。而且 data 属性是每个模块独有的,data们不会相互影响。

// hmr-test.js 的部分代码
let i = import.meta?.hot?.data?.cache?.getI() || 0
const timer = setInterval(() => { console.log(++i) }, 1000)

if (import.meta.hot) {
  // 触发热更新时缓存i值在闭包里
  if (import.meta.hot.data) {
    import.meta.hot.data.cache = {
      getI () {
        return i
      }
    }
  }
  import.meta.hot.dispose(() => {
    if (timer) clearInterval(timer)
  })
}

我们如果使用vue/react开启dev server触发热更新重新渲染的时候data/state里的数据也不会重置,因为热更新插件帮我们缓存好了,简单使用几乎不需要我们介入。

完成的文件代码,简单过一遍 :

// main.js
import { render } from './hmr-test'

render()

if (import.meta.hot) {
  import.meta.hot.accept(['./hmr-test'], ([newHmrTest]) => {
    newHmrTest.render()
  })
}

// hmr-test.js
export function render () {
  document.querySelector('#app').innerHTML = `
    

Hello HMR!

` } let i = import.meta?.hot?.data?.cache?.getI() || 0 const timer = setInterval(() => { console.log(++i) }, 1000) if (import.meta.hot) { // 触发热更新时缓存i值在闭包里 if (import.meta.hot.data) { import.meta.hot.data.I = i import.meta.hot.data.cache = { getI () { return i } } } import.meta.hot.dispose(() => { if (timer) clearInterval(timer) }) }

眼尖的同学会发现代码里藏着问题,在main里只干预了hmr-test模块的热更新的处理。在main里修改代码会触发默认的热更新行为,就是上面提到的刷新页面,会导致我们白缓存变量 i 的值。

这时只需在main里补充accept,覆盖触发热更新后的默认行为:

// main.js
import { render } from './hmr-test'

render()

if (import.meta.hot) {
  import.meta.hot.accept(['./hmr-test'], ([newHmrTest]) => {
    newHmrTest.render()
  })

  import.meta.hot.accept(() => {})
}

decline

阻止了默认的刷新行为,如果想在某个地方想让页面强制刷新可以使用decline方法。

⚠️ decline的优先级比accept要低,如果对模块进行了accept,decline会失效。

invalidate

因为decline受优先级影响,不太好用,所以如果想在accept里强制浏览器刷新可以使用invalidate

// hmr-test.js 的部分代码

// 将 i 挂载到hmr-test模块里
export let i = import.meta?.hot?.data?.cache?.getI() || 0

// main.js 的部分代码
if (import.meta.hot) {
  import.meta.hot.accept(['./hmr-test'], ([newHmrTest]) => {
    //  当i > 10刷新页面
    if (newHmrTest.i > 0) import.meta.hot.invalidate()
    else newHmrTest.render()
  })

  import.meta.hot.accept(() => {})
}

这些api其实在日常开发中很少用到,在vite/webpack的插件里编译vue/react框架的模块代码时,就会集成这部分代码。

jsx相关

vite没有默认支持jsx语法,需要依赖插件进行语法解析。

@vitejs/plugin-vue-jsx

本地开发过程中,vite对react和vue中jsx代码的处理有很大的区别。
react:vite在react中编译jsx格式的代码输出还是以jsx的形式,因为开发阶段使用esbuild,esbuild原生支持jsx语法,最终编译成createElement的js代码。
vue:vite在vue中编译jsx格式的代码输出的是纯js代码,会将jsx的代码编译成createVNode的形式,虚拟dom的写法,vue3不使用esbuild的原因是esbuild只支持最简单的jsx语法,不能满足vue3的使用,vue3对jsx编译出来的createElement里有更多的参数,有其他更多的使用方式和优化的方法。所以vue选择使用babel来编译jsx的代码,但不能完全支持,毕竟jsx是react的官方语法,vue需要vitejs/plugin-vue-jsx插件来定制专属的jsx语法,插件会将vue中jsx代码直接编译成js。

@vue/babel-plugin-jsx

安装插件
npm install @vue/babel-plugin-jsx -D

配置 Babel

// .babelrc
{
  "plugins": ["@vue/babel-plugin-jsx"]
}

参数

transformOn

Type: boolean Default: false

on: { click: xx } 转成 onClick: xxx

optimize

Type: boolean

Default: false

是否开启优化. 如果你对 Vue 3 不太熟悉,不建议打开,否则打包时可能会有些奇怪的问题

isCustomElement

Type: (tag: string) => boolean

Default: undefined

自定义元素

mergeProps

Type: boolean

Default: true

合并 class / style / onXXX handlers. 不开启遇到重复的props后面的会覆盖前面的

enableObjectSlots

使用对象插槽,简化插槽代码。虽然在 JSX 中比较好使,但是会增加一些 _isSlot 的运行时条件判断,这会增加你的项目体积。即使你关闭了 enableObjectSlotsv-slots 还是可以使用

pragma

Type: string

Default: createVNode

替换编译 JSX 表达式的时候使用的函数,也算是为用户留了一个编译前的窗口。

你可能感兴趣的:(vite HMR api)