Vue3 组合式 API 实战:构建高复用业务组件库的架构设计与代码规范

组合式 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/

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

相关推荐