Vue 3 组合式函数设计模式
1. 组合式函数概述
组合式函数(Composition Functions)是Vue 3中实现代码复用的核心机制,它允许开发者将组件中的公共逻辑提取到独立的函数中,然后在多个组件中复用。设计良好的组合式函数可以提高代码的可读性、可维护性和可测试性。
1.1 什么是设计模式
设计模式是解决特定问题的最佳实践,它描述了在特定场景下如何组织和编写代码。对于组合式函数,设计模式包括命名约定、参数设计、返回值设计、状态管理、生命周期管理等方面。
1.2 为什么需要设计模式
设计良好的组合式函数具有以下特点:
- 可读性高:代码结构清晰,易于理解
- 可维护性强:逻辑组织合理,易于修改
- 可测试性好:可以单独测试,不需要依赖组件实例
- 可复用性强:可以在多个组件中复用
- 类型安全:在TypeScript环境下,支持类型推断和检查
2. 命名约定
命名约定是设计模式的重要组成部分,它可以提高代码的可读性和可维护性。
2.1 函数命名
组合式函数的命名应该遵循以下约定:
- 使用"use"前缀:所有组合式函数都应该以"use"前缀开头,如
useCounter、useUser等 - 使用动词或动词短语:描述函数的功能,如
useEventListener、useFetch等 - 使用驼峰命名法:如
useLocalStorage、useDarkMode等 - 避免使用缩写:除非是广泛使用的缩写,如
useHTTP、useAPI等
// 好的命名
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 返回值命名
组合式函数的返回值应该遵循以下约定:
- 使用描述性的名称:如
count、loading、error等 - 使用驼峰命名法:如
isLoading、hasError等 - 保持一致性:相同类型的返回值使用相同的命名,如所有布尔类型的返回值都使用
isXxx或hasXxx前缀
// 好的返回值命名
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 返回值类型
组合式函数的返回值可以是以下类型:
- 单个响应式值:如
ref、reactive等 - 对象:包含多个响应式值和方法
- 函数:如工厂函数、高阶函数等
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创建,如ref、reactive等:
// 好的做法:使用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 状态共享
如果需要在多个组合式函数之间共享状态,可以使用以下方式:
- 使用依赖注入:通过
provide和inject共享状态 - 使用全局状态管理:如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的生命周期钩子,如onMounted、onUnmounted等:
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的高级应用。