Vue3组合式API组件设计模式进阶:Composable函数与状态分层

Vue3组合式API组件设计模式进阶

Vue3组合式API(Composition API)不只是选项式API的语法糖,它提供了一套全新的逻辑组织与复用范式。企业级前端工程化体系中,组合式API配合TypeScript类型系统,能构建出高内聚、低耦合的组件架构。组件库设计中,合理运用组合式函数(Composables)可以将复杂的组件逻辑拆解为可测试、可复用的独立单元。

Composable函数的设计原则与实现

Composable是Vue3逻辑复用的核心机制,设计时遵循三个原则:

  1. 单一职责:一个Composable只封装一个关注点的逻辑
  2. 显式入参:通过参数接收依赖,而非从全局状态读取
  3. 返回响应式引用:返回的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/

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

相关推荐