为什么需要规范化的Composable封装
Vue3的Composition API将逻辑复用从Mixins的隐式依赖彻底转向显式引用,Composable函数成为组织业务逻辑的核心单元。但实际项目中,Composable的封装质量差异巨大——有的函数职责清晰、类型完备、测试友好,有的则变成另一个万能函数。规范化的Composable封装需要解决三个问题:逻辑内聚与职责边界、响应式状态管理、TypeScript类型安全。
Composable设计五原则
原则1:单一职责。一个Composable只做一件事。useUserList管用户列表,useUserForm管表单,不要合并成useUser既管列表又管表单又管权限。
原则2:输入参数类型化。所有入参使用interface定义,拒绝any。
原则3:返回值结构化。返回ref和方法,用as const保持类型窄化。
原则4:副作用隔离。生命周期钩子(onMounted、watch等)在Composable内部注册,调用方无需关心清理逻辑。
原则5:可测试性。Composable不直接依赖全局状态(如Pinia store),通过参数注入依赖。
实战:封装一个通用分页Composable
分页是后台管理系统中最常见的逻辑,抽取为Composable后可在任意列表页复用:
import { ref, computed, watch, type Ref } from 'vue'
interface UsePaginationOptions<T> {
fetcher: (page: number, pageSize: number) => Promise<{ data: T[]; total: number }>
pageSize?: number
initialPage?: number
immediate?: boolean
}
interface UsePaginationReturn<T> {
data: Ref<T[]>
total: Ref<number>
currentPage: Ref<number>
pageSize: Ref<number>
loading: Ref<boolean>
totalPages: Ref<number>
goToPage: (page: number) => void
refresh: () => void
}
export function usePagination<T>(
options: UsePaginationOptions<T>
): UsePaginationReturn<T> {
const { fetcher, immediate = true } = options
const pageSize = ref(options.pageSize ?? 20)
const currentPage = ref(options.initialPage ?? 1)
const data = ref<T[]>([]) as Ref<T[]>
const total = ref(0)
const loading = ref(false)
const totalPages = computed(() => Math.ceil(total.value / pageSize.value))
async function fetchData() {
loading.value = true
try {
const result = await fetcher(currentPage.value, pageSize.value)
data.value = result.data
total.value = result.total
} catch (error) {
data.value = []
total.value = 0
} finally {
loading.value = false
}
}
function goToPage(page: number) {
const clamped = Math.max(1, Math.min(page, totalPages.value))
currentPage.value = clamped
}
function refresh() { fetchData() }
watch([currentPage, pageSize], () => { fetchData() })
if (immediate) { fetchData() }
return { data, total, currentPage, pageSize, loading, totalPages, goToPage, refresh }
}
在组件中使用Composable
<script setup lang="ts">
import { usePagination } from '@/composables/usePagination'
import { getUserList } from '@/api/user'
interface UserItem {
id: number
name: string
email: string
}
const pagination = usePagination<UserItem>({
fetcher: async (page, pageSize) => {
const res = await getUserList({ page, pageSize })
return { data: res.list, total: res.total }
},
pageSize: 20
})
</script>
TypeScript类型推导进阶:泛型约束与条件类型
当Composable需要支持多种API响应格式时,泛型约束可以让类型推导更加精确:
interface ApiPageResponse<T> {
list: T[]
total: number
page: number
pageSize: number
}
type PaginationFetcher<T, R extends ApiPageResponse<T>> =
(page: number, pageSize: number) => Promise<R>
// 使用infer提取数组元素类型
type ExtractListItem<T> = T extends ApiPageResponse<infer U> ? U : never
// 实际应用
type UserResponse = ApiPageResponse<{ id: number; name: string }>
type UserType = ExtractListItem<UserResponse>
// UserType = { id: number; name: string }
Composable测试策略
Composable的测试需要Vue Test Utils提供响应式上下文。核心思路是:mock掉fetcher依赖,验证返回值的响应式行为:
import { usePagination } from '@/composables/usePagination'
describe('usePagination', () => {
it('should call fetcher on initial load', async () => {
const mockFetcher = vi.fn().mockResolvedValue({
data: [{ id: 1, name: 'test' }],
total: 1
})
const result = usePagination({
fetcher: mockFetcher,
immediate: false
})
await result.refresh()
expect(mockFetcher).toHaveBeenCalledWith(1, 20)
expect(result.data.value).toHaveLength(1)
expect(result.total.value).toBe(1)
})
})
Vue3的Composable不是简单的代码复用工具,它是组织业务逻辑的结构化单元。从入参类型定义到返回值结构化,从副作用隔离到依赖注入测试,每一步的规范化都直接影响代码的可维护性。封装Composable时,先定义interface再写实现——这个顺序本身就是在约束设计质量。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-han-shu-feng-zhuang-shi-jian-cong-composable/