最近把项目的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的使用经验或者问题,欢迎交流讨论。