最近把项目的Vue版本从3.2升级到了3.4,用了一段时间之后,发现3.4带来了很多实用的新特性,能让代码更简洁、性能更好。
Vue 3.4是2023年底发布的,带来了defineModel、useTemplateRef、v-bind同名简写、响应式系统重构等重要更新。这些新特性看起来简单,但要用好、用对,还是需要一些实践经验的。
这篇文章就来分享一下我在Vue 3.4+项目中总结的最佳实践,结合实际项目中的经验,聊聊怎么用好这些新特性,写出更优雅、更高效的Vue代码。
升级前的准备
先说升级前的准备工作。
第一,确认依赖兼容性。升级Vue之前,要确认项目中使用的UI组件库(Element Plus、Ant Design Vue、Vuetify等)、状态管理库(Pinia)、路由(Vue Router)等都支持Vue 3.4。大部分主流库都已经支持了,但一些比较老的或者小众的库可能还没适配,需要检查一下。
第二,阅读更新日志。Vue 3.4的更新日志在官方GitHub上,列出了所有的新特性、破坏性变更和bug修复。特别是破坏性变更,要仔细看,确认会不会影响你的项目。3.4的破坏性变更不多,主要是一些全局API的调整和类型定义的变化。
第三,做好版本控制。升级之前先commit或者建一个分支,这样如果升级出问题,可以随时回退。不要直接在主分支上升级。
第四,逐步升级。如果项目比较大,可以先在一个分支上升级,跑通测试,确认没有问题之后再合并到主分支。不要一次性升级所有东西,先升级Vue,再升级相关的依赖,逐步验证。
升级的命令很简单:
npm install vue@latest vue-router@latest pinia@latest升级之后,先跑一遍项目,看看有没有报错,然后逐步使用新特性重构代码。
defineModel:双向绑定的正确姿势
defineModel是Vue 3.4最重要的新特性之一,它简化了组件之间的双向绑定。
在3.4之前,实现一个支持v-model的组件,需要这样写:
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
function updateValue(e) {
emit('update:modelValue', e.target.value)
}
</script>
<template>
<input :value="modelValue" @input="updateValue" />
</template>用了defineModel之后,就简单多了:
<script setup>
const model = defineModel()
</script>
<template>
<input v-model="model" />
</template>defineModel返回一个ref,直接用v-model绑定到原生元素上就行,不需要手动定义props和emit,也不需要写更新函数。
如果需要多个v-model(比如v-model:title和v-model:content),可以给defineModel传一个参数:
<script setup>
const title = defineModel('title')
const content = defineModel('content')
</script>最佳实践:
- 优先用defineModel替代以前的props + emit模式,代码更简洁。
- defineModel返回的是ref,在script里要通过.value访问,在template里直接用。
- 可以给defineModel传第二个参数,设置类型和默认值:
const model = defineModel({ type: String, default: '' })。 - 注意defineModel是宏,不需要导入,直接用就行。
- 如果需要在更新前做验证或转换,可以用defineModel的get/set选项:
<script setup>
const model = defineModel({
get(value) {
return value
},
set(value) {
return value.trim()
}
})
</script>useTemplateRef:模板引用的新方式
useTemplateRef是Vue 3.4新增的API,用来获取模板引用(template ref),替代以前的ref + 模板ref的方式。
在3.4之前,获取模板引用是这样的:
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
inputRef.value.focus()
})
</script>
<template>
<input ref="inputRef" />
</template>用了useTemplateRef之后:
<script setup>
import { useTemplateRef, onMounted } from 'vue'
const inputRef = useTemplateRef('input')
onMounted(() => {
inputRef.value.focus()
})
</script>
<template>
<input ref="input" />
</template>看起来差别不大,但useTemplateRef有一个重要的优势:它的类型是正确的。以前用ref(null)的时候,TypeScript的类型是Ref<null>,访问.value的时候会报类型错误,需要手动指定类型。useTemplateRef会自动根据模板中的元素类型推断,类型更安全。
最佳实践:
- 新项目优先用useTemplateRef,类型更安全,语义更清晰。
- 注意useTemplateRef的参数是模板中ref属性的名字,是一个字符串,不是变量名。
- useTemplateRef返回的ref是只读的,不能手动赋值,只能在模板中通过ref属性绑定。
- 对于v-for中的ref,useTemplateRef不太适用,还是用函数式ref比较好。
v-bind同名简写
Vue 3.4新增了v-bind的同名简写语法。当属性名和变量名相同时,可以只写属性名,不用写:prop="prop"。
以前的写法:
<script setup>
const title = ref('Hello')
const content = ref('World')
</script>
<template>
<MyComponent :title="title" :content="content" />
</template>3.4之后可以简写为:
<template>
<MyComponent :title :content />
</template>这个简写和ES6对象的简写语法很像,能让模板更简洁。
最佳实践:
- 变量名和属性名相同时,优先用简写,代码更简洁。
- 注意简写只适用于v-bind(:),不适用于v-on(@)。
- 团队协作时,要确保大家都熟悉这个语法,避免看不懂。
- 不要为了简写而强行改变变量名,保持变量名的语义清晰更重要。
响应式系统优化
Vue 3.4对响应式系统做了重构,主要是优化了effect的调度和依赖收集,性能更好了。
其中一个重要的变化是,reactive的解构不再丢失响应性了。在3.4之前,解构reactive对象会丢失响应性:
const state = reactive({ count: 0 })
let { count } = state // 3.4之前:count不是响应式的
count++ // 不会触发更新3.4之后,解构reactive对象会自动用toRef包裹,保持响应性:
const state = reactive({ count: 0 })
let { count } = state // 3.4之后:count是ref,保持响应性
count.value++ // 会触发更新不过这个特性需要注意,解构出来的是ref,需要用.value访问,在template里可以直接用。
最佳实践:
- 3.4之后可以放心地解构reactive对象了,但要注意解构出来的是ref,需要用.value。
- 如果你不确定项目的Vue版本,还是用toRefs比较稳妥,兼容性更好。
- 响应式系统的性能优化是自动的,不需要做额外的配置,但可以利用新特性写出更简洁的代码。
性能优化最佳实践
除了新特性,Vue 3.4项目还有一些通用的性能优化最佳实践。
第一,合理使用computed。computed会缓存结果,只有依赖变化时才重新计算。对于复杂的计算逻辑,一定要用computed,不要在template里写复杂的表达式。
<script setup>
// 好的做法
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// 不好的做法
// <template>{{ firstName + ' ' + lastName }}</template>
</script>第二,用v-for的时候一定要加key,而且key要用稳定的唯一标识(比如id),不要用index。用index作为key,在列表变化的时候会导致不必要的重渲染。
第三,合理使用v-show和v-if。频繁切换的用v-show(只是隐藏,不销毁),条件很少变化的用v-if(销毁和重建,节省初始渲染时间)。
第四,用shallowRef和shallowReactive优化大对象的响应式。对于一些很大的对象(比如地图实例、复杂的配置),如果不需要深度响应式,可以用shallowRef或shallowReactive,减少响应式系统的开销。
第五,组件懒加载。对于不常用的组件(比如弹窗、抽屉),用defineAsyncComponent异步加载,减少初始包体积。
const Modal = defineAsyncComponent(() => import('./Modal.vue'))第六,合理使用keep-alive。对于需要缓存状态的组件(比如列表页,从详情页返回时保持滚动位置),用keep-alive缓存。但不要滥用,缓存太多组件会占用内存。
第七,避免在template里调用函数。每次渲染都会重新调用函数,影响性能。如果需要计算值,用computed。
代码组织最佳实践
Vue 3.4项目的代码组织,也有一些最佳实践。
第一,用Composition API + script setup。这是Vue 3的推荐写法,代码更简洁,逻辑复用更方便。Options API虽然还支持,但新项目不建议用了。
第二,按功能组织代码,而不是按类型。以前我们习惯把代码分成components、store、utils、assets等目录,现在更推荐按功能模块组织,比如user模块、order模块,每个模块里包含自己的组件、逻辑、类型。这样找代码更方便,模块之间的界限更清晰。
第三,组合式函数(Composables)复用逻辑。把可复用的逻辑抽成composables,比如useUser、useCart、usePagination。composables用use开头,返回响应式数据和方法。
// usePagination.js
export function usePagination(fetchData) {
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const list = ref([])
async function load() {
const res = await fetchData(page.value, pageSize.value)
list.value = res.data
total.value = res.total
}
return { page, pageSize, total, list, load }
}第四,类型定义集中管理。用TypeScript的话,把类型定义放在单独的types目录里,或者和composables放在一起。保持类型的一致性和可维护性。
第五,组件命名规范。组件名用PascalCase,多单词(避免和HTML元素冲突)。文件名和组件名保持一致。props和emits要明确定义,不要用隐式的。
状态管理:Pinia最佳实践
Vue 3.4项目推荐用Pinia做状态管理,Vuex已经不推荐了。
Pinia的最佳实践:
- 用setup语法定义store,和Composition API风格一致:
export const useUserStore = defineStore('user', () => {
const user = ref(null)
const token = ref('')
function setUser(u) {
user.value = u
}
return { user, token, setUser }
})- 按模块划分store,不要把所有状态都放在一个store里。比如user store、cart store、order store,每个store管自己的领域。
- 异步操作放在actions里,不要在组件里直接做复杂的异步逻辑。
- 用storeToRefs解构store,保持响应性:
const { user, token } = storeToRefs(userStore)
const { setUser } = userStore- 持久化用pinia-plugin-persistedstate插件,简单好用。
路由最佳实践
Vue Router 4配合Vue 3.4,有一些最佳实践。
第一,路由懒加载。每个路由对应的组件都用动态import,减少初始包体积:
const Home = () => import('./views/Home.vue')第二,路由守卫做权限控制。用beforeEach守卫检查登录状态和权限,未登录的跳转到登录页,没权限的跳转到403页。
第三,路由元信息(meta)存页面配置。比如页面标题、是否需要登录、权限标识等,都可以放在meta里,在守卫和组件里读取。
第四,用命名路由而不是path跳转。用name跳转,改path的时候不用改所有跳转的地方。
第五,滚动行为优化。配置scrollBehavior,切换路由时滚动到顶部,或者保持滚动位置。
测试最佳实践
测试是保证代码质量的重要手段,Vue 3.4项目推荐用Vitest + Vue Test Utils。
第一,单元测试composables。composables是纯逻辑,最容易测试,覆盖率要高。
第二,组件测试测行为不测实现。从用户的角度测试组件,比如点击按钮之后发生了什么,而不是测试内部的state和方法。
第三,用Vue Test Utils的mount和shallowMount。mount渲染完整的组件树,shallowMount只渲染当前组件,子组件用桩代替。简单组件用mount,复杂组件用shallowMount。
第四,测试异步行为用async/await。Vue的更新是异步的,测试的时候要await nextTick()或者await相关的操作。
第五,端到端测试用Playwright或Cypress,覆盖关键的用户流程。
一些常见的坑
最后说一下Vue 3.4项目中常见的坑。
第一个坑:defineModel和v-model的参数名不一致。父组件用v-model:title,子组件用defineModel('title'),名字要完全一致,否则不会生效。
第二个坑:useTemplateRef的参数写错。useTemplateRef的参数是模板中ref属性的字符串名字,不是变量名。经常有人写成useTemplateRef(inputRef),应该是useTemplateRef('input')。
第三坑:解构reactive之后忘记用.value。3.4之后解构reactive会保持响应性,但解构出来的是ref,在script里要用.value访问,很多人忘记加.value,导致不生效。
第四个坑:v-for的key用index。列表变化的时候,用index作为key会导致组件状态错乱,特别是有表单输入的列表。一定要用唯一的id作为key。
第五个坑:在setup里直接访问props。props是响应式的,但直接解构会丢失响应性(3.4之前),要用toRefs或者直接用props.xxx访问。
第六个坑:watch的immediate和deep。watch默认不是深度监听的,如果监听的是对象,需要加deep: true。如果需要立即执行,加immediate: true。
写在最后
Vue 3.4带来了很多实用的新特性,defineModel让双向绑定更简单,useTemplateRef让模板引用类型更安全,v-bind简写让模板更简洁,响应式系统的优化让性能更好。
但新特性只是工具,真正重要的是写出高质量、可维护的代码。合理使用新特性,配合良好的代码组织、性能优化、测试,才能发挥Vue 3.4的最大价值。
这篇文章分享了我在Vue 3.4+项目中总结的最佳实践,包括新特性的用法、性能优化、代码组织、状态管理、路由、测试和常见的坑。希望能给正在用Vue 3.4的朋友一些参考。
Vue还在不断发展,新特性和最佳实践也在不断更新。保持学习,关注官方更新,在实践中总结经验,才能写出越来越好的Vue代码。
如果你有什么Vue 3.4的使用经验或者问题,欢迎交流讨论。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录