标签:Vue 3, 前端开发, Composition API, 组件复用, 状态管理
简介:全面介绍Vue 3 Composition API的核心特性,通过实际项目案例演示如何利用Composition API实现组件逻辑复用、状态管理和复杂业务逻辑的优雅组织。
引言:从Options API到Composition API的演进
随着前端应用复杂度的不断提升,Vue 2时代的 Options API(即基于 data, methods, computed, watch 等选项的声明式写法)在处理大型组件时逐渐暴露出一些结构性问题:
- 逻辑分散:同一功能的代码被拆分到不同选项中,难以维护。
- 复用困难:当多个组件需要共享相同逻辑时,依赖
mixins的方式会导致命名冲突和作用域污染。 - 类型推导弱:在TypeScript中,
Options API的类型推断能力有限,不利于构建可维护的大型项目。
为解决这些问题,Vue 3引入了全新的 Composition API。它不再依赖于 this 上下文,而是以函数形式组织逻辑,提供了更灵活、可组合、可重用的编程范式。
为什么选择Composition API?
- 逻辑复用更自然:通过自定义 Composable 函数实现跨组件逻辑共享。
- 更好的类型支持:在 TypeScript 中能提供完整的类型推导与提示。
- 更强的代码组织能力:将相关逻辑按功能模块组织,提升可读性和可维护性。
- 避免 this 指向陷阱:不再依赖
this,减少运行时错误。 - 与现代JavaScript生态无缝集成:支持
const,let,async/await,import/export等现代语法。
本文将深入探讨 Composition API 的核心机制,并通过真实项目案例展示其在组件复用与状态管理中的最佳实践。
一、Composition API 核心概念详解
1.1 setup() 函数:入口点
setup() 是 Composition API 的起点,它是组件实例创建后、挂载前执行的函数,接收两个参数:
export default {
setup(props, context) {
// props: 父组件传入的属性
// context: 包含 attrs, slots, emit 等上下文信息
}
}
⚠️ 注意:在
setup()中无法访问this,所有响应式数据必须通过ref/reactive创建。
1.2 响应式基础:ref 与 reactive
ref<T>:创建一个响应式引用
import { ref } from 'vue'
const count = ref(0)
// 读取值:count.value
console.log(count.value) // 0
// 改变值:count.value = newVal
count.value = 10
ref 可用于任何类型(基本类型、对象、数组等),并自动实现响应式绑定。
reactive<T>:创建一个深层响应式对象
import { reactive } from 'vue'
const state = reactive({
name: 'Alice',
age: 25,
hobbies: ['coding', 'reading']
})
state.name = 'Bob'
state.hobbies.push('traveling')
✅ 推荐:对复杂对象使用
reactive;对简单值或需要解构的场景使用ref。
1.3 计算属性:computed
import { computed } from 'vue'
const fullName = computed(() => {
return `${person.firstName} ${person.lastName}`
})
computed 会缓存结果,在依赖未变化时不重新计算,性能优化显著。
1.4 监听器:watch
基础监听
import { watch } from 'vue'
watch(
() => user.age,
(newAge, oldAge) => {
console.log(`年龄从 ${oldAge} 变为 ${newAge}`)
}
)
多项监听
watch(
[() => user.firstName, () => user.lastName],
([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`姓名更新:${newFirst} ${newLast}`)
}
)
深层监听
watch(
() => user.profile,
(newProfile, oldProfile) => {
// 只有当 profile 内部属性变化时触发
},
{ deep: true }
)
💡 提示:
watch支持返回停止监听函数,适合异步操作清理。
1.5 生命周期钩子:onXxx API
Vue 3 提供了与生命周期对应的 onXxx 函数,可在 setup() 中使用:
| 旧写法 | 新写法 |
|---|---|
beforeCreate |
❌ 无对应(已合并至 setup) |
created |
onMounted |
beforeMount |
onBeforeMount |
mounted |
onMounted |
beforeUpdate |
onBeforeUpdate |
updated |
onUpdated |
beforeUnmount |
onBeforeUnmount |
unmounted |
onUnmounted |
import { onMounted, onUnmounted } from 'vue'
export default {
setup() {
onMounted(() => {
console.log('组件已挂载')
})
onUnmounted(() => {
console.log('组件即将销毁')
})
}
}
✅ 优点:统一生命周期管理,便于测试与调试。
二、组件复用:Composable 函数的设计与实践
2.1 什么是 Composable 函数?
Composable 函数 是一个返回响应式数据和方法的纯函数,可以被任意组件调用,实现逻辑复用。
典型结构如下:
// composable/useCounter.ts
import { ref } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => {
count.value += 1
}
const decrement = () => {
count.value -= 1
}
const reset = () => {
count.value = initialValue
}
return {
count,
increment,
decrement,
reset
}
}
2.2 通用复用:表单验证逻辑
假设我们有一个用户注册表单,包含用户名、邮箱、密码校验。我们可以将其抽象为一个 Composable:
// composable/useFormValidation.ts
import { ref, computed } from 'vue'
interface ValidationResult {
valid: boolean
errors: string[]
}
export function useFormValidation(initialValues: Record<string, any>) {
const form = ref({ ...initialValues })
const errors = ref<Record<string, string>>({})
const validateField = (field: string, value: string): boolean => {
switch (field) {
case 'username':
return value.length >= 3 && /^[a-zA-Z0-9_]+$/.test(value)
case 'email':
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
case 'password':
return value.length >= 8
default:
return true
}
}
const validateAll = (): ValidationResult => {
const fieldErrors: Record<string, string> = {}
let isValid = true
for (const [key, value] of Object.entries(form.value)) {
if (!validateField(key, String(value))) {
fieldErrors[key] = `Invalid ${key}`
isValid = false
}
}
errors.value = fieldErrors
return { valid: isValid, errors: Object.values(fieldErrors) }
}
const reset = () => {
form.value = { ...initialValues }
errors.value = {}
}
const setField = (field: string, value: any) => {
form.value[field] = value
// 可选:实时校验
if (errors.value[field]) {
delete errors.value[field]
}
}
const getFieldError = (field: string) => errors.value[field]
return {
form,
errors,
validateAll,
reset,
setField,
getFieldError,
isDirty: computed(() => Object.keys(form.value).some(k => form.value[k] !== initialValues[k]))
}
}
2.3 组件中使用:注册页面示例
<!-- RegisterForm.vue -->
<script setup>
import { useFormValidation } from '@/composables/useFormValidation'
const initialValues = {
username: '',
email: '',
password: ''
}
const { form, errors, validateAll, reset, setField, getFieldError, isDirty } = useFormValidation(initialValues)
const handleSubmit = () => {
const result = validateAll()
if (result.valid) {
alert('注册成功!')
reset()
} else {
console.log('表单验证失败:', result.errors)
}
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<div>
<label>用户名:</label>
<input
v-model="form.username"
:class="{ error: getFieldError('username') }"
@blur="validateAll()"
/>
<span v-if="getFieldError('username')" class="error-text">{{ getFieldError('username') }}</span>
</div>
<div>
<label>邮箱:</label>
<input
v-model="form.email"
:class="{ error: getFieldError('email') }"
@blur="validateAll()"
/>
<span v-if="getFieldError('email')" class="error-text">{{ getFieldError('email') }}</span>
</div>
<div>
<label>密码:</label>
<input
type="password"
v-model="form.password"
:class="{ error: getFieldError('password') }"
@blur="validateAll()"
/>
<span v-if="getFieldError('password')" class="error-text">{{ getFieldError('password') }}</span>
</div>
<button type="submit" :disabled="!isDirty">提交</button>
<button type="button" @click="reset">重置</button>
</form>
</template>
<style scoped>
.error { border-color: red; }
.error-text { color: red; font-size: 0.8em; }
</style>
✅ 优势:
- 表单逻辑完全独立于组件,可复用于登录页、资料修改页等。
- 易于单元测试。
- 支持动态字段配置。
三、状态管理:从局部状态到全局状态
3.1 局部状态管理:使用 useXxx Composable
对于组件内部状态,建议封装成 Composable,例如:
// composable/useLocalStorage.ts
import { ref, watch } from 'vue'
export function useLocalStorage<T>(key: string, initialValue: T): Ref<T> {
const storedValue = localStorage.getItem(key)
const value = ref<T>(
storedValue ? JSON.parse(storedValue) : initialValue
)
watch(
value,
(newVal) => {
localStorage.setItem(key, JSON.stringify(newVal))
},
{ deep: true }
)
return value
}
使用:
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'
const theme = useLocalStorage('app-theme', 'light')
// 切换主题
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>
🌟 这种模式非常适合保存用户偏好、本地缓存等。
3.2 全局状态管理:Pinia vs Vuex
虽然 Vue 3 推荐使用 Pinia 作为官方推荐的状态管理库,但你可以通过 Composable + provide/inject 实现轻量级全局状态。
方案一:使用 Pinia(推荐)
// stores/userStore.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: null as any,
isLoggedIn: false
}),
getters: {
displayName() {
return this.userInfo?.name || 'Anonymous'
}
},
actions: {
login(userData) {
this.userInfo = userData
this.isLoggedIn = true
},
logout() {
this.userInfo = null
this.isLoggedIn = false
}
}
})
<script setup>
import { useUserStore } from '@/stores/userStore'
const userStore = useUserStore()
const login = () => {
userStore.login({ name: 'Alice', email: 'alice@example.com' })
}
const logout = () => {
userStore.logout()
}
</script>
✅ Pinia 优势:
- 原生支持 TypeScript
- 支持热更新
- 模块化设计,易于拆分
- 无需额外插件即可使用
方案二:基于 provide/inject 手动实现全局状态(适用于小型项目)
// composables/useGlobalState.ts
import { provide, inject, ref } from 'vue'
const GLOBAL_STATE_KEY = 'globalState'
export function useGlobalState() {
const state = inject(GLOBAL_STATE_KEY)
if (!state) {
throw new Error('useGlobalState must be used within a provider')
}
return state
}
export function provideGlobalState() {
const state = {
user: ref(null),
theme: ref('light'),
notifications: ref([] as Array<{ id: number; msg: string }>)
}
provide(GLOBAL_STATE_KEY, state)
return state
}
<!-- App.vue -->
<script setup>
import { provideGlobalState } from '@/composables/useGlobalState'
provideGlobalState()
</script>
<!-- UserProfile.vue -->
<script setup>
import { useGlobalState } from '@/composables/useGlobalState'
const { user, theme, notifications } = useGlobalState()
const updateUser = (data) => {
user.value = data
}
const addNotification = (msg) => {
notifications.value.push({ id: Date.now(), msg })
}
</script>
⚠️ 注意:
provide/inject不支持响应式穿透,需确保ref/reactive正确包裹。
四、高级实战:复杂业务逻辑的组织与解耦
4.1 案例:商品详情页的“库存与价格联动”逻辑
需求:商品详情页需要根据数量、规格、促销活动动态计算总价,并实时更新库存状态。
抽象 Composable:useProductPriceCalculator
// composable/useProductPriceCalculator.ts
import { ref, computed, watch } from 'vue'
export interface ProductOption {
id: string
name: string
price: number
stock: number
selected: boolean
}
export interface Promotion {
id: string
name: string
discountRate: number
condition: (quantity: number) => boolean
}
interface ProductConfig {
basePrice: number
options: ProductOption[]
promotions: Promotion[]
maxQuantityPerOrder: number
}
export function useProductPriceCalculator(config: ProductConfig) {
const quantity = ref(1)
const selectedOptionId = ref<string | null>(null)
const selectedOption = computed(() => {
return config.options.find(opt => opt.id === selectedOptionId.value)
})
const total = computed(() => {
const base = selectedOption.value?.price ?? config.basePrice
const subtotal = base * quantity.value
const promotion = config.promotions.find(p => p.condition(quantity.value))
if (promotion) {
return subtotal * (1 - promotion.discountRate)
}
return subtotal
})
const availableStock = computed(() => {
const option = selectedOption.value
if (!option) return Infinity
return Math.min(option.stock, config.maxQuantityPerOrder)
})
const canAddToCart = computed(() => {
return quantity.value <= availableStock.value
})
const addToCart = () => {
if (!canAddToCart.value) {
console.warn('库存不足')
return false
}
// 模拟添加到购物车
console.log(`已加入 ${quantity.value} 件,总价:${total.value.toFixed(2)} 元`)
return true
}
const updateQuantity = (val: number) => {
const clamped = Math.max(1, Math.min(val, availableStock.value))
quantity.value = clamped
}
const selectOption = (id: string) => {
selectedOptionId.value = id
}
// 同步库存变化
watch(selectedOption, () => {
if (quantity.value > availableStock.value) {
quantity.value = availableStock.value
}
})
return {
quantity,
selectedOptionId,
selectedOption,
total,
availableStock,
canAddToCart,
addToCart,
updateQuantity,
selectOption
}
}
4.2 组件中使用:ProductDetail.vue
<script setup>
import { useProductPriceCalculator } from '@/composables/useProductPriceCalculator'
const productConfig = {
basePrice: 199,
maxQuantityPerOrder: 10,
options: [
{ id: 'size-s', name: 'S', price: 199, stock: 50, selected: false },
{ id: 'size-m', name: 'M', price: 219, stock: 30, selected: true },
{ id: 'size-l', name: 'L', price: 239, stock: 15, selected: false }
],
promotions: [
{
id: 'bulk-10',
name: '满10件享9折',
discountRate: 0.1,
condition: q => q >= 10
},
{
id: 'bulk-5',
name: '满5件享95折',
discountRate: 0.05,
condition: q => q >= 5
}
]
}
const {
quantity,
selectedOptionId,
selectedOption,
total,
availableStock,
canAddToCart,
addToCart,
updateQuantity,
selectOption
} = useProductPriceCalculator(productConfig)
</script>
<template>
<div class="product-detail">
<h2>商品名称</h2>
<!-- 规格选择 -->
<div class="options">
<label>请选择尺寸:</label>
<div>
<label v-for="opt in productConfig.options" :key="opt.id">
<input
type="radio"
:value="opt.id"
v-model="selectedOptionId"
@change="selectOption(opt.id)"
/>
{{ opt.name }} (¥{{ opt.price }}) - 库存:{{ opt.stock }}
</label>
</div>
</div>
<!-- 数量控制 -->
<div class="quantity-control">
<label>数量:</label>
<button @click="updateQuantity(quantity - 1)" :disabled="quantity <= 1">-</button>
<span>{{ quantity }}</span>
<button @click="updateQuantity(quantity + 1)" :disabled="quantity >= availableStock">+</button>
<span class="stock-info">
可售:{{ availableStock }} 件
</span>
</div>
<!-- 总价显示 -->
<div class="total">
<strong>总价:¥{{ total.toFixed(2) }}</strong>
</div>
<!-- 购买按钮 -->
<button
@click="addToCart"
:disabled="!canAddToCart"
:class="{ disabled: !canAddToCart }"
>
{{ canAddToCart ? '加入购物车' : '库存不足' }}
</button>
</div>
</template>
<style scoped>
.product-detail { padding: 20px; }
.options label { margin-right: 10px; }
.quantity-control button { width: 30px; height: 30px; margin: 0 5px; }
.stock-info { color: #666; font-size: 0.9em; }
.disabled { opacity: 0.5; cursor: not-allowed; }
</style>
✅ 本案例体现 Composition API 的核心优势:
- 业务逻辑集中在一个文件中,清晰可读。
- 状态与视图分离,便于单元测试。
- 可轻松复用于其他商品页或弹窗组件。
五、最佳实践总结
| 主题 | 最佳实践 |
|---|---|
| 命名规范 | useXxx 前缀表示 Composable;xxxRef 表示响应式引用 |
| 类型安全 | 使用 TypeScript 定义接口与泛型 |
| 副作用管理 | 在 watch、onMounted 等中及时清理资源 |
| 依赖注入 | 尽量避免过度使用 provide/inject,优先考虑 Pinia |
| 组合顺序 | 将 ref/reactive 放在顶部,逻辑按功能分组 |
| 测试友好 | 所有逻辑应可独立测试,不依赖 DOM |
5.1 单元测试示例(Jest + Vitest)
// tests/useProductPriceCalculator.test.ts
import { describe, it, expect } from 'vitest'
import { useProductPriceCalculator } from '@/composables/useProductPriceCalculator'
const config = {
basePrice: 100,
maxQuantityPerOrder: 5,
options: [
{ id: 's', name: 'S', price: 100, stock: 10, selected: true },
{ id: 'm', name: 'M', price: 120, stock: 5, selected: false }
],
promotions: [
{ id: '5', name: '满5件9折', discountRate: 0.1, condition: q => q >= 5 }
]
}
describe('useProductPriceCalculator', () => {
it('should calculate correct total with promotion', () => {
const { quantity, total, addToCart } = useProductPriceCalculator(config)
quantity.value = 5
expect(total.value).toBe(500 * 0.9) // 450
})
it('should prevent adding more than stock', () => {
const { quantity, availableStock } = useProductPriceCalculator(config)
quantity.value = 10
expect(quantity.value).toBe(availableStock.value) // 5
})
})
结语:拥抱 Composition API,构建可维护的现代前端架构
Vue 3 的 Composition API 不仅仅是一次语法升级,更是前端工程思想的一次跃迁。它让我们从“组件为中心”的思维转向“逻辑为中心”的设计模式。
通过合理使用 Composable 函数,我们可以:
- 将重复逻辑提取为可复用模块;
- 使复杂业务逻辑清晰可读;
- 极大提升团队协作效率;
- 为未来迁移至微前端、服务端渲染打下坚实基础。
🔥 记住:一个好的 Composable,应该像一个“乐高积木”——独立、可组合、可测试。
从今天开始,让每个 useXXX 都成为你代码库中最干净、最优雅的部分。
参考文档:
✅ 本文完整代码可在 GitHub 仓库中获取:github.com/example/vue3-composition-practice

评论 (0)