Vue 3 Composition API 企业级架构:Composables 复用 + TypeScript 深度集成
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支持 |
| 代码量 | 模板代码多 | 简洁精炼 |
| 可测试性 | 需挂载组件 | 纯函数可独立测试 |
最佳实践建议
- Composable粒度:遵循单一职责原则,一个composable只封装一个功能域
- 命名规范:统一使用
use前缀,文件名与函数名保持一致 - 类型优先:先定义类型接口,再实现composable,确保类型安全
- 副作用管理:在
onScopeDispose中清理定时器、事件监听等副作用 - 文档注释:每个composable添加JSDoc注释,说明参数、返回值和使用场景