Skip to content

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 的最佳实践包括:

核心原则

  1. 单一职责:每个组合函数专注于一个功能
  2. 可组合性:设计可以相互组合的函数
  3. 类型安全:充分利用 TypeScript 的类型系统
  4. 资源管理:正确处理生命周期和资源清理

性能优化

  1. 合理使用响应式 API:根据需求选择合适的响应式函数
  2. 优化计算属性:避免副作用,利用缓存机制
  3. 精确侦听:避免不必要的深度侦听

开发体验

  1. 清晰的 API 设计:提供直观易用的接口
  2. 完善的类型定义:提供良好的 IDE 支持
  3. 充分的测试覆盖:确保代码质量和可靠性

通过遵循这些最佳实践,我们可以充分发挥 Vue 3 Composition API 的优势,构建出更加灵活、可维护和高性能的应用程序。


发布于 2024年10月10日

Released under the MIT License.