Vue 3 Composition API 最佳实践
Vue 3 的 Composition API 为我们提供了更灵活、更强大的组件逻辑组织方式。本文将分享在 Ghuo Design 组件库开发过程中总结的 Composition API 最佳实践,帮助开发者写出更优雅、更可维护的代码。
Composition API 核心概念
1. 响应式系统
Vue 3 的响应式系统是 Composition API 的基础:
typescript
import { ref, reactive, computed, watch, watchEffect } from 'vue'
// 基础响应式数据
const count = ref(0)
const user = reactive({
name: 'John',
age: 30,
email: 'john@example.com'
})
// 计算属性
const doubleCount = computed(() => count.value * 2)
const userInfo = computed(() => `${user.name} (${user.age})`)
// 侦听器
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`)
})
// 立即执行的侦听器
watchEffect(() => {
console.log(`Current count: ${count.value}`)
})2. 组合函数(Composables)
组合函数是 Composition API 的核心模式:
typescript
// composables/useCounter.ts
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
const isEven = computed(() => count.value % 2 === 0)
const isPositive = computed(() => count.value > 0)
return {
count: readonly(count),
increment,
decrement,
reset,
isEven,
isPositive
}
}
// 在组件中使用
export default defineComponent({
setup() {
const { count, increment, decrement, isEven } = useCounter(10)
return {
count,
increment,
decrement,
isEven
}
}
})最佳实践模式
1. 组合函数设计原则
单一职责原则
每个组合函数应该专注于一个特定的功能:
typescript
// ✅ 好的实践 - 单一职责
export function useLocalStorage(key: string, defaultValue: any) {
const storedValue = localStorage.getItem(key)
const value = ref(storedValue ? JSON.parse(storedValue) : defaultValue)
const setValue = (newValue: any) => {
value.value = newValue
localStorage.setItem(key, JSON.stringify(newValue))
}
return { value: readonly(value), setValue }
}
export function useValidation(rules: ValidationRule[]) {
const errors = ref<string[]>([])
const isValid = computed(() => errors.value.length === 0)
const validate = (value: any) => {
errors.value = rules
.filter(rule => !rule.validator(value))
.map(rule => rule.message)
}
return { errors: readonly(errors), isValid, validate }
}
// ❌ 不好的实践 - 职责混乱
export function useFormWithStorage(key: string, rules: ValidationRule[]) {
// 混合了存储和验证逻辑
// 违反了单一职责原则
}可组合性
设计可以相互组合的函数:
typescript
// 基础组合函数
export function useToggle(initialValue = false) {
const value = ref(initialValue)
const toggle = () => value.value = !value.value
const setTrue = () => value.value = true
const setFalse = () => value.value = false
return { value: readonly(value), toggle, setTrue, setFalse }
}
export function useAsync<T>(asyncFn: () => Promise<T>) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const loading = ref(false)
const execute = async () => {
loading.value = true
error.value = null
try {
data.value = await asyncFn()
} catch (err) {
error.value = err as Error
} finally {
loading.value = false
}
}
return {
data: readonly(data),
error: readonly(error),
loading: readonly(loading),
execute
}
}
// 组合使用
export function useModal() {
const { value: visible, toggle, setTrue: show, setFalse: hide } = useToggle()
const { data, loading, error, execute: loadData } = useAsync(fetchModalData)
const openModal = async () => {
show()
await loadData()
}
return {
visible,
show: openModal,
hide,
toggle,
data,
loading,
error
}
}2. 类型安全实践
泛型约束
使用 TypeScript 泛型提供类型安全:
typescript
interface UseListOptions<T> {
initialItems?: T[]
keyField?: keyof T
sortBy?: keyof T
filterFn?: (item: T) => boolean
}
export function useList<T extends Record<string, any>>(
options: UseListOptions<T> = {}
) {
const {
initialItems = [],
keyField = 'id' as keyof T,
sortBy,
filterFn
} = options
const items = ref<T[]>(initialItems)
const selectedItems = ref<T[]>([])
const addItem = (item: T) => {
items.value.push(item)
}
const removeItem = (key: T[keyof T]) => {
const index = items.value.findIndex(item => item[keyField] === key)
if (index > -1) {
items.value.splice(index, 1)
}
}
const updateItem = (key: T[keyof T], updates: Partial<T>) => {
const index = items.value.findIndex(item => item[keyField] === key)
if (index > -1) {
items.value[index] = { ...items.value[index], ...updates }
}
}
const filteredItems = computed(() => {
let result = items.value
if (filterFn) {
result = result.filter(filterFn)
}
if (sortBy) {
result = [...result].sort((a, b) => {
const aVal = a[sortBy]
const bVal = b[sortBy]
return aVal > bVal ? 1 : aVal < bVal ? -1 : 0
})
}
return result
})
const selectItem = (item: T) => {
const index = selectedItems.value.findIndex(
selected => selected[keyField] === item[keyField]
)
if (index > -1) {
selectedItems.value.splice(index, 1)
} else {
selectedItems.value.push(item)
}
}
const selectAll = () => {
selectedItems.value = [...filteredItems.value]
}
const clearSelection = () => {
selectedItems.value = []
}
return {
items: readonly(items),
selectedItems: readonly(selectedItems),
filteredItems,
addItem,
removeItem,
updateItem,
selectItem,
selectAll,
clearSelection
}
}
// 使用示例
interface User {
id: number
name: string
email: string
role: string
}
const {
items: users,
selectedItems: selectedUsers,
filteredItems: filteredUsers,
addItem: addUser,
removeItem: removeUser,
selectItem: selectUser
} = useList<User>({
keyField: 'id',
sortBy: 'name',
filterFn: user => user.role === 'admin'
})返回值类型定义
明确定义组合函数的返回值类型:
typescript
interface UseFormReturn<T> {
values: Readonly<Ref<T>>
errors: Readonly<Ref<Record<keyof T, string[]>>>
isValid: Readonly<Ref<boolean>>
isDirty: Readonly<Ref<boolean>>
isSubmitting: Readonly<Ref<boolean>>
setValue: <K extends keyof T>(key: K, value: T[K]) => void
setValues: (values: Partial<T>) => void
validate: () => Promise<boolean>
submit: () => Promise<void>
reset: () => void
}
export function useForm<T extends Record<string, any>>(
initialValues: T,
validationRules?: ValidationRules<T>,
onSubmit?: (values: T) => Promise<void>
): UseFormReturn<T> {
const values = ref<T>({ ...initialValues })
const errors = ref<Record<keyof T, string[]>>({} as Record<keyof T, string[]>)
const isSubmitting = ref(false)
const isValid = computed(() =>
Object.values(errors.value).every(fieldErrors => fieldErrors.length === 0)
)
const isDirty = computed(() =>
JSON.stringify(values.value) !== JSON.stringify(initialValues)
)
const setValue = <K extends keyof T>(key: K, value: T[K]) => {
values.value[key] = value
validateField(key)
}
const setValues = (newValues: Partial<T>) => {
Object.assign(values.value, newValues)
validate()
}
const validateField = async (key: keyof T) => {
if (!validationRules?.[key]) return
const fieldRules = validationRules[key]
const fieldErrors: string[] = []
for (const rule of fieldRules) {
const isValid = await rule.validator(values.value[key], values.value)
if (!isValid) {
fieldErrors.push(rule.message)
}
}
errors.value[key] = fieldErrors
}
const validate = async (): Promise<boolean> => {
if (!validationRules) return true
await Promise.all(
Object.keys(validationRules).map(key => validateField(key as keyof T))
)
return isValid.value
}
const submit = async () => {
if (!onSubmit) return
isSubmitting.value = true
try {
const isFormValid = await validate()
if (isFormValid) {
await onSubmit(values.value)
}
} finally {
isSubmitting.value = false
}
}
const reset = () => {
values.value = { ...initialValues }
errors.value = {} as Record<keyof T, string[]>
}
return {
values: readonly(values),
errors: readonly(errors),
isValid,
isDirty,
isSubmitting: readonly(isSubmitting),
setValue,
setValues,
validate,
submit,
reset
}
}3. 生命周期管理
资源清理
确保在组件卸载时正确清理资源:
typescript
export function useEventListener(
target: EventTarget | Ref<EventTarget | null>,
event: string,
handler: EventListener,
options?: AddEventListenerOptions
) {
const cleanup = () => {
const targetElement = unref(target)
if (targetElement) {
targetElement.removeEventListener(event, handler, options)
}
}
const setup = () => {
const targetElement = unref(target)
if (targetElement) {
targetElement.addEventListener(event, handler, options)
}
}
// 立即设置监听器
setup()
// 监听 target 变化
if (isRef(target)) {
watch(target, (newTarget, oldTarget) => {
if (oldTarget) {
oldTarget.removeEventListener(event, handler, options)
}
if (newTarget) {
newTarget.addEventListener(event, handler, options)
}
})
}
// 组件卸载时清理
onUnmounted(cleanup)
// 返回手动清理函数
return cleanup
}
// 使用示例
export default defineComponent({
setup() {
const buttonRef = ref<HTMLButtonElement>()
// 自动管理事件监听器
useEventListener(buttonRef, 'click', () => {
console.log('Button clicked')
})
useEventListener(window, 'resize', () => {
console.log('Window resized')
})
return { buttonRef }
}
})异步操作管理
正确处理异步操作和取消:
typescript
export function useAsyncOperation<T>(
operation: (signal: AbortSignal) => Promise<T>
) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const loading = ref(false)
const abortController = ref<AbortController | null>(null)
const execute = async (...args: any[]) => {
// 取消之前的操作
if (abortController.value) {
abortController.value.abort()
}
abortController.value = new AbortController()
loading.value = true
error.value = null
try {
data.value = await operation(abortController.value.signal)
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
error.value = err
}
} finally {
loading.value = false
}
}
const cancel = () => {
if (abortController.value) {
abortController.value.abort()
loading.value = false
}
}
// 组件卸载时取消操作
onUnmounted(cancel)
return {
data: readonly(data),
error: readonly(error),
loading: readonly(loading),
execute,
cancel
}
}
// 使用示例
const { data: users, loading, error, execute: fetchUsers } = useAsyncOperation(
async (signal: AbortSignal) => {
const response = await fetch('/api/users', { signal })
return response.json()
}
)4. 状态管理模式
全局状态管理
使用 provide/inject 实现轻量级状态管理:
typescript
// stores/useGlobalStore.ts
interface GlobalState {
user: User | null
theme: 'light' | 'dark'
language: string
}
const GlobalStateKey: InjectionKey<GlobalState> = Symbol('GlobalState')
export function provideGlobalStore() {
const state = reactive<GlobalState>({
user: null,
theme: 'light',
language: 'zh-CN'
})
const setUser = (user: User | null) => {
state.user = user
}
const setTheme = (theme: 'light' | 'dark') => {
state.theme = theme
document.documentElement.setAttribute('data-theme', theme)
}
const setLanguage = (language: string) => {
state.language = language
}
const store = {
state: readonly(state),
setUser,
setTheme,
setLanguage
}
provide(GlobalStateKey, store)
return store
}
export function useGlobalStore() {
const store = inject(GlobalStateKey)
if (!store) {
throw new Error('useGlobalStore must be used within a provider')
}
return store
}
// App.vue
export default defineComponent({
setup() {
const store = provideGlobalStore()
// 初始化主题
onMounted(() => {
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark'
if (savedTheme) {
store.setTheme(savedTheme)
}
})
return {}
}
})
// 子组件中使用
export default defineComponent({
setup() {
const { state, setTheme } = useGlobalStore()
const toggleTheme = () => {
const newTheme = state.theme === 'light' ? 'dark' : 'light'
setTheme(newTheme)
localStorage.setItem('theme', newTheme)
}
return {
theme: toRef(state, 'theme'),
toggleTheme
}
}
})局部状态管理
使用组合函数管理复杂的局部状态:
typescript
interface TableState<T> {
data: T[]
loading: boolean
error: Error | null
pagination: {
current: number
pageSize: number
total: number
}
sorting: {
field: keyof T | null
order: 'asc' | 'desc' | null
}
filters: Record<string, any>
selection: T[]
}
export function useTable<T extends Record<string, any>>(
fetchData: (params: any) => Promise<{ data: T[]; total: number }>,
options: {
initialPageSize?: number
defaultSorting?: { field: keyof T; order: 'asc' | 'desc' }
} = {}
) {
const state = reactive<TableState<T>>({
data: [],
loading: false,
error: null,
pagination: {
current: 1,
pageSize: options.initialPageSize || 10,
total: 0
},
sorting: {
field: options.defaultSorting?.field || null,
order: options.defaultSorting?.order || null
},
filters: {},
selection: []
})
const loadData = async () => {
state.loading = true
state.error = null
try {
const params = {
page: state.pagination.current,
pageSize: state.pagination.pageSize,
sortField: state.sorting.field,
sortOrder: state.sorting.order,
...state.filters
}
const result = await fetchData(params)
state.data = result.data
state.pagination.total = result.total
} catch (error) {
state.error = error as Error
} finally {
state.loading = false
}
}
const changePage = (page: number) => {
state.pagination.current = page
loadData()
}
const changePageSize = (pageSize: number) => {
state.pagination.pageSize = pageSize
state.pagination.current = 1
loadData()
}
const sort = (field: keyof T) => {
if (state.sorting.field === field) {
state.sorting.order = state.sorting.order === 'asc' ? 'desc' : 'asc'
} else {
state.sorting.field = field
state.sorting.order = 'asc'
}
state.pagination.current = 1
loadData()
}
const filter = (filters: Record<string, any>) => {
state.filters = { ...filters }
state.pagination.current = 1
loadData()
}
const toggleSelection = (item: T) => {
const index = state.selection.findIndex(selected => selected.id === item.id)
if (index > -1) {
state.selection.splice(index, 1)
} else {
state.selection.push(item)
}
}
const selectAll = () => {
state.selection = [...state.data]
}
const clearSelection = () => {
state.selection = []
}
// 初始加载
onMounted(loadData)
return {
state: readonly(state),
loadData,
changePage,
changePageSize,
sort,
filter,
toggleSelection,
selectAll,
clearSelection
}
}性能优化技巧
1. 响应式优化
使用 shallowRef 和 shallowReactive
对于大型对象或不需要深度响应式的数据:
typescript
import { shallowRef, shallowReactive, triggerRef } from 'vue'
// 大型数据集合使用 shallowRef
const largeDataSet = shallowRef<LargeObject[]>([])
const updateLargeDataSet = (newData: LargeObject[]) => {
largeDataSet.value = newData
// 手动触发更新
triggerRef(largeDataSet)
}
// 只有顶层属性需要响应式
const shallowState = shallowReactive({
count: 0,
config: {
// 这个对象内部的变化不会触发响应式更新
theme: 'light',
language: 'en'
}
})使用 markRaw 标记非响应式对象
typescript
import { markRaw } from 'vue'
// 第三方库实例不需要响应式
const chart = markRaw(new Chart(canvas, config))
const map = markRaw(new GoogleMap(element, options))
// 在状态中使用
const state = reactive({
chartInstance: chart,
mapInstance: map,
// 其他响应式数据
data: []
})2. 计算属性优化
避免在计算属性中进行副作用操作
typescript
// ❌ 不好的实践
const expensiveComputed = computed(() => {
// 副作用操作
console.log('Computing...')
localStorage.setItem('cache', JSON.stringify(data.value))
return data.value.map(item => ({
...item,
processed: true
}))
})
// ✅ 好的实践
const expensiveComputed = computed(() => {
return data.value.map(item => ({
...item,
processed: true
}))
})
// 副作用操作放在 watchEffect 中
watchEffect(() => {
console.log('Data changed:', expensiveComputed.value)
localStorage.setItem('cache', JSON.stringify(expensiveComputed.value))
})使用计算属性缓存昂贵的计算
typescript
export function useExpensiveCalculation(data: Ref<number[]>) {
const result = computed(() => {
// 昂贵的计算只在 data 变化时执行
return data.value.reduce((acc, curr, index) => {
// 复杂的计算逻辑
return acc + Math.pow(curr, index)
}, 0)
})
return { result }
}3. 侦听器优化
使用 flush: 'post' 优化 DOM 更新时机
typescript
// 在 DOM 更新后执行
watch(
data,
(newData) => {
// 此时 DOM 已经更新
nextTick(() => {
// 执行需要 DOM 的操作
updateChart(newData)
})
},
{ flush: 'post' }
)
// 或者使用 watchPostEffect
watchPostEffect(() => {
// 在 DOM 更新后自动执行
updateChart(data.value)
})使用 deep: false 优化对象侦听
typescript
const user = ref({ name: 'John', profile: { age: 30, email: 'john@example.com' } })
// ❌ 深度侦听,性能开销大
watch(user, (newUser) => {
console.log('User changed:', newUser)
}, { deep: true })
// ✅ 只侦听引用变化
watch(user, (newUser) => {
console.log('User reference changed:', newUser)
})
// ✅ 侦听特定属性
watch(() => user.value.name, (newName) => {
console.log('Name changed:', newName)
})测试策略
1. 组合函数测试
typescript
// tests/composables/useCounter.test.ts
import { describe, it, expect } from 'vitest'
import { useCounter } from '@/composables/useCounter'
describe('useCounter', () => {
it('should initialize with default value', () => {
const { count } = useCounter()
expect(count.value).toBe(0)
})
it('should initialize with custom value', () => {
const { count } = useCounter(10)
expect(count.value).toBe(10)
})
it('should increment count', () => {
const { count, increment } = useCounter(5)
increment()
expect(count.value).toBe(6)
})
it('should decrement count', () => {
const { count, decrement } = useCounter(5)
decrement()
expect(count.value).toBe(4)
})
it('should reset count', () => {
const { count, increment, reset } = useCounter(5)
increment()
increment()
expect(count.value).toBe(7)
reset()
expect(count.value).toBe(5)
})
it('should compute isEven correctly', () => {
const { count, increment, isEven } = useCounter(2)
expect(isEven.value).toBe(true)
increment()
expect(isEven.value).toBe(false)
})
})2. 异步组合函数测试
typescript
// tests/composables/useAsync.test.ts
import { describe, it, expect, vi } from 'vitest'
import { useAsync } from '@/composables/useAsync'
describe('useAsync', () => {
it('should handle successful async operation', async () => {
const mockFn = vi.fn().mockResolvedValue('success')
const { data, loading, error, execute } = useAsync(mockFn)
expect(loading.value).toBe(false)
expect(data.value).toBe(null)
expect(error.value).toBe(null)
const promise = execute()
expect(loading.value).toBe(true)
await promise
expect(loading.value).toBe(false)
expect(data.value).toBe('success')
expect(error.value).toBe(null)
})
it('should handle failed async operation', async () => {
const mockError = new Error('Test error')
const mockFn = vi.fn().mockRejectedValue(mockError)
const { data, loading, error, execute } = useAsync(mockFn)
await execute()
expect(loading.value).toBe(false)
expect(data.value).toBe(null)
expect(error.value).toBe(mockError)
})
})总结
Vue 3 Composition API 的最佳实践包括:
核心原则
- 单一职责:每个组合函数专注于一个功能
- 可组合性:设计可以相互组合的函数
- 类型安全:充分利用 TypeScript 的类型系统
- 资源管理:正确处理生命周期和资源清理
性能优化
- 合理使用响应式 API:根据需求选择合适的响应式函数
- 优化计算属性:避免副作用,利用缓存机制
- 精确侦听:避免不必要的深度侦听
开发体验
- 清晰的 API 设计:提供直观易用的接口
- 完善的类型定义:提供良好的 IDE 支持
- 充分的测试覆盖:确保代码质量和可靠性
通过遵循这些最佳实践,我们可以充分发挥 Vue 3 Composition API 的优势,构建出更加灵活、可维护和高性能的应用程序。
发布于 2024年10月10日