Vue 3 组合式函数设计模式

1. 组合式函数概述

组合式函数(Composition Functions)是Vue 3中实现代码复用的核心机制,它允许开发者将组件中的公共逻辑提取到独立的函数中,然后在多个组件中复用。设计良好的组合式函数可以提高代码的可读性、可维护性和可测试性。

1.1 什么是设计模式

设计模式是解决特定问题的最佳实践,它描述了在特定场景下如何组织和编写代码。对于组合式函数,设计模式包括命名约定、参数设计、返回值设计、状态管理、生命周期管理等方面。

1.2 为什么需要设计模式

设计良好的组合式函数具有以下特点:

  • 可读性高:代码结构清晰,易于理解
  • 可维护性强:逻辑组织合理,易于修改
  • 可测试性好:可以单独测试,不需要依赖组件实例
  • 可复用性强:可以在多个组件中复用
  • 类型安全:在TypeScript环境下,支持类型推断和检查

2. 命名约定

命名约定是设计模式的重要组成部分,它可以提高代码的可读性和可维护性。

2.1 函数命名

组合式函数的命名应该遵循以下约定:

  • 使用"use"前缀:所有组合式函数都应该以"use"前缀开头,如useCounteruseUser
  • 使用动词或动词短语:描述函数的功能,如useEventListeneruseFetch
  • 使用驼峰命名法:如useLocalStorageuseDarkMode
  • 避免使用缩写:除非是广泛使用的缩写,如useHTTPuseAPI
// 好的命名
export function useCounter(initialValue = 0) {
  // ...
}

export function useEventListener(target, event, handler) {
  // ...
}

export function useLocalStorage(key, initialValue) {
  // ...
}

// 不好的命名
export function counter(initialValue = 0) {
  // 缺少"use"前缀
}

export function addEvent(target, event, handler) {
  // 缺少"use"前缀
}

export function useLS(key, initialValue) {
  // 使用了不常见的缩写
}

2.2 返回值命名

组合式函数的返回值应该遵循以下约定:

  • 使用描述性的名称:如countloadingerror
  • 使用驼峰命名法:如isLoadinghasError
  • 保持一致性:相同类型的返回值使用相同的命名,如所有布尔类型的返回值都使用isXxxhasXxx前缀
// 好的返回值命名
export function useFetch(url) {
  const data = ref(null)
  const isLoading = ref(false)
  const hasError = ref(false)
  const error = ref(null)
  
  // ...
  
  return {
    data,
    isLoading,
    hasError,
    error
  }
}

// 不好的返回值命名
export function useFetch(url) {
  const d = ref(null)
  const l = ref(false)
  const e = ref(false)
  const err = ref(null)
  
  // ...
  
  return {
    d,
    l,
    e,
    err
  }
}

3. 参数设计

参数设计是组合式函数设计的重要方面,它直接影响函数的灵活性和易用性。

3.1 参数类型

组合式函数的参数可以是以下类型:

  • 基本类型:如字符串、数字、布尔值等
  • 响应式对象:如ref、reactive等
  • 函数:如回调函数、事件处理函数等
  • 对象:如选项对象等

3.2 参数默认值

为可选参数提供默认值,提高函数的易用性:

// 好的做法:提供默认值
export function useCounter(initialValue = 0) {
  // ...
}

export function useFetch(url, options = {}) {
  const defaultOptions = {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json'
    },
    timeout: 5000
  }
  
  const finalOptions = { ...defaultOptions, ...options }
  
  // ...
}

// 不好的做法:不提供默认值
export function useCounter(initialValue) {
  // 需要在函数内部处理undefined情况
  const count = ref(initialValue || 0)
  
  // ...
}

3.3 选项对象

对于复杂的组合式函数,可以使用选项对象来组织参数,提高函数的灵活性和易用性:

// 好的做法:使用选项对象
export function useFetch(url, {
  method = 'GET',
  headers = {
    'Content-Type': 'application/json'
  },
  timeout = 5000,
  onSuccess,
  onError
} = {}) {
  // ...
}

// 使用方式
const { data, isLoading } = useFetch('https://api.example.com/users', {
  method: 'POST',
  onSuccess: (data) => {
    console.log('Success:', data)
  },
  onError: (error) => {
    console.error('Error:', error)
  }
})

// 不好的做法:使用多个位置参数
export function useFetch(url, method, headers, timeout, onSuccess, onError) {
  // 参数太多,难以使用
  // ...
}

4. 返回值设计

返回值设计是组合式函数设计的重要方面,它直接影响函数的易用性和灵活性。

4.1 返回值类型

组合式函数的返回值可以是以下类型:

  • 单个响应式值:如refreactive
  • 对象:包含多个响应式值和方法
  • 函数:如工厂函数、高阶函数等

4.2 返回值结构

组合式函数的返回值结构应该遵循以下约定:

  • 保持一致性:相同类型的组合式函数返回相同结构的值
  • 只返回必要的值:不返回不需要的中间状态
  • 返回响应式值:如果需要在模板中使用,返回响应式值
  • 返回方法:如果需要操作状态,返回方法
// 好的返回值结构
export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue
  
  return {
    count,
    increment,
    decrement,
    reset
  }
}

// 不好的返回值结构
export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const internalState = ref('some internal state') // 不应该返回内部状态
  
  const increment = () => count.value++
  
  return {
    count,
    internalState, // 不应该返回内部状态
    increment
  }
}

4.3 响应式值的处理

组合式函数的返回值应该是响应式的,或者可以转换为响应式值:

// 返回响应式值
export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  return {
    count // 响应式值
  }
}

// 返回计算属性
export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const double = computed(() => count.value * 2)
  
  return {
    count,
    double // 计算属性,响应式值
  }
}

// 返回普通值(不推荐)
export function useCounter(initialValue = 0) {
  let count = initialValue // 普通值,不是响应式的
  
  const increment = () => count++
  
  return {
    count, // 普通值,不是响应式的
    increment
  }
}

5. 状态管理

状态管理是组合式函数设计的重要方面,它包括状态的创建、更新、共享等方面。

5.1 状态创建

组合式函数的状态应该使用响应式API创建,如refreactive等:

// 好的做法:使用ref创建状态
export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  return {
    count
  }
}

// 好的做法:使用reactive创建状态
export function useUser(initialUser = { name: '', email: '' }) {
  const user = reactive(initialUser)
  
  return {
    user
  }
}

// 不好的做法:使用普通变量创建状态
export function useCounter(initialValue = 0) {
  let count = initialValue // 普通变量,不是响应式的
  
  return {
    count
  }
}

5.2 状态更新

组合式函数的状态更新应该通过方法进行,而不是直接暴露状态:

// 好的做法:提供方法更新状态
export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue
  
  return {
    count,
    increment,
    decrement,
    reset
  }
}

// 不好的做法:直接暴露状态,允许外部修改
export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  return {
    count // 外部可以直接修改count.value
  }
}

5.3 状态共享

如果需要在多个组合式函数之间共享状态,可以使用以下方式:

  • 使用依赖注入:通过provideinject共享状态
  • 使用全局状态管理:如Pinia、Vuex等
  • 使用单例模式:在组合式函数内部维护一个单例状态
// 使用单例模式共享状态
let instance = null

export function useSingleton() {
  if (instance) {
    return instance
  }
  
  const state = ref(0)
  
  const increment = () => state.value++
  
  instance = {
    state,
    increment
  }
  
  return instance
}

6. 生命周期管理

生命周期管理是组合式函数设计的重要方面,它包括组件挂载、更新、卸载等阶段的处理。

6.1 使用生命周期钩子

组合式函数可以使用Vue的生命周期钩子,如onMountedonUnmounted等:

export function useEventListener(target, event, handler) {
  // 组件挂载时添加事件监听器
  onMounted(() => {
    target.addEventListener(event, handler)
  })
  
  // 组件卸载时移除事件监听器
  onUnmounted(() => {
    target.removeEventListener(event, handler)
  })
}

6.2 清理资源

在组件卸载时,组合式函数应该清理所有资源,如事件监听器、定时器、WebSocket连接等:

export function useInterval(callback, delay) {
  let timer = null
  
  const start = () => {
    if (timer) {
      clearInterval(timer)
    }
    
    timer = setInterval(callback, delay)
  }
  
  const stop = () => {
    if (timer) {
      clearInterval(timer)
      timer = null
    }
  }
  
  // 组件挂载时启动定时器
  onMounted(() => {
    start()
  })
  
  // 组件卸载时清理定时器
  onUnmounted(() => {
    stop()
  })
  
  return {
    start,
    stop
  }
}

6.3 避免在生命周期钩子中执行耗时操作

避免在生命周期钩子中执行耗时操作,如API请求、复杂计算等,否则会阻塞组件的渲染:

// 好的做法:使用异步函数执行耗时操作
export function useFetch(url) {
  const data = ref(null)
  const isLoading = ref(false)
  const error = ref(null)
  
  const fetchData = async () => {
    isLoading.value = true
    error.value = null
    
    try {
      const response = await fetch(url)
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      isLoading.value = false
    }
  }
  
  onMounted(() => {
    fetchData()
  })
  
  return {
    data,
    isLoading,
    error,
    fetchData
  }
}

// 不好的做法:在生命周期钩子中执行同步耗时操作
export function useComplexCalculation() {
  const result = ref(null)
  
  onMounted(() => {
    // 耗时的同步计算
    let sum = 0
    for (let i = 0; i < 1000000; i++) {
      sum += i
    }
    result.value = sum
  })
  
  return {
    result
  }
}

7. 错误处理

错误处理是组合式函数设计的重要方面,它包括错误的捕获、处理、传播等方面。

7.1 捕获错误

组合式函数应该捕获所有可能的错误,避免错误传播到组件中:

// 好的做法:捕获错误
export function useFetch(url) {
  const data = ref(null)
  const isLoading = ref(false)
  const error = ref(null)
  
  const fetchData = async () => {
    isLoading.value = true
    error.value = null
    
    try {
      const response = await fetch(url)
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      isLoading.value = false
    }
  }
  
  onMounted(() => {
    fetchData()
  })
  
  return {
    data,
    isLoading,
    error,
    fetchData
  }
}

// 不好的做法:不捕获错误
export function useFetch(url) {
  const data = ref(null)
  const isLoading = ref(false)
  
  const fetchData = async () => {
    isLoading.value = true
    
    const response = await fetch(url)
    data.value = await response.json() // 可能抛出错误
    
    isLoading.value = false
  }
  
  onMounted(() => {
    fetchData() // 错误会传播到组件中
  })
  
  return {
    data,
    isLoading,
    fetchData
  }
}

7.2 提供错误信息

组合式函数应该提供清晰的错误信息,便于开发者调试:

// 好的做法:提供详细的错误信息
export function useLocalStorage(key, initialValue) {
  const value = ref(initialValue)
  const error = ref(null)
  
  const save = (newValue) => {
    try {
      localStorage.setItem(key, JSON.stringify(newValue))
      value.value = newValue
      error.value = null
    } catch (err) {
      error.value = `Failed to save to localStorage: ${err.message}`
      console.error(error.value)
    }
  }
  
  // ...
  
  return {
    value,
    error,
    save
  }
}

// 不好的做法:提供不清晰的错误信息
export function useLocalStorage(key, initialValue) {
  const value = ref(initialValue)
  const error = ref(null)
  
  const save = (newValue) => {
    try {
      localStorage.setItem(key, JSON.stringify(newValue))
      value.value = newValue
    } catch (err) {
      error.value = 'Error'
    }
  }
  
  // ...
  
  return {
    value,
    error,
    save
  }
}

8. 类型安全

类型安全是组合式函数设计的重要方面,它可以提高代码的可靠性和可维护性。

8.1 使用TypeScript

在TypeScript环境下,组合式函数应该添加类型注解,提高类型安全性:

export function useCounter(initialValue: number = 0) {
  const count = ref(initialValue)
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue
  
  return {
    count: Readonly<typeof count>, // 只读的响应式值
    increment,
    decrement,
    reset
  }
}

interface User {
  id: number
  name: string
  email: string
}

export function useUser<T extends User>(initialUser?: T) {
  const user = ref<T | null>(initialUser || null)
  
  const updateUser = (newUser: Partial<T>) => {
    if (user.value) {
      user.value = { ...user.value, ...newUser }
    }
  }
  
  return {
    user,
    updateUser
  }
}

8.2 为返回值添加类型

组合式函数应该为返回值添加类型,提高类型安全性:

interface FetchResult<T> {
  data: T | null
  isLoading: boolean
  hasError: boolean
  error: string | null
  refetch: () => Promise<void>
}

export function useFetch<T>(url: string): FetchResult<T> {
  const data = ref<T | null>(null)
  const isLoading = ref(false)
  const hasError = ref(false)
  const error = ref<string | null>(null)
  
  const refetch = async () => {
    // ...
  }
  
  // ...
  
  return {
    data,
    isLoading,
    hasError,
    error,
    refetch
  }
}

9. 测试

测试是组合式函数设计的重要方面,它可以提高代码的可靠性和可维护性。

9.1 单元测试

组合式函数可以进行单元测试,不需要依赖组件实例:

// useCounter.test.js
import { useCounter } from './useCounter'
import { nextTick } from 'vue'

describe('useCounter', () => {
  it('should initialize with correct value', () => {
    const { count } = useCounter(5)
    expect(count.value).toBe(5)
  })
  
  it('should increment count correctly', () => {
    const { count, increment } = useCounter()
    increment()
    expect(count.value).toBe(1)
  })
  
  it('should decrement count correctly', () => {
    const { count, decrement } = useCounter(5)
    decrement()
    expect(count.value).toBe(4)
  })
  
  it('should reset count correctly', () => {
    const { count, increment, reset } = useCounter(5)
    increment()
    reset()
    expect(count.value).toBe(5)
  })
})

9.2 集成测试

组合式函数也可以进行集成测试,测试它在组件中的使用情况:

// CounterComponent.test.js
import { mount } from '@vue/test-utils'
import CounterComponent from './CounterComponent.vue'

describe('CounterComponent', () => {
  it('should display correct count', () => {
    const wrapper = mount(CounterComponent)
    expect(wrapper.text()).toContain('Count: 0')
  })
  
  it('should increment count when button is clicked', async () => {
    const wrapper = mount(CounterComponent)
    await wrapper.find('button').trigger('click')
    expect(wrapper.text()).toContain('Count: 1')
  })
})

10. 最佳实践

10.1 保持函数简洁

组合式函数应该保持简洁,只负责单一功能:

// 好的做法:只负责单一功能
export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue
  
  return {
    count,
    increment,
    decrement,
    reset
  }
}

// 不好的做法:负责多个不相关的功能
export function useCounterAndUser(initialValue = 0, userId = 1) {
  // 计数器逻辑
  const count = ref(initialValue)
  const increment = () => count.value++
  
  // 用户逻辑
  const user = ref(null)
  const loadUser = async () => {
    user.value = await fetch(`/api/users/${userId}`)
  }
  
  return {
    count,
    increment,
    user,
    loadUser
  }
}

10.2 避免副作用

组合式函数应该避免在函数调用时产生副作用,除非是必要的:

// 好的做法:避免副作用
export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const increment = () => count.value++
  
  return {
    count,
    increment
  }
}

// 不好的做法:产生不必要的副作用
export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  // 不必要的副作用
  console.log('useCounter called with initialValue:', initialValue)
  
  const increment = () => count.value++
  
  return {
    count,
    increment
  }
}

10.3 支持选项配置

组合式函数应该支持选项配置,提高灵活性:

// 好的做法:支持选项配置
export function useFetch(url, options = {}) {
  const defaultOptions = {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json'
    },
    timeout: 5000,
    onSuccess: () => {},
    onError: () => {}
  }
  
  const finalOptions = { ...defaultOptions, ...options }
  
  // ...
}

// 不好的做法:不支持选项配置
export function useFetch(url, method = 'GET') {
  // 只支持method参数,不灵活
  // ...
}

10.4 文档化函数

组合式函数应该添加文档注释,提高代码的可读性和可维护性:

/**
 * Counter hook that provides count state and increment/decrement methods
 * @param {number} initialValue - Initial count value (default: 0)
 * @returns {Object} Count state and methods
 * @returns {Ref<number>} returns.count - Current count value
 * @returns {Function} returns.increment - Increment count by 1
 * @returns {Function} returns.decrement - Decrement count by 1
 * @returns {Function} returns.reset - Reset count to initial value
 */
export function useCounter(initialValue = 0) {
  // ...
}

11. 实战练习

练习1:设计一个useLocalStorage组合式函数

设计一个useLocalStorage组合式函数,用于在localStorage中存储和读取数据:

  • 支持从localStorage读取初始值
  • 支持保存数据到localStorage
  • 支持删除localStorage中的数据
  • 支持错误处理
  • 支持TypeScript类型

练习2:设计一个useDarkMode组合式函数

设计一个useDarkMode组合式函数,用于管理应用的深色模式:

  • 支持从localStorage读取初始模式
  • 支持切换深色/浅色模式
  • 支持监听系统主题变化
  • 支持应用主题到DOM

练习3:设计一个useDebounce组合式函数

设计一个useDebounce组合式函数,用于防抖处理:

  • 支持自定义延迟时间
  • 支持取消防抖
  • 支持立即执行
  • 支持TypeScript类型

12. 总结

组合式函数是Vue 3中实现代码复用的核心机制,设计良好的组合式函数具有以下特点:

  • 命名规范:使用"use"前缀,描述性的名称
  • 参数设计合理:提供默认值,支持选项配置
  • 返回值结构清晰:只返回必要的值,返回响应式值
  • 状态管理合理:使用响应式API,提供方法更新状态
  • 生命周期管理完善:清理资源,避免内存泄漏
  • 错误处理完善:捕获错误,提供清晰的错误信息
  • 类型安全:支持TypeScript类型注解
  • 可测试:可以进行单元测试和集成测试

掌握组合式函数的设计模式,是编写高质量Vue 3应用的关键。通过合理的设计模式,可以提高代码的可读性、可维护性和可测试性,实现高效的代码复用。

13. 扩展阅读

通过本集的学习,我们深入理解了Vue 3中组合式函数的设计模式,包括命名约定、参数设计、返回值设计、状态管理、生命周期管理、错误处理、类型安全和测试等方面。在下一集中,我们将学习可复用逻辑抽象,这是组合式API的高级应用。

« 上一篇 混入(mixin)的替代方案 下一篇 » 可复用逻辑抽象