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<ModuleNamespace | undefined>) => 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<T extends string>(
    event: T,
    cb: (payload: InferCustomEventPayload<T>) => void
  ): void
  send<T extends string>(event: T, data?: InferCustomEventPayload<T>): 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 = `
    <div>
      <h1>Hello HMR!</h1>
    </div>
  `
}
// 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 = `
    <div>
      <h1>Hello HMR!</h1>
    </div>
  `
}

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 表达式的时候使用的函数,也算是为用户留了一个编译前的窗口。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容