先说明一下:Vue 3.3在本文写作时(2022年4月)尚未发布(Vue 3.3于2023年5月发布)。本文基于Vue 3.2的实际使用经验,分享Vue 3 Composition API的最佳实践。这些经验也适用于未来的Vue 3.3版本,帮你写出更优雅、更易维护的Vue代码。

我用Vue 3做项目有一年多了,从Options API迁移到Composition API,踩了不少坑,也总结了一些经验。今天分享我在实际项目中总结的Vue 3最佳实践,包括组件设计、状态管理、性能优化、代码组织,以及一些常见的坑。

一、为什么用Composition API

先说说为什么要用Composition API。

Vue 2的Options API,把代码按选项组织:data、methods、computed、watch。小组件这样写没问题,但大组件就很痛苦。一个功能的逻辑分散在各个选项里,要来回跳着看,很难维护。

Composition API把相关的逻辑组织在一起。一个功能的状态、计算属性、方法都写在一起,代码内聚性高,可读性好,也方便复用。

Composition API的优势:

  1. 更好的逻辑复用:用组合式函数(composables)提取公共逻辑
  2. 更好的代码组织:相关逻辑放在一起
  3. 更好的类型推导:TypeScript支持更好
  4. 更小的打包体积:tree-shaking更好

如果你还在用Options API,建议迁移到Composition API。尤其是<script setup>语法糖,写起来非常简洁。

二、组件设计最佳实践

1. 组件要小而精

一个组件不要太大。如果一个组件超过300行,就该考虑拆分了。

拆分的原则:

  • 按功能拆分:每个功能一个组件
  • 按职责拆分:展示组件和业务组件分开
  • 按复用性拆分:可复用的部分抽成公共组件

小组件更容易理解、测试、复用。大组件维护起来很痛苦。

2. props要明确

props要定义清楚类型、默认值、是否必填。

<script setup>
const props = defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  },
  disabled: {
    type: Boolean,
    default: false
  }
})
</script>

不要用数组形式的props(props: ['title', 'count']),没有类型检查,容易出bug。

3. 事件要命名清晰

事件名用kebab-case,语义清晰。

<script setup>
const emit = defineEmits(['update:modelValue', 'delete', 'confirm'])
</script>

支持v-model的组件,用update:modelValue事件。多个v-model用update:title这样的形式。

4. 不要直接修改props

props是只读的,不要在子组件里直接修改。需要修改时,用emit通知父组件。

错误的做法:

props.title = 'new title' // 错误,props是只读的

正确的做法:

emit('update:title', 'new title')

5. 用插槽提高灵活性

对于需要自定义内容的组件,用插槽(slot),而不是传一大堆props。

<!-- 子组件 -->
<template>
  <div class="card">
    <slot name="header"></slot>
    <slot></slot>
    <slot name="footer"></slot>
  </div>
</template>

<!-- 父组件 -->
<Card>
  <template #header>
    <h2>标题</h2>
  </template>
  <p>内容</p>
  <template #footer>
    <button>确定</button>
  </template>
</Card>

插槽比props更灵活,能适应更多场景。

三、Composition API最佳实践

1. 用<script setup>

<script setup>是Vue 3.2引入的语法糖,写Composition API非常简洁。

<script setup>
import { ref, computed } from 'vue'

const count = ref(0)
const double = computed(() => count.value * 2)

function increment() {
  count.value++
}
</script>

不用写setup()函数,不用return,顶层的变量和函数直接在模板里用。

现在Vue 3的官方推荐就是<script setup>,新项目直接用它。

2. ref和reactive的选择

  • ref:用于基本类型(字符串、数字、布尔值),也可以用于对象
  • reactive:用于对象类型

简单的原则:基本类型用ref,对象用reactive。

但实际上,ref也可以用于对象,而且更灵活。我个人习惯全部用ref,统一风格。

注意:ref在script里要通过.value访问,在模板里自动解包,不用.value

3. 组合式函数(composables)

把可复用的逻辑抽成组合式函数,以use开头。

// useCounter.js
import { ref } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  function increment() {
    count.value++
  }
  
  function decrement() {
    count.value--
  }
  
  function reset() {
    count.value = initialValue
  }
  
  return {
    count,
    increment,
    decrement,
    reset
  }
}

在组件里使用:

<script setup>
import { useCounter } from './useCounter'

const { count, increment, decrement } = useCounter(0)
</script>

组合式函数是Composition API最大的优势,能大大提高代码复用性。常见的composables:useRequest(请求)、useDebounce(防抖)、useLocalStorage(本地存储)、useMouse(鼠标位置)等。

4. 计算属性要纯

计算属性(computed)应该是纯函数,不要有副作用。

错误的做法:

const fullName = computed(() => {
  console.log('computed') // 副作用,不应该
  return firstName.value + ' ' + lastName.value
})

计算属性只负责计算和返回值,不要修改其他状态,不要发请求,不要操作DOM。

5. watch的使用

watch用于监听数据变化,执行副作用。

// 监听一个ref
watch(count, (newVal, oldVal) => {
  console.log(`count changed from ${oldVal} to ${newVal}`)
})

// 监听多个源
watch([count, name], ([newCount, newName]) => {
  // ...
})

// 深度监听
watch(
  () => state.user,
  (newUser) => {
    // ...
  },
  { deep: true }
)

// 立即执行
watch(
  count,
  (newVal) => {
    // ...
  },
  { immediate: true }
)

注意:

  • 不要过度使用watch,很多时候用computed更合适
  • 监听对象时,用函数返回属性,而不是直接监听整个对象
  • 清理副作用:在watch里返回一个清理函数

6. 生命周期钩子

<script setup>里,生命周期钩子用函数形式:

import { onMounted, onUnmounted, onUpdated } from 'vue'

onMounted(() => {
  console.log('mounted')
})

onUpdated(() => {
  console.log('updated')
})

onUnmounted(() => {
  console.log('unmounted')
})

注意:beforeCreatecreated<script setup>里不需要,因为setup本身就相当于这两个阶段。

四、状态管理

1. 小项目用provide/inject

如果项目不大,不需要Vuex/Pinia,可以用provide/inject做状态管理。

// 父组件
import { provide, ref } from 'vue'

const user = ref({ name: '张三' })
provide('user', user)

// 子组件
import { inject } from 'vue'

const user = inject('user')

但provide/inject适合简单的状态共享。复杂的状态管理,还是用Pinia。

2. 用Pinia,不用Vuex

Vue 3的官方状态管理库是Pinia,Vuex已经不推荐了。

Pinia的优势:

  • 更简洁的API
  • 更好的TypeScript支持
  • 没有mutations,直接修改状态
  • 支持组合式风格
// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', () => {
  const name = ref('张三')
  const age = ref(25)
  
  const fullInfo = computed(() => `${name.value},${age.value}岁`)
  
  function setName(newName) {
    name.value = newName
  }
  
  return {
    name,
    age,
    fullInfo,
    setName
  }
})

在组件里使用:

<script setup>
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
</script>

Pinia比Vuex简单很多,新项目直接用Pinia。

3. 状态要按需共享

不要把所有状态都放到全局store里。只有多个组件共享的状态才放store,组件内部的状态用组件自己的ref。

放太多状态到store,会导致store臃肿,调试困难。

五、性能优化

1. 用v-once

不需要更新的内容,用v-once标记,只渲染一次。

<p v-once>这段内容只渲染一次,不会更新</p>

2. 用v-memo

Vue 3.2新增的v-memo,可以缓存组件/元素的渲染结果,依赖不变就跳过渲染。

<div v-memo="[item.id, item.name]">
  <!-- 只有item.id或item.name变化时才重新渲染 -->
</div>

适合大列表的性能优化。

3. 合理使用key

v-for一定要加key,而且用稳定的唯一标识(比如id),不要用index。

<!-- 好 -->
<li v-for="item in list" :key="item.id">{{ item.name }}</li>

<!-- 不好 -->
<li v-for="(item, index) in list" :key="index">{{ item.name }}</li>

用index当key,在列表排序或删除时会出问题。

4. 异步组件

大组件用defineAsyncComponent异步加载,减少首屏体积。

import { defineAsyncComponent } from 'vue'

const BigComponent = defineAsyncComponent(() => import('./BigComponent.vue'))

配合路由懒加载,效果更好。

5. 虚拟列表

长列表(几百上千条)用虚拟列表,只渲染可视区域的内容。

可以用vue-virtual-scroller等库,不要自己造轮子。

6. 避免不必要的响应式

不需要响应式的数据,不要用ref/reactive。比如常量、配置项,直接用普通变量。

// 好
const MAX_COUNT = 100 // 常量,不需要响应式

// 不好
const maxCount = ref(100) // 不需要响应式却用了ref

7. shallowRef和shallowReactive

大对象如果只需要顶层响应式,用shallowRefshallowReactive,减少响应式开销。

import { shallowRef } from 'vue'

const bigData = shallowRef({ /* 很大的对象 */ })

修改时需要整体替换,不能直接修改嵌套属性。

六、代码组织

1. 目录结构

推荐的目录结构:

src/
├── components/      # 公共组件
├── views/           # 页面组件
├── composables/     # 组合式函数
├── stores/          # Pinia stores
├── router/          # 路由
├── utils/           # 工具函数
├── api/             # API请求
├── assets/          # 静态资源
└── App.vue

2. 组件命名

  • 组件名用PascalCase:UserProfile.vue
  • 多词命名,避免和HTML标签冲突:UserProfile而不是Profile
  • 基础组件加前缀:BaseButton.vueAppModal.vue

3. 组合式函数命名

  • use开头:useRequest.jsuseDebounce.js
  • 放在composables/目录下

4. 一个文件一个组件

每个组件一个.vue文件,不要在一个文件里写多个组件。

七、常见的坑

说说我踩过的坑。

坑一:忘记.value

在script里访问ref的值,要加.value。新手经常忘。

const count = ref(0)

console.log(count) // 输出的是ref对象,不是0
console.log(count.value) // 输出0,正确

在模板里不用加.value,自动解包。

坑二:直接修改props

props是只读的,直接修改会报警告。需要修改时用emit。

坑三:watch里修改监听的源

在watch里修改自己监听的源,会导致无限循环。

// 错误,会无限循环
watch(count, () => {
  count.value++
})

坑四:v-if和v-for一起用

不要在同一个元素上同时用v-if和v-for。v-for的优先级比v-if高,会先循环再判断,浪费性能。

<!-- 不好 -->
<li v-for="item in list" v-if="item.active">{{ item.name }}</li>

<!-- 好,先用computed过滤 -->
<li v-for="item in activeList">{{ item.name }}</li>

坑五:响应式丢失

解构reactive对象会丢失响应式。

const state = reactive({ name: '张三', age: 25 })

const { name } = state // name丢失了响应式

需要用toRefs

const { name } = toRefs(state) // name保持响应式

坑六:在setup里用this

Composition API里没有this。不要用this.$emitthis.$refs,用defineEmitsref代替。

八、TypeScript支持

Vue 3对TypeScript的支持很好,建议用TypeScript。

<script setup lang="ts">
import { ref, computed } from 'vue'

interface User {
  name: string
  age: number
}

const user = ref<User>({ name: '张三', age: 25 })

const isAdult = computed(() => user.value.age >= 18)
</script>

props和emit也可以用类型:

const props = defineProps<{
  title: string
  count?: number
}>()

const emit = defineEmits<{
  (e: 'update:title', value: string): void
  (e: 'delete'): void
}>()

TypeScript能在编译时发现很多错误,大大提高代码质量。

九、工具推荐

推荐一些Vue 3的开发工具:

  1. Vite:构建工具,比Vue CLI快很多
  2. Vue Router 4:Vue 3的路由
  3. Pinia:状态管理
  4. VueUse:常用的组合式函数集合
  5. Element Plus:Vue 3的UI组件库
  6. Volar:VS Code的Vue 3插件(比Vetur好)
  7. Vue DevTools:浏览器调试工具

十、写在最后

Vue 3的Composition API,让前端开发变得更优雅、更灵活。但要用好它,需要遵循一些最佳实践,避免踩坑。

核心原则:

  1. 组件小而精
  2. 逻辑用组合式函数复用
  3. 状态管理用Pinia
  4. 注意性能优化
  5. 用TypeScript

至于未来的Vue 3.3,预计会在TypeScript支持、宏函数、开发体验等方面有提升。但核心的Composition API用法不会变,这些最佳实践依然适用。

2022年了,Vue 3已经很成熟了,生态也完善了。新项目直接上Vue 3 + Vite + Pinia + TypeScript,体验很好。

最后,用一句话总结:"Vue 3的最佳实践,本质上就是让代码更清晰、更易维护、更易复用。"

祝大家都能写出优雅的Vue代码。