Vue 3 Composition API 企业级架构:Composables 复用 + TypeScript 深度集成

作者:忆笙智云官方 | 发布时间:2026-06-16 09:00 | 更新时间:2026-06-16 09:00

Vue 3 Composition API企业级架构实践

引言

Vue 3的Composition API是对Options API的范式升级,它以函数为组织单元,天然支持逻辑复用和代码拆分,在企业级项目中展现出显著优势。相比Options API按选项类型(data/methods/computed)组织代码的方式,Composition API按功能关注点组织代码,使得复杂组件的维护成本大幅降低。

本文将深入探讨Vue 3 Composition API在企业项目中的最佳实践,涵盖setup语法糖、composables复用模式、TypeScript集成等核心话题。

一、Options API vs Composition API

1.1 代码组织对比

flowchart LR
    subgraph Options API
        direction TB
        A1[data: 用户相关] --- A2[data: 权限相关]
        A2 --- A3[methods: 用户相关]
        A3 --- A4[methods: 权限相关]
        A4 --- A5[computed: 用户相关]
        A5 --- A6[computed: 权限相关]
    end

    subgraph Composition API
        direction TB
        B1[useUser: 用户相关全部逻辑] --- B2[usePermission: 权限相关全部逻辑]
    end

    style B1 fill:#4CAF50,color:#fff
    style B2 fill:#2196F3,color:#fff

Options API中,同一功能的代码散落在data、methods、computed等不同选项中;Composition API则将同一功能的逻辑聚合在一起,形成内聚的代码块。

1.2 典型对比示例

// Options API:逻辑分散
export default {
  data() {
    return {
      // 用户相关
      userList: [],
      userLoading: false,
      // 权限相关
      permissions: [],
      permLoading: false,
    }
  },
  methods: {
    // 用户相关
    async fetchUsers() { ... },
    async deleteUser(id) { ... },
    // 权限相关
    async fetchPermissions() { ... },
    async assignPermission() { ... },
  },
  computed: {
    // 用户相关
    activeUsers() { ... },
    // 权限相关
    hasAdminPerm() { ... },
  }
}
<!-- Composition API:逻辑聚合 -->
<script setup>
// 用户相关逻辑 - 完整内聚
const { userList, userLoading, fetchUsers, deleteUser, activeUsers } = useUser()

// 权限相关逻辑 - 完整内聚
const { permissions, permLoading, fetchPermissions, assignPermission, hasAdminPerm } = usePermission()
</script>

二、setup语法糖深度实践

2.1 基础用法

<script setup>
import { ref, reactive, computed, watch, onMounted } from 'vue'

// 响应式状态
const loading = ref(false)
const formData = reactive({
  username: '',
  email: '',
  role: ''
})

// 计算属性
const isFormValid = computed(() => {
  return formData.username.trim() !== '' && formData.email.includes('@')
})

// 方法
const handleSubmit = async () => {
  if (!isFormValid.value) return
  loading.value = true
  try {
    await createUserApi(formData)
    ElMessage.success('创建成功')
  } finally {
    loading.value = false
  }
}

// 生命周期
onMounted(() => {
  loadRoleOptions()
})
</script>

2.2 defineProps与defineEmits

<script setup>
/**
 * 用户编辑对话框组件
 * 通过defineProps接收外部数据,defineEmits声明事件
 */
const props = defineProps({
  modelValue: { type: Boolean, default: false },
  userId: { type: Number, default: null },
  mode: { type: String, default: 'add', validator: v => ['add', 'edit'].includes(v) }
})

const emit = defineEmits(['update:modelValue', 'success'])

// 双向绑定:v-model支持
const visible = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val)
})

// 监听对话框打开
watch(() => props.modelValue, (val) => {
  if (val && props.mode === 'edit' && props.userId) {
    loadUserDetail(props.userId)
  }
})
</script>

2.3 defineExpose选择性暴露

<script setup>
/**
 * 父组件只能通过ref访问expose的方法
 * 实现组件API的最小暴露原则
 */
const formRef = ref(null)
const resetForm = () => {
  formRef.value?.resetFields()
}

const validate = async () => {
  return await formRef.value?.validate()
}

// 只暴露必要的方法,隐藏内部实现
defineExpose({ resetForm, validate })
</script>

三、Composables复用模式

3.1 Composable设计原则

flowchart TD
    A[Composable设计原则] --> B[单一职责]
    A --> C[输入为Ref]
    A --> D[返回Ref]
    A --> E[副作用清理]
    A --> F[命名规范use前缀]

    B --> B1[一个composable只做一件事]
    C --> C1[接收ref参数,支持响应式追踪]
    D --> D1[返回ref,保持响应式链路]
    E --> E1[onScopeDispose清理副作用]
    F --> F1[useXxx命名,语义清晰]

    style A fill:#4CAF50,color:#fff

3.2 通用分页Composable

/**
 * 通用分页查询composable
 * 封装分页逻辑,支持任意列表查询场景
 */
import { ref, reactive, type Ref } from 'vue'
import { ElMessage } from 'element-plus'

interface PaginationState<T> {
  list: Ref<T[]>
  loading: Ref<boolean>
  total: Ref<number>
  pagination: {
    pageNum: number
    pageSize: number
  }
  query: () => Promise<void>
  resetQuery: () => Promise<void>
  handleSizeChange: (size: number) => Promise<void>
  handleCurrentChange: (page: number) => Promise<void>
}

export function usePagination<T>(
  apiFn: (params: any) => Promise<any>,
  defaultParams: Record<string, any> = {}
): PaginationState<T> {

  const list = ref<T[]>([]) as Ref<T[]>
  const loading = ref(false)
  const total = ref(0)

  const pagination = reactive({
    pageNum: 1,
    pageSize: 10
  })

  const queryParams = reactive({ ...defaultParams })

  /** 执行查询 */
  const query = async () => {
    loading.value = true
    try {
      const { data } = await apiFn({
        ...queryParams,
        pageNum: pagination.pageNum,
        pageSize: pagination.pageSize
      })
      list.value = data.records
      total.value = data.total
    } catch (e: any) {
      ElMessage.error(e.message || '查询失败')
    } finally {
      loading.value = false
    }
  }

  /** 重置查询条件并重新查询 */
  const resetQuery = async () => {
    Object.assign(queryParams, { ...defaultParams })
    pagination.pageNum = 1
    await query()
  }

  const handleSizeChange = async (size: number) => {
    pagination.pageSize = size
    pagination.pageNum = 1
    await query()
  }

  const handleCurrentChange = async (page: number) => {
    pagination.pageNum = page
    await query()
  }

  return {
    list, loading, total, pagination,
    query, resetQuery, handleSizeChange, handleCurrentChange
  }
}

使用示例:

<script setup lang="ts">
import { usePagination } from '@/composables/usePagination'
import { getUserPage } from '@/api/system/user'

// 一行代码获得完整的分页能力
const {
  list: userList,
  loading,
  total,
  pagination,
  query,
  resetQuery,
  handleSizeChange,
  handleCurrentChange
} = usePagination<UserVO>(getUserPage, { status: 1 })

// 初始化查询
onMounted(() => query())
</script>

3.3 表单Composable

/**
 * 通用表单composable
 * 封装表单提交、重置、校验等通用逻辑
 */
import { ref, type Ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance } from 'element-plus'

interface FormOptions<T, R> {
  /** 新增API */
  createFn: (data: T) => Promise<R>
  /** 修改API */
  updateFn: (data: T) => Promise<R>
  /** 详情API */
  detailFn?: (id: number) => Promise<T>
  /** 成功回调 */
  onSuccess?: () => void
}

export function useForm<T extends Record<string, any>>(
  options: FormOptions<T, any>
) {
  const formRef = ref<FormInstance>()
  const formLoading = ref(false)
  const formData = ref<Partial<T>>({}) as Ref<Partial<T>>
  const mode = ref<'add' | 'edit'>('add')

  /** 打开表单 */
  const openForm = async (m: 'add' | 'edit', id?: number) => {
    mode.value = m
    formRef.value?.resetFields()
    if (m === 'edit' && id && options.detailFn) {
      formLoading.value = true
      try {
        const { data } = await options.detailFn(id)
        formData.value = data
      } finally {
        formLoading.value = false
      }
    }
  }

  /** 提交表单 */
  const submitForm = async () => {
    const valid = await formRef.value?.validate().catch(() => false)
    if (!valid) return

    formLoading.value = true
    try {
      if (mode.value === 'add') {
        await options.createFn(formData.value as T)
        ElMessage.success('新增成功')
      } else {
        await options.updateFn(formData.value as T)
        ElMessage.success('修改成功')
      }
      options.onSuccess?.()
    } finally {
      formLoading.value = false
    }
  }

  return { formRef, formLoading, formData, mode, openForm, submitForm }
}

3.4 字典数据Composable

/**
 * 字典数据composable
 * 自动加载并缓存字典数据
 */
import { ref, onMounted } from 'vue'

// 全局字典缓存,避免重复请求
const dictCache = new Map<string, DictItem[]>()

export function useDict(dictType: string) {
  const options = ref<DictItem[]>([])
  const loading = ref(false)

  const loadDict = async () => {
    // 优先从缓存获取
    if (dictCache.has(dictType)) {
      options.value = dictCache.get(dictType)!
      return
    }

    loading.value = true
    try {
      const { data } = await getDictDataApi(dictType)
      options.value = data
      dictCache.set(dictType, data)
    } finally {
      loading.value = false
    }
  }

  /** 根据字典值获取标签 */
  const getLabel = (value: string | number): string => {
    return options.value.find(item => item.value === String(value))?.label ?? ''
  }

  onMounted(loadDict)

  return { options, loading, getLabel }
}

四、TypeScript深度集成

4.1 API类型定义规范

/**
 * API类型定义 - 三层模型
 */

// 1. 请求参数类型
interface UserQueryDTO {
  username?: string
  status?: number
  pageNum: number
  pageSize: number
}

// 2. 响应数据类型
interface UserVO {
  id: number
  username: string
  nickname: string
  email: string
  phone: string
  status: number
  createTime: string
  roles: RoleVO[]
}

// 3. 表单提交类型
interface UserFormDTO {
  id?: number
  username: string
  nickname: string
  email: string
  phone: string
  roleIds: number[]
}

// 4. 通用响应包装
interface ApiResult<T> {
  code: number
  msg: string
  data: T
}

// 5. 分页响应
interface PageResult<T> {
  records: T[]
  total: number
  pageNum: number
  pageSize: number
}

4.2 泛型Composable类型约束

/**
 * 带类型的CRUD composable
 * 通过泛型约束确保类型安全
 */
import type { Ref } from 'vue'

interface CrudOptions<Q, F, V> {
  pageApi: (params: Q) => Promise<ApiResult<PageResult<V>>>
  detailApi: (id: number) => Promise<ApiResult<V>>
  createApi: (data: F) => Promise<ApiResult<void>>
  updateApi: (data: F) => Promise<ApiResult<void>>
  deleteApi: (ids: number[]) => Promise<ApiResult<void>>
}

export function useCrud<Q extends Record<string, any>, F, V>(
  options: CrudOptions<Q, F, V>
) {
  // 所有返回值都有完整类型推导
  const list: Ref<V[]> = ref([])
  const loading = ref(false)

  const queryPage = async (params: Q): Promise<void> => {
    loading.value = true
    try {
      const { data } = await options.pageApi(params)
      list.value = data.records
    } finally {
      loading.value = false
    }
  }

  return { list, loading, queryPage }
}

五、企业级架构实践

5.1 组件通信模式

flowchart TD
    subgraph 父子组件
        A[父组件] -->|props| B[子组件]
        B -->|emit| A
        B -->|defineExpose/ref| A
    end

    subgraph 跨层级组件
        C[祖先组件] -->|provide| D[后代组件]
        D -->|inject| C
    end

    subgraph 全局状态
        E[Pinia Store] -->|storeToRefs| F[任意组件]
    end

    subgraph 事件总线
        G[组件A] -->|mitt| H[组件B]
    end

    style A fill:#4CAF50,color:#fff
    style C fill:#2196F3,color:#fff
    style E fill:#FF9800,color:#fff

5.2 项目Composable分层架构

src/composables/
├── core/                    # 核心层:最通用的composable
│   ├── usePagination.ts     # 分页
│   ├── useForm.ts           # 表单
│   ├── useLoading.ts        # 加载状态
│   └── useDict.ts           # 字典
├── business/                # 业务层:特定业务域的composable
│   ├── useUser.ts           # 用户相关
│   ├── usePermission.ts     # 权限相关
│   └── useMenu.ts           # 菜单相关
└── platform/                # 平台层:与平台特性耦合的composable
    ├── useWebSocket.ts      # WebSocket
    └── useTheme.ts          # 主题

5.3 Composable组合模式

<script setup lang="ts">
/**
 * 用户管理页面 - 通过组合多个composable构建
 * 每个composable负责一块独立的功能域
 */
import { usePagination } from '@/composables/core/usePagination'
import { useForm } from '@/composables/core/useForm'
import { useDict } from '@/composables/core/useDict'
import { getUserPage, createUser, updateUser, getUserDetail, deleteUser } from '@/api/system/user'

// 分页查询
const { list, loading, total, pagination, query, resetQuery } = usePagination(getUserPage)

// 表单操作
const { formRef, formData, formLoading, openForm, submitForm } = useForm({
  createFn: createUser,
  updateFn: updateUser,
  detailFn: getUserDetail,
  onSuccess: query  // 提交成功后自动刷新列表
})

// 字典数据
const { options: statusOptions, getLabel: getStatusLabel } = useDict('sys_status')

// 删除操作
const handleDelete = async (id: number) => {
  await ElMessageBox.confirm('确认删除该用户?', '提示')
  await deleteUser([id])
  ElMessage.success('删除成功')
  query()
}

onMounted(query)
</script>

结论与建议

Composition API核心优势总结

维度 Options API Composition API
代码组织 按选项类型分散 按功能关注点聚合
逻辑复用 Mixins(命名冲突) Composables(清晰来源)
类型推导 需额外装饰器 原生TypeScript支持
代码量 模板代码多 简洁精炼
可测试性 需挂载组件 纯函数可独立测试

最佳实践建议

  1. Composable粒度:遵循单一职责原则,一个composable只封装一个功能域
  2. 命名规范:统一使用use前缀,文件名与函数名保持一致
  3. 类型优先:先定义类型接口,再实现composable,确保类型安全
  4. 副作用管理:在onScopeDispose中清理定时器、事件监听等副作用
  5. 文档注释:每个composable添加JSDoc注释,说明参数、返回值和使用场景

相关资源链接