Vue3组合式API组件设计模式进阶
Vue3组合式API(Composition API)不只是选项式API的语法糖,它提供了一套全新的逻辑组织与复用范式。企业级前端工程化体系中,组合式API配合TypeScript类型系统,能构建出高内聚、低耦合的组件架构。组件库设计中,合理运用组合式函数(Composables)可以将复杂的组件逻辑拆解为可测试、可复用的独立单元。
Composable函数的设计原则与实现
Composable是Vue3逻辑复用的核心机制,设计时遵循三个原则:
- 单一职责:一个Composable只封装一个关注点的逻辑
- 显式入参:通过参数接收依赖,而非从全局状态读取
- 返回响应式引用:返回的ref和computed保持响应式链路
实战示例——表格分页与排序逻辑:
import { ref, computed, watch, type Ref } from 'vue'
interface PaginationOptions<T> {
data: Ref<T[]>
pageSize?: number
initialSortKey?: keyof T
}
interface SortState<T> {
key: keyof T
direction: 'asc' | 'desc'
}
export function usePagination<T>(options: PaginationOptions<T>) {
const { data, pageSize = 10 } = options
const currentPage = ref(1)
const sortState = ref<SortState<T> | null>(
options.initialSortKey
? { key: options.initialSortKey, direction: 'asc' }
: null
)
const sortedData = computed(() => {
if (!sortState.value) return data.value
const { key, direction } = sortState.value
return [...data.value].sort((a, b) => {
const cmp = a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0
return direction === 'asc' ? cmp : -cmp
})
})
const totalPages = computed(() => Math.ceil(data.value.length / pageSize))
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize
return sortedData.value.slice(start, start + pageSize)
})
function goToPage(page: number) {
currentPage.value = Math.max(1, Math.min(page, totalPages.value))
}
function toggleSort(key: keyof T) {
if (sortState.value?.key === key) {
sortState.value.direction = sortState.value.direction === 'asc' ? 'desc' : 'asc'
} else {
sortState.value = { key, direction: 'asc' }
}
}
// 数据变化时重置到第一页
watch(() => data.value.length, () => {
currentPage.value = 1
})
return {
currentPage,
totalPages,
paginatedData,
sortState,
goToPage,
toggleSort
}
}
组件Props与Emits的类型安全设计
Vue3配合TypeScript的defineProps和defineEmits宏,可以实现端到端的类型安全。企业级组件库需要严格的Props校验:
<script setup lang="ts">
import type { PropType } from 'vue'
// Props类型定义
interface TableColumn<T> {
key: keyof T
title: string
width?: number
sortable?: boolean
render?: (value: T[keyof T], row: T) => string
}
interface TableProps<T> {
data: T[]
columns: TableColumn<T>[]
loading?: boolean
pageSize?: number
rowKey: keyof T
}
const props = withDefaults(defineProps<TableProps<any>>(), {
loading: false,
pageSize: 10
})
// Emits类型定义
const emit = defineEmits<{
rowClick: [row: any]
sort: [key: string, direction: 'asc' | 'desc']
pageChange: [page: number]
}>()
// 使用Composable
const {
currentPage,
totalPages,
paginatedData,
sortState,
goToPage,
toggleSort
} = usePagination({
data: toRef(props, 'data'),
pageSize: props.pageSize
})
function handleSort(key: string) {
toggleSort(key)
emit('sort', key, sortState.value?.direction ?? 'asc')
}
</script>
<template>
<div class="data-table" :class="{ 'is-loading': loading }">
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key" :style="{ width: col.width + 'px' }">
{{ col.title }}
<button v-if="col.sortable" @click="handleSort(col.key as string)">
{{ sortState?.key === col.key ? (sortState.direction === 'asc' ? '↑' : '↓') : '↕' }}
</button>
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in paginatedData" :key="row[rowKey]" @click="emit('rowClick', row)">
<td v-for="col in columns" :key="col.key">
<template v-if="col.render">
<component :is="col.render(row[col.key], row)" />
</template>
<template v-else>
{{ row[col.key] }}
</template>
</td>
</tr>
</tbody>
</table>
<div class="pagination">
<button :disabled="currentPage === 1" @click="goToPage(currentPage - 1)">上一页</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)">下一页</button>
</div>
</div>
</template>
跨端组件库的响应式布局适配
企业级组件库需要适配从移动端到大屏的多终端场景。Vue3响应式布局方案核心是CSS Container Queries配合Vue3的useBreakpoint Composable:
import { ref, onMounted, onUnmounted } from 'vue'
type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
const BREAKPOINTS: Record<Breakpoint, number> = {
xs: 0,
sm: 640,
md: 768,
lg: 1024,
xl: 1280
}
export function useBreakpoint() {
const current = ref<Breakpoint>('md')
function update() {
const width = window.innerWidth
if (width >= BREAKPOINTS.xl) current.value = 'xl'
else if (width >= BREAKPOINTS.lg) current.value = 'lg'
else if (width >= BREAKPOINTS.md) current.value = 'md'
else if (width >= BREAKPOINTS.sm) current.value = 'sm'
else current.value = 'xs'
}
onMounted(() => {
update()
window.addEventListener('resize', update, { passive: true })
})
onUnmounted(() => {
window.removeEventListener('resize', update)
})
const isMobile = computed(() => ['xs', 'sm'].includes(current.value))
const isDesktop = computed(() => ['lg', 'xl'].includes(current.value))
return { current, isMobile, isDesktop, breakpoints: BREAKPOINTS }
}
组件内按断点条件渲染:
<script setup>
const { isMobile } = useBreakpoint()
</script>
<template>
<MobileLayout v-if="isMobile" />
<DesktopLayout v-else />
</template>
前端工程化中的状态管理模式
Vue3生态中状态管理方案多样,Pinia已成为官方推荐。但企业级应用中,全局状态与组件本地状态的边界划分直接影响应用的可维护性。
状态分层策略:
- 组件本地状态:UI交互状态(展开/折叠、表单输入值、当前选中项)用ref/reactive管理
- Composable作用域状态:跨组件复用的业务逻辑状态(分页、筛选、排序)封装在Composable内
- 全局共享状态:用户信息、权限数据、全局配置用Pinia Store管理
- 服务端状态:API请求数据用TanStack Query(Vue Query)管理,自动处理缓存与重校验
Pinia Store示例:
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
const userInfo = ref<UserInfo | null>(null)
const permissions = ref<string[]>([])
const isLoggedIn = computed(() => !!userInfo.value)
async function login(credentials: LoginPayload) {
const res = await authApi.login(credentials)
userInfo.value = res.user
permissions.value = res.permissions
localStorage.setItem('token', res.token)
}
function logout() {
userInfo.value = null
permissions.value = []
localStorage.removeItem('token')
}
function hasPermission(perm: string): boolean {
return permissions.value.includes(perm)
}
return { userInfo, permissions, isLoggedIn, login, logout, hasPermission }
})
Vue Query管理API数据:
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
export function useOrderList(params: Ref<OrderQuery>) {
return useQuery({
queryKey: ['orders', params],
queryFn: () => orderApi.getList(params.value),
staleTime: 30000, // 30秒内不重新请求
gcTime: 300000, // 5分钟后回收缓存
refetchOnWindowFocus: false
})
}
export function useCreateOrder() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: orderApi.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['orders'] })
}
})
}
这种分层方案将服务端状态从Pinia中剥离,由Vue Query统一管理缓存、重试和后台刷新,Pinia只负责客户端状态,职责边界清晰,测试也更容易。
组件测试策略
组合式API天然利于单元测试——Composable是纯函数,脱离组件上下文即可测试:
import { usePagination } from '../composables/usePagination'
import { ref } from 'vue'
describe('usePagination', () => {
const mockData = ref([
{ id: 1, name: 'Alpha', score: 95 },
{ id: 2, name: 'Beta', score: 82 },
{ id: 3, name: 'Gamma', score: 91 }
])
it('分页计算正确', () => {
const { paginatedData, totalPages, currentPage } = usePagination({
data: mockData,
pageSize: 2
})
expect(totalPages.value).toBe(2)
expect(paginatedData.value).toHaveLength(2)
expect(currentPage.value).toBe(1)
})
it('排序功能正常', () => {
const { paginatedData, toggleSort } = usePagination({
data: mockData,
pageSize: 10,
initialSortKey: 'score'
})
expect(paginatedData.value[0].name).toBe('Beta') // 82最低,升序排列
toggleSort('score')
expect(paginatedData.value[0].name).toBe('Alpha') // 95最高,降序排列
})
})
组件级测试使用Vue Test Utils,重点验证交互行为与事件触发,而非实现细节。Composable的纯逻辑测试覆盖核心分支,组件测试覆盖DOM交互,端到端测试覆盖关键用户流程——三层测试策略在保证覆盖率的同时降低维护成本。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-zu-jian-she-ji-mo-shi-jin-jie-composable/