Vue3 组合式 API 最佳实践
Vue3 的组合式 API(Composition API)是一种全新的编写 Vue 组件的方式,它让我们可以更灵活地组织组件逻辑。本文将探讨组合式 API 的最佳实践和常见模式,帮助你更高效地构建 Vue 应用。
为什么使用组合式 API?
在开始之前,让我们先了解为什么 Vue3 引入了组合式 API:
- 更好的逻辑复用 - 与混入(mixins)相比,组合式 API 提供了更清晰的逻辑复用机制
- 更灵活的代码组织 - 按功能/关注点组织代码,而不是选项类型
- 更好的类型推导 - 对 TypeScript 的支持更加友好
- 更小的打包体积 - 更好的 Tree-shaking 支持
组合式 API 基础
如果你刚接触组合式 API,以下是一个基本的使用示例:
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 响应式状态
const count = ref(0)
// 方法
function increment() {
count.value++
}
</script>
这个简单的例子展示了组合式 API 的基本用法,使用 ref
创建响应式变量,并在模板中直接使用它。
组织组件代码的最佳实践
1. 使用 <script setup>
语法
<script setup>
是组合式 API 的语法糖,它简化了组件的编写:
<script setup>
// 导入的组件自动注册
import ChildComponent from './ChildComponent.vue'
// 导出的变量自动暴露给模板
const message = 'Hello World'
// 定义的函数自动暴露给模板
function handleClick() {
console.log('Clicked!')
}
</script>
<template>
<div>
{{ message }}
<button @click="handleClick">Click me</button>
<ChildComponent />
</div>
</template>
<script setup>
的优势:
- 更少的样板代码
- 能够使用纯 TypeScript 编写组件逻辑
- 更好的运行时性能
- 更好的 IDE 类型推断
2. 按功能组织代码
组合式 API 的一个主要优势是可以按功能组织代码,而不是按选项类型:
<script setup>
import { ref, computed, onMounted } from 'vue'
import { fetchUserData } from '@/api/user'
// 用户功能
const userId = ref(1)
const userData = ref(null)
const userError = ref(null)
const isLoading = ref(false)
async function loadUser() {
isLoading.value = true
try {
userData.value = await fetchUserData(userId.value)
} catch (error) {
userError.value = error
} finally {
isLoading.value = false
}
}
// 计数功能
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
// 生命周期
onMounted(() => {
loadUser()
})
</script>
3. 使用组合函数(Composables)提取和重用逻辑
组合函数是组合式 API 最强大的功能之一,它让你能够提取和重用逻辑:
// useUser.js
import { ref } from 'vue'
import { fetchUserData } from '@/api/user'
export function useUser(initialUserId = 1) {
const userId = ref(initialUserId)
const userData = ref(null)
const userError = ref(null)
const isLoading = ref(false)
async function loadUser() {
isLoading.value = true
try {
userData.value = await fetchUserData(userId.value)
} catch (error) {
userError.value = error
} finally {
isLoading.value = false
}
}
return {
userId,
userData,
userError,
isLoading,
loadUser
}
}
然后在组件中使用:
<script setup>
import { useUser } from '@/composables/useUser'
const { userId, userData, userError, isLoading, loadUser } = useUser()
// 使用解构返回的变量和方法
</script>
4. 使用 provide
和 inject
管理跨层级的状态
当你需要在多个组件之间共享状态时,provide
和 inject
是比 props 更好的选择:
<!-- ParentComponent.vue -->
<script setup>
import { provide, readonly, ref } from 'vue'
const theme = ref('light')
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
// 提供只读值防止子组件修改
provide('theme', readonly(theme))
// 提供方法让子组件可以修改值
provide('toggleTheme', toggleTheme)
</script>
<!-- ChildComponent.vue -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
const toggleTheme = inject('toggleTheme')
</script>
5. 使用 defineProps
和 defineEmits
处理组件通信
在 <script setup>
中,使用 defineProps
和 defineEmits
替代 props
和 emits
选项:
<script setup>
const props = defineProps({
modelValue: {
type: String,
required: true
},
placeholder: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
function updateValue(event) {
emit('update:modelValue', event.target.value)
}
</script>
<template>
<input
:value="modelValue"
:placeholder="placeholder"
@input="updateValue"
@focus="emit('focus', $event)"
@blur="emit('blur', $event)"
/>
</template>
6. 使用 TypeScript 增强类型安全
组合式 API 对 TypeScript 有很好的支持:
<script setup lang="ts">
import { ref, computed } from 'vue'
interface User {
id: number
name: string
email: string
}
const user = ref<User | null>(null)
const props = defineProps<{
initialCount: number
step?: number
}>()
const count = ref(props.initialCount)
const step = computed(() => props.step ?? 1)
function increment() {
count.value += step.value
}
// 使用 defineEmits 的类型版本
const emit = defineEmits<{
(e: 'change', count: number): void
(e: 'reset'): void
}>()
function reset() {
count.value = props.initialCount
emit('reset')
}
</script>
高级模式和技巧
异步组件和加载状态管理
<script setup>
import { ref, onMounted } from 'vue'
const data = ref(null)
const error = ref(null)
const isLoading = ref(false)
async function fetchData() {
isLoading.value = true
try {
const response = await fetch('https://api.example.com/data')
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
isLoading.value = false
}
}
onMounted(fetchData)
</script>
<template>
<div>
<div v-if="isLoading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else-if="data">
<!-- 渲染数据 -->
<pre>{{ data }}</pre>
</div>
<div v-else>No data</div>
</div>
</template>
这种模式可以进一步提取为可复用的组合函数:
// useAsync.js
import { ref } from 'vue'
export function useAsync(asyncFunction) {
const data = ref(null)
const error = ref(null)
const isLoading = ref(false)
async function execute(...args) {
isLoading.value = true
data.value = null
error.value = null
try {
data.value = await asyncFunction(...args)
} catch (e) {
error.value = e
} finally {
isLoading.value = false
}
}
return {
data,
error,
isLoading,
execute
}
}
响应式状态管理
对于简单的状态管理,可以使用 reactive
和 provide
/inject
:
// store.js
import { reactive, readonly } from 'vue'
const state = reactive({
count: 0,
users: []
})
// 只导出只读状态,防止直接修改
export const useStore = () => {
const increment = () => {
state.count++
}
const decrement = () => {
state.count--
}
const addUser = (user) => {
state.users.push(user)
}
return {
state: readonly(state),
increment,
decrement,
addUser
}
}
<!-- App.vue -->
<script setup>
import { provide } from 'vue'
import { useStore } from './store'
const store = useStore()
provide('store', store)
</script>
<!-- ChildComponent.vue -->
<script setup>
import { inject } from 'vue'
const { state, increment } = inject('store')
</script>
<template>
<div>
<p>Count: {{ state.count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
使用 watchEffect
处理副作用
watchEffect
是一个强大的 API,适合处理副作用:
<script setup>
import { ref, watchEffect } from 'vue'
const userId = ref(1)
const userData = ref(null)
watchEffect(async () => {
// 会自动追踪 userId.value 的依赖
userData.value = await fetch(`/api/users/${userId.value}`)
.then(r => r.json())
})
// 更改 userId 将自动触发 watchEffect 重新执行
function nextUser() {
userId.value++
}
</script>
使用 toRefs
解构保持响应性
当你需要解构一个响应式对象但又想保持响应性时,toRefs
是最佳选择:
<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({
name: 'John',
age: 30
})
// 解构后 name 和 age 仍然是响应式的
const { name, age } = toRefs(state)
function updateName(newName) {
name.value = newName // 会更新 state.name
}
</script>
使用 shallowRef
和 shallowReactive
优化性能
对于大对象或仅需要跟踪引用变化的场景,可以使用浅层响应式 API:
<script setup>
import { shallowRef, shallowReactive } from 'vue'
// 只有 .value 的变化会被追踪,不会深度追踪对象属性
const userData = shallowRef({ name: 'John', age: 30 })
// 只有对象本身的属性变化会被追踪,不会追踪嵌套对象
const options = shallowReactive({
theme: 'dark',
settings: { notification: true }
})
// 更改 .value 会触发更新
userData.value = { name: 'Jane', age: 28 }
// 更改顶层属性会触发更新
options.theme = 'light'
// 更改嵌套属性不会触发更新
options.settings.notification = false
</script>
常见陷阱和解决方案
1. 引用响应式变量时忘记 .value
const count = ref(0)
// 错误 - 不会改变响应式状态
function increment() {
count++ // 应该是 count.value++
}
// 正确
function increment() {
count.value++
}
2. 解构响应式对象导致丢失响应性
const state = reactive({ count: 0 })
// 错误 - count 不再是响应式的
const { count } = state
// 正确 - 使用 toRefs
const { count } = toRefs(state)
// 或者直接在模板中访问
// <div>{{ state.count }}</div>
3. 初始化 ref
时值类型不匹配
// 错误 - 后续将字符串赋值给数字 ref 会导致意外行为
const count = ref(0)
count.value = '1' // 不会报错,但可能导致问题
// 正确 - 使用 TypeScript
const count = ref<number>(0)
count.value = '1' // TypeScript 会报错
4. 组合函数内部使用生命周期钩子
// useUser.js - 错误
import { ref, onMounted } from 'vue'
export function useUser() {
const user = ref(null)
// 错误 - 生命周期钩子应该在 setup 或 <script setup> 中直接调用
onMounted(() => {
// 获取用户
})
return { user }
}
// 正确 - 返回一个可以在组件中调用的初始化函数
export function useUser() {
const user = ref(null)
function loadUser() {
// 获取用户
}
return {
user,
loadUser // 组件可以在 onMounted 中调用
}
}
性能优化
1. 使用 computed
缓存计算结果
<script setup>
import { ref, computed } from 'vue'
const items = ref([1, 2, 3, 4, 5])
// 不好 - 每次重新渲染都会重新计算
const doubledBad = () => items.value.map(item => item * 2)
// 好 - 只有当 items 变化时才会重新计算
const doubledGood = computed(() => items.value.map(item => item * 2))
</script>
2. 使用 v-memo
减少不必要的组件重新渲染
<template>
<div>
<!-- 只有当 item.id 变化时才会重新渲染 -->
<div v-for="item in items" :key="item.id" v-memo="[item.id]">
{{ expensiveOperation(item) }}
</div>
</div>
</template>
3. 使用 v-once
渲染一次性内容
<template>
<div>
<!-- 只渲染一次,永不更新 -->
<header v-once>
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</header>
<!-- 动态内容 -->
<main>{{ dynamicContent }}</main>
</div>
</template>
4. 使用 defineAsyncComponent
延迟加载组件
<script setup>
import { defineAsyncComponent } from 'vue'
// 异步加载重量级组件
const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)
</script>
工具和生态系统
1. 使用 Volar 提升开发体验
Volar 是专为 Vue 3 设计的 VS Code 插件,它提供了:
- 完整的组合式 API 类型支持
- 模板表达式类型检查
- 组件 props 类型检查
- 定义块跳转
2. 与 TypeScript 的配合使用
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
// 使用类型声明 props
const props = defineProps<{
title: string
items?: string[]
}>()
// 使用类型声明 emits
const emit = defineEmits<{
(e: 'select', id: number): void
(e: 'update', value: string): void
}>()
// 为 props 提供默认值
withDefaults(defineProps<{
title: string
count?: number
}>(), {
count: 0
})
</script>
3. 与 Pinia 结合使用
Pinia 是 Vue 团队推荐的状态管理库,它与组合式 API 完美结合:
// store.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
}
}
})
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
// 直接访问 state
console.log(counter.count)
// 调用 action
counter.increment()
// 访问 getter
console.log(counter.doubleCount)
</script>
结论
Vue3 的组合式 API 为构建复杂应用提供了更好的工具。通过按逻辑关注点组织代码、提取可重用的组合函数,以及利用 TypeScript 的类型系统,我们可以编写更可维护和健壮的 Vue 应用。
虽然学习曲线可能比选项式 API 更陡峭,但长期来看,组合式 API 提供的灵活性和可扩展性是值得的投资。随着项目规模和复杂度增长,组合式 API 的优势会变得更加明显。
最后,记住没有万能的解决方案 - 根据项目需求选择最合适的工具和模式,有时候混合使用选项式 API 和组合式 API 可能是最佳选择。