Vue3组合式API为什么是现代前端工程化的基础
Vue3的组合式API(Composition API)不只是Options API的替代写法,它改变了前端代码的组织方式——从按选项类型分散到按逻辑关注点聚合。当组件逻辑复杂度上升时,Options API的data、methods、computed分散在不同选项中,同一业务逻辑的代码被割裂到多个位置,维护成本急剧上升。组合式API通过Composable函数将相关逻辑封装在一起,是实现大型前端项目可维护性的关键。
Composable设计模式:从逻辑提取到可复用单元
Composable(可组合函数)是组合式API的核心封装单元。一个良好的Composable应该满足:单一职责、显式参数、清晰的返回值类型、自动清理副作用。
基础模式:数据获取Composable
// composables/useFetch.ts
import { ref, shallowRef, type Ref } from 'vue'
interface UseFetchOptions<T> {
immediate?: boolean
initialData?: T
onSuccess?: (data: T) => void
onError?: (error: Error) => void
}
interface UseFetchReturn<T> {
data: Readonly<Ref<T | null>>
error: Readonly<Ref<Error | null>>
isLoading: Readonly<Ref<boolean>>
execute: () => Promise<void>
}
export function useFetch<T>(
url: string | Ref<string>,
options: UseFetchOptions<T> = {}
): UseFetchReturn<T> {
const data = shallowRef<T | null>(options.initialData ?? null)
const error = ref<Error | null>(null)
const isLoading = ref(false)
let abortController: AbortController | null = null
const execute = async () => {
abortController?.abort()
abortController = new AbortController()
isLoading.value = true
error.value = null
try {
const resolvedUrl = typeof url === 'string' ? url : url.value
const response = await fetch(resolvedUrl, {
signal: abortController.signal
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
data.value = await response.json() as T
options.onSuccess?.(data.value)
} catch (e) {
if (e instanceof Error && e.name !== 'AbortError') {
error.value = e
options.onError?.(e)
}
} finally {
isLoading.value = false
}
}
if (options.immediate !== false) {
execute()
}
return {
data: data as Readonly<Ref<T | null>>,
error: error as Readonly<Ref<Error | null>>,
isLoading: isLoading as Readonly<Ref<boolean>>,
execute
}
}
使用示例:
// 在组件中使用
const { data: users, isLoading, error, execute: refresh } = useFetch<User[]>(
'/api/users',
{ immediate: true }
)
Composable组合模式:多个Composable协同
复杂业务场景下,多个Composable需要协同工作。典型例子:分页列表 = 数据获取 + 分页状态 + 筛选条件。
// composables/usePagination.ts
import { ref, computed, watch, type Ref } from 'vue'
interface UsePaginationOptions {
pageSize: number
total: Ref<number>
}
export function usePagination(options: UsePaginationOptions) {
const currentPage = ref(1)
const pageSize = ref(options.pageSize)
const totalPages = computed(() =>
Math.ceil(options.total.value / pageSize.value)
)
const offset = computed(() =>
(currentPage.value - 1) * pageSize.value
)
// 页码变化时重置到第1页
watch(pageSize, () => { currentPage.value = 1 })
const goToPage = (page: number) => {
currentPage.value = Math.max(1, Math.min(page, totalPages.value))
}
return { currentPage, pageSize, totalPages, offset, goToPage }
}
// 在组件中组合使用
const total = ref(0)
const { currentPage, pageSize, offset } = usePagination({ pageSize: 20, total })
const { data: items } = useFetch(computed(() =>
`/api/items?offset=${offset.value}&limit=${pageSize.value}`
))
状态管理:Pinia + Composable的分层架构
Pinia是Vue3官方推荐的状态管理库。在大型项目中,推荐的状态管理分层架构:
- 局部状态:组件内部ref/reactive,不需要跨组件共享
- Composable状态:通过Composable共享,适用于强关联的功能模块(如表单校验、分页)
- 全局Store:Pinia Store管理跨模块共享的业务数据(如用户信息、权限、全局配置)
// stores/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '')
const userInfo = shallowRef<UserInfo | null>(null)
const isLoggedIn = computed(() => !!token.value)
const login = async (credentials: LoginParams) => {
const res = await authApi.login(credentials)
token.value = res.token
userInfo.value = res.user
localStorage.setItem('token', res.token)
}
const logout = () => {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
}
return { token, userInfo, isLoggedIn, login, logout }
})
关键设计原则:Pinia Store只管理全局共享状态,功能模块内的状态用Composable封装。避免把所有状态都塞进Store——那会让Store变成巨型全局变量池,失去模块化的意义。
TypeScript类型安全:泛型Composable的进阶写法
TypeScript与组合式API结合能显著提升开发体验和代码可靠性。泛型Composable是进阶写法:
// composables/useTable.ts — 通用表格逻辑
interface TableState<T> {
items: T[]
selected: Set<number>
sortBy: keyof T | null
sortOrder: 'asc' | 'desc'
}
export function useTable<T extends Record<string, unknown>>(
fetchFn: (params: TableParams) => Promise<TableResponse<T>>
) {
const state = reactive<TableState<T>>({
items: [],
selected: new Set(),
sortBy: null,
sortOrder: 'asc'
})
const sortedItems = computed(() => {
if (!state.sortBy) return state.items
return [...state.items].sort((a, b) => {
const aVal = a[state.sortBy!]
const bVal = b[state.sortBy!]
const modifier = state.sortOrder === 'asc' ? 1 : -1
return aVal < bVal ? -modifier : aVal > bVal ? modifier : 0
})
})
const toggleSort = (key: keyof T) => {
if (state.sortBy === key) {
state.sortOrder = state.sortOrder === 'asc' ? 'desc' : 'asc'
} else {
state.sortBy = key
state.sortOrder = 'asc'
}
}
return { state: readonly(state), sortedItems, toggleSort }
}
响应式陷阱与性能优化
组合式API有几个常见的响应式陷阱:
陷阱1:解构reactive丢失响应性
// 错误:解构后丢失响应性
const state = reactive({ count: 0, name: '' })
const { count } = state // count是普通number,不是ref
// 正确:使用toRefs
const { count } = toRefs(state) // count是ref
陷阱2:watch监听reactive对象的属性
// 错误:无法直接监听reactive对象的某个属性
watch(() => state.count, (val) => { ... }) // 不会触发
// 正确方式1:getter函数
watch(() => state.count, (val) => { ... }) // Vue3.2+支持
// 正确方式2:用ref代替
const count = ref(0)
watch(count, (val) => { ... })
性能优化:shallowRef和shallowReactive
当数据对象很大但不需要深度响应时,使用shallow版本避免深层代理的性能开销:
// 大型列表数据无需深度响应
const largeList = shallowRef<Item[]>([])
// 更新时替换整个数组引用
largeList.value = [...largeList.value, newItem] // 触发更新
largeList.value.push(newItem) // 不触发更新!
Vue3组合式API的核心价值在于:通过Composable函数将逻辑封装为可复用、可组合、类型安全的单元,配合Pinia实现分层状态管理,再辅以TypeScript泛型约束——这套架构能支撑大型前端项目从开发到长期维护的全周期需求。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-she-ji-mo-shi-composable-han-shu-yu/