Vue3组合式API配合TypeScript构建可复用业务组件实战

为什么组合式API更适合TypeScript

Vue3的选项式API(Options API)配合TypeScript存在天然的类型推导问题——data、computed、methods中的属性需要手动标注类型,this的上下文推断依赖Vue的内部类型体操。组合式API(Composition API)使用普通变量和函数组织逻辑,TypeScript的类型推导开箱即用,不需要额外的类型声明或defineComponent包装。

更重要的是,组合式API的逻辑复用粒度更细。选项式API只能通过Mixins复用,存在命名冲突和来源不透明的硬伤;组合式API通过Composables函数复用逻辑,来源明确、类型安全、可组合嵌套。

Composables函数的TypeScript类型设计

一个规范的Composable函数需要清晰的入参类型、返回值类型和泛型约束:

// composables/useTable.ts
import { ref, reactive, computed, type Ref } from 'vue'

// 分页参数类型
interface Pagination {
  page: number
  pageSize: number
  total: Ref<number>
}

// 表格数据请求函数的约束
interface TableRequest<T> {
  (params: Record<string, unknown>): Promise<{ list: T[]; total: number }>
}

// 泛型Composable:支持任意行数据类型
export function useTable<T extends Record<string, unknown>>(
  fetchFn: TableRequest<T>,
  defaultPageSize = 10
) {
  const loading = ref(false)
  const dataSource = ref<T[]>([]) as Ref<T[]>
  const pagination = reactive<Pagination>({
    page: 1,
    pageSize: defaultPageSize,
    total: ref(0)
  })

  const fetch = async (extraParams?: Record<string, unknown>) => {
    loading.value = true
    try {
      const { list, total } = await fetchFn({
        page: pagination.page,
        pageSize: pagination.pageSize,
        ...extraParams
      })
      dataSource.value = list
      pagination.total.value = total
    } finally {
      loading.value = false
    }
  }

  const handlePageChange = (page: number) => {
    pagination.page = page
    fetch()
  }

  return {
    loading,
    dataSource,
    pagination: computed(() => ({
      current: pagination.page,
      pageSize: pagination.pageSize,
      total: pagination.total.value
    })),
    fetch,
    handlePageChange
  }
}

泛型<T>让dataSource的类型根据API返回值自动推导,组件中使用时无需重复声明类型。

业务组件Props的严格类型约束

Vue3.3+支持defineProps的泛型写法,比运行时声明更简洁且类型更严格:

<!-- components/DataTable.vue -->
<script setup lang="ts" generic="T extends Record<string, unknown>">
import type { PropType } from 'vue'

// 列配置类型
interface Column {
  key: keyof T
  title: string
  width?: number
  sortable?: boolean
  render?: (value: T[keyof T], row: T) => string
}

const props = defineProps<{
  data: T[]
  columns: Column[]
  loading?: boolean
  rowKey?: keyof T
  selectable?: boolean
}>()

const emit = defineEmits<{
  (e: 'rowClick', row: T): void
  (e: 'selectionChange', rows: T[]): void
}>()

// 类型安全的行点击处理
const handleRowClick = (row: T) => {
  emit('rowClick', row)
}
</script>

泛型组件的关键优势:Column.key的类型是keyof T,只能传入数据对象中存在的字段名,拼写错误在编译期直接报错。

组件事件与Slot的类型安全设计

Slot的Props类型推导是Vue3.3的另一大改进:

<!-- components/DataTable.vue template -->
<template>
  <table>
    <thead>
      <tr>
        <th v-for="col in columns" :key="String(col.key)" :style="{ width: col.width + 'px' }">
          {{ col.title }}
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="row in data" :key="String(row[rowKey ?? 'id'])" @click="handleRowClick(row)">
        <td v-for="col in columns" :key="String(col.key)">
          <slot :name="String(col.key)" :row="row" :value="row[col.key]">
            {{ col.render ? col.render(row[col.key], row) : row[col.key] }}
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

使用方的Slot可以拿到完整类型:

<!-- 页面中使用DataTable -->
<script setup lang="ts">
interface User {
  id: number
  name: string
  email: string
  status: 'active' | 'inactive'
}

// dataSource 类型自动推导为 Ref<User[]>
const { loading, dataSource, fetch } = useTable<User>(fetchUserList)
</script>

<template>
  <DataTable :data="dataSource" :columns="columns" :loading="loading">
    <!-- name列自定义渲染 -->
    <template #name="{ row }">
      <!-- row 类型自动推导为 User -->
      <a :href="`/users/${row.id}`">{{ row.name }}</a>
    </template>
  </DataTable>
</template>

组件库的统一导出与Tree-shaking优化

多个可复用组件需要统一的导出入口,方便业务项目按需引入:

// components/index.ts
export { default as DataTable } from './DataTable.vue'
export { default as SearchForm } from './SearchForm.vue'
export { default as ConfirmDialog } from './ConfirmDialog.vue'

export { useTable } from '../composables/useTable'
export { useForm } from '../composables/useForm'
export { usePermission } from '../composables/usePermission'

业务项目中按需引入:

// 自动按需导入(推荐)
// vite.config.ts 配置 unplugin-vue-components
import Components from 'unplugin-vue-components/vite'

export default defineConfig({
  plugins: [
    Components({
      dirs: ['src/components'],
      extensions: ['vue'],
      deep: true,
      dts: 'src/components.d.ts'  // 自动生成类型声明文件
    })
  ]
})

这种配置下,组件不需要手动import,模板中直接使用DataTable即可,构建工具自动处理按需引入和类型声明。

组件单元测试的TypeScript类型保障

使用Vitest + Vue Test Utils进行组件测试时,TypeScript类型保障同样重要:

// __tests__/DataTable.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import DataTable from '../DataTable.vue'
import type { ComponentMountingOptions } from '@vue/test-utils'

interface User {
  id: number
  name: string
  email: string
}

const mockData: User[] = [
  { id: 1, name: 'Alice', email: 'alice@test.com' },
  { id: 2, name: 'Bob', email: 'bob@test.com' }
]

const columns = [
  { key: 'name' as const, title: '姓名' },
  { key: 'email' as const, title: '邮箱' }
]

describe('DataTable', () => {
  it('渲染正确的行数', () => {
    const wrapper = mount(DataTable, {
      props: { data: mockData, columns }
    })
    expect(wrapper.findAll('tbody tr')).toHaveLength(2)
  })

  it('点击行触发rowClick事件', async () => {
    const wrapper = mount(DataTable, {
      props: { data: mockData, columns }
    })
    await wrapper.findAll('tbody tr')[0].trigger('click')
    expect(wrapper.emitted('rowClick')![0]).toEqual([mockData[0]])
  })
})

类型安全的组件体系,让编译期就能捕获Props传递错误、事件参数不匹配、Slot数据类型不一致等问题。相比运行时报错定位,编译期错误的修复成本几乎为零。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-pei-he-typescript-gou-jian-ke-fu-yong-ye/

(0)
小编小编
上一篇 1天前
下一篇 1天前

相关推荐