Vue3组合式函数封装实践:从Composable设计到TypeScript类型推导全流程

为什么需要规范化的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/

(0)
小编小编
上一篇 11小时前
下一篇 11小时前

相关推荐