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
的运行时条件判断,这会增加你的项目体积。即使你关闭了 enableObjectSlots
,v-slots
还是可以使用
pragma
Type: string
Default: createVNode
替换编译 JSX 表达式的时候使用的函数,也算是为用户留了一个编译前的窗口。