组合式 API 的复用逻辑基础
Vue3 组合式 API 的核心价值在于逻辑复用。选项式 API 下,同一个功能的代码被拆散到 data、methods、computed、watch 各处,跨组件复用只能靠 Mixin,而 Mixin 的命名冲突和来源不透明是公认的痛点。组合式 API 用普通 JavaScript 函数封装可复用逻辑,函数名就是命名空间,变量来源清晰可追溯。
一个标准的 Composable 函数结构:
// useRequest.ts - 通用请求组合式函数
import { ref, shallowRef, type Ref } from 'vue'
interface UseRequestOptions<T> {
immediate?: boolean
initialData?: T
}
interface UseRequestReturn<T> {
data: Ref<T | undefined>
loading: Ref<boolean>
error: Ref<Error | null>
execute: (...args: any[]) => Promise<T | undefined>
}
export function useRequest<T>(
fetcher: (...args: any[]) => Promise<T>,
options: UseRequestOptions<T> = {}
): UseRequestReturn<T> {
const { immediate = false, initialData } = options
const data = shallowRef<T | undefined>(initialData)
const loading = ref(false)
const error = ref<Error | null>(null)
async function execute(...args: any[]): Promise<T | undefined> {
loading.value = true
error.value = null
try {
const result = await fetcher(...args)
data.value = result
return result
} catch (e) {
error.value = e as Error
return undefined
} finally {
loading.value = false
}
}
if (immediate) {
execute()
}
return { data, loading, error, execute }
}
组件库分层架构设计
企业级组件库需要清晰的分层,避免业务逻辑和 UI 逻辑混在一起:
src/components/
├── base/ # 基础组件:无业务属性,纯 UI
│ ├── BaseButton.vue
│ ├── BaseInput.vue
│ ├── BaseModal.vue
│ └── BaseTable.vue
├── business/ # 业务组件:组合基础组件 + 业务逻辑
│ ├── UserSelect.vue
│ ├── PermissionTag.vue
│ └── StatusBadge.vue
├── composables/ # 组合式函数:纯逻辑,无 UI
│ ├── useRequest.ts
│ ├── usePagination.ts
│ ├── useForm.ts
│ └── usePermission.ts
└── utils/ # 工具函数
├── validators.ts
└── formatters.ts
分层原则:
- base 组件只接收 props 和 emit 事件,不依赖任何业务接口
- business 组件可以调用 composables,但不应直接调用 API
- composables 只封装逻辑,不依赖任何 Vue 组件
usePagination 通用分页逻辑实现
分页是后台管理系统出现频率最高的交互模式之一。将其抽成 Composable 后,任何列表页只需 3 行代码即可获得完整的分页能力:
// usePagination.ts
import { ref, computed, watch, type Ref } from 'vue'
interface PaginationState {
page: number
pageSize: number
total: number
}
export function usePagination(fetchFn: (params: any) => Promise) {
const pagination = ref<PaginationState>({
page: 1,
pageSize: 20,
total: 0,
})
const list = ref<any[]>([])
const loading = ref(false)
async function fetchList() {
loading.value = true
try {
const res = await fetchFn({
page: pagination.value.page,
pageSize: pagination.value.pageSize,
})
list.value = res.list
pagination.value.total = res.total
} finally {
loading.value = false
}
}
function handlePageChange(page: number) {
pagination.value.page = page
fetchList()
}
function handleSizeChange(size: number) {
pagination.value.pageSize = size
pagination.value.page = 1
fetchList()
}
function reset() {
pagination.value.page = 1
fetchList()
}
const totalPages = computed(() => {
return Math.ceil(pagination.value.total / pagination.value.pageSize)
})
return {
list,
loading,
pagination,
totalPages,
fetchList,
handlePageChange,
handleSizeChange,
reset,
}
}
useForm 表单逻辑封装
表单的校验、提交、重置逻辑在选项式 API 中非常冗长。用 Composable 封装后,表单组件只需关注模板:
// useForm.ts
import { reactive, ref } from 'vue'
type Validator = (value: any) => string | null
interface FieldConfig {
value: any
validators?: Validator[]
}
export function useForm<T extends Record<string, FieldConfig>>(config: T) {
const formData = reactive(
Object.fromEntries(
Object.entries(config).map(([key, field]) => [key, field.value])
)
)
const errors = reactive(
Object.fromEntries(
Object.keys(config).map(key => [key, null as string | null])
)
)
const submitting = ref(false)
function validate(): boolean {
let valid = true
for (const [key, field] of Object.entries(config)) {
if (field.validators) {
for (const validator of field.validators) {
const msg = validator(formData[key])
errors[key] = msg
if (msg) {
valid = false
break
}
}
}
}
return valid
}
async function handleSubmit(
submitFn: (data: typeof formData) => Promise<void>
): Promise<boolean> {
if (!validate()) return false
submitting.value = true
try {
await submitFn(formData)
return true
} catch {
return false
} finally {
submitting.value = false
}
}
function resetForm() {
for (const [key, field] of Object.entries(config)) {
formData[key] = field.value
errors[key] = null
}
}
return { formData, errors, submitting, validate, handleSubmit, resetForm }
}
使用示例:
const { formData, errors, handleSubmit, resetForm } = useForm({
username: { value: '', validators: [
v => v ? null : '用户名不能为空',
v => v.length >= 3 ? null : '用户名至少3个字符',
]},
email: { value: '', validators: [
v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? null : '邮箱格式不正确',
]},
})
async function onSubmit() {
const ok = await handleSubmit(async (data) => {
await api.createUser(data)
})
if (ok) {
message.success('创建成功')
resetForm()
}
}
组件文档与类型规范
组件库没有文档等于没有。每个组件和 Composable 必须有 TypeScript 类型和 JSDoc 注释:
/**
* 通用请求组合式函数
* @typeparam T - 响应数据类型
* @param fetcher - 请求函数,接收任意参数返回 Promise
* @param options - 配置项
* @returns 响应式数据、加载状态、错误信息和执行函数
*
* @example
* ```ts
* const { data, loading, execute } = useRequest(
* (id: string) => api.getUser(id),
* { immediate: false }
* )
* ```
*/
Props 定义也必须完整标注类型和默认值:
interface BaseButtonProps {
/** 按钮类型 */
type?: 'primary' | 'secondary' | 'danger' | 'ghost'
/** 按钮尺寸 */
size?: 'small' | 'medium' | 'large'
/** 是否禁用 */
disabled?: boolean
/** 是否加载中 */
loading?: boolean
}
const props = withDefaults(defineProps<BaseButtonProps>(), {
type: 'secondary',
size: 'medium',
disabled: false,
loading: false,
})
组件库的工程化还包括按需导入支持。通过 unplugin-vue-components 配合组件库的 resolver,业务项目使用组件时无需手动 import,打包时自动按需引入,减少 bundle 体积。整个组件库的发布、版本管理和变更日志可以用 changesets 工具链管理,这里不再展开。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-shi-zhan-gou-jian-gao-fu-yong-ye-wu-zu/