Vue3 Composables设计实战:从逻辑复用到组件库架构搭建

Vue3 Composables设计到底该怎么写

Vue3组合式API的核心价值不是setup语法糖,而是Composables——可复用的状态逻辑单元。但实际项目中大量Composables变成了”把代码从组件搬到函数”的搬运工,既没有做到逻辑内聚,也没有实现真正的复用。本文从设计原则出发,结合组件库架构实践,拆解Composables的正确写法。

Composables设计的三个核心原则

好的Composable满足三个标准:输入明确、副作用可控、返回值可组合。反模式是把组件里的refwatch直接搬到函数里,变成一个没有封装边界的逻辑碎片。

// 反模式:逻辑碎片化,依赖外部状态
export function useUserList() {
  // 隐式依赖外部store
  const store = useUserStore()
  const list = ref([])
  // 副作用散落在函数体外
  onMounted(async () => {
    list.value = await store.fetchAll()
  })
  return { list }
}

问题在哪?调用者无法控制请求时机,无法传参,无法复用在不同场景。

// 正确模式:输入明确,副作用可控
export function useUserList(options: {
  pageSize?: number
  immediate?: boolean
}) {
  const { pageSize = 20, immediate = true } = options
  const list = ref<User[]>([])
  const loading = ref(false)
  const error = ref<Error | null>(null)
  const page = ref(1)
  const total = ref(0)

  async function fetch(currentPage = 1) {
    loading.value = true
    error.value = null
    try {
      const res = await api.getUsers({ page: currentPage, pageSize })
      list.value = res.data
      total.value = res.total
      page.value = currentPage
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  // immediate控制是否自动执行
  if (immediate) {
    onMounted(() => fetch())
  }

  return { list, loading, error, page, total, fetch }
}

调用方决定何时请求,可以传参控制分页,还能在不同组件中独立使用。

响应式状态管理:避免Ref套Ref陷阱

Composable返回值混用refreactive是常见混乱源。建议统一用ref返回,原因:ref可以替换整个值,reactive不能替换引用。

// 返回reactive的问题
export function useForm() {
  const form = reactive({ name: '', email: '' })
  const reset = () => {
    // 这里无法重置——form = { name: '', email: '' } 丢失响应式
    form.name = ''
    form.email = ''
  }
  return { form, reset }
}

// 用ref重构——可以整体替换
export function useForm() {
  const form = ref({ name: '', email: '' })
  const reset = () => {
    form.value = { name: '', email: '' }  // 直接替换,保留响应式
  }
  return { form, reset }
}

Composable组合:构建业务逻辑积木

真正的复用发生在Composable之间的组合。一个复杂表单场景可以这样拆分:

// 基础Composable:表单校验
export function useFormValidation<T extends Record<string, any>>(
  form: Ref<T>,
  rules: Record<keyof T, (v: any) => string | true>
) {
  const errors = ref<Record<keyof T, string>>({} as any)
  const validateField = (key: keyof T) => {
    const rule = rules[key]
    const result = rule(form.value[key])
    errors.value[key] = result === true ? '' : result
    return result === true
  }
  const validate = () => {
    let valid = true
    for (const key in rules) {
      if (!validateField(key)) valid = false
    }
    return valid
  }
  return { errors, validate, validateField }
}

// 基础Composable:防抖请求
export function useDebouncedRequest<T>(
  requestFn: () => Promise<T>,
  delay = 300
) {
  const timer = ref<ReturnType<typeof setTimeout>>()
  const data = ref<T | null>(null)
  const loading = ref(false)

  const execute = () => {
    clearTimeout(timer.value)
    timer.value = setTimeout(async () => {
      loading.value = true
      try {
        data.value = await requestFn()
      } finally {
        loading.value = false
      }
    }, delay)
  }

  onUnmounted(() => clearTimeout(timer.value))
  return { data, loading, execute }
}

// 业务Composable:组合基础能力
export function useUserForm() {
  const form = ref({ name: '', email: '', role: 'user' })
  const { errors, validate } = useFormValidation(form, {
    name: v => v.length >= 2 || '姓名至少2个字符',
    email: v => /^[^\s@]+@[^\s@]+$/.test(v) || '邮箱格式不正确',
    role: v => ['user', 'admin'].includes(v) || '角色无效'
  })
  const { loading, execute } = useDebouncedRequest(
    () => api.createUser(form.value),
    500
  )
  const submit = () => {
    if (validate()) execute()
  }
  return { form, errors, loading, submit }
}

组件库中的Composables架构设计

在组件库场景中,Composables需要处理更复杂的问题:跨组件通信、可覆盖的默认行为、TypeScript类型推导。

// 组件库Composable标准模板
export interface UsePopoverOptions {
  trigger?: 'click' | 'hover' | 'focus'
  placement?: 'top' | 'bottom' | 'left' | 'right'
  offset?: number
  disabled?: Ref<boolean>
  onOpen?: () => void
  onClose?: () => void
}

export function usePopover(target: Ref<HTMLElement | undefined>, options: UsePopoverOptions = {}) {
  const {
    trigger = 'click',
    placement = 'bottom',
    offset = 8,
    disabled = ref(false),
    onOpen,
    onClose
  } = options

  const visible = ref(false)
  const popoverStyle = ref<Record<string, string>>({})
  let cleanup: (() => void) | null = null

  const updatePosition = () => {
    if (!target.value || !visible.value) return
    const rect = target.value.getBoundingClientRect()
    const positions = {
      top:    { top: `${rect.top - offset}px`, left: `${rect.left + rect.width / 2}px` },
      bottom: { top: `${rect.bottom + offset}px`, left: `${rect.left + rect.width / 2}px` },
      left:   { top: `${rect.top + rect.height / 2}px`, left: `${rect.left - offset}px` },
      right:  { top: `${rect.top + rect.height / 2}px`, left: `${rect.right + offset}px` },
    }
    popoverStyle.value = positions[placement]
  }

  const open = () => {
    if (disabled.value) return
    visible.value = true
    nextTick(updatePosition)
    onOpen?.()
  }

  const close = () => {
    visible.value = false
    onClose?.()
  }

  const toggle = () => visible.value ? close() : open()

  // 按Esc关闭
  useEventListener('keydown', (e: KeyboardEvent) => {
    if (e.key === 'Escape' && visible.value) close()
  })

  // 点击外部关闭
  onClickOutside(target, close)

  return { visible, popoverStyle, open, close, toggle }
}

这个Composable的设计要点:配置项全部可选且有合理默认值;事件回调让调用方可以覆盖默认行为;返回open/close/toggle方法而非内部绑定事件,让调用方决定触发方式。

跨组件状态共享:provide/inject模式

深层嵌套组件间共享状态,不要用全局store,用provide/inject做作用域隔离:

// 创建注入键
export const FORM_KEY: InjectionKey<ReturnType<typeof useUserForm>> = Symbol('form')

// 父组件provide
export function provideUserForm() {
  const formComposable = useUserForm()
  provide(FORM_KEY, formComposable)
  return formComposable
}

// 子组件inject
export function useInjectUserForm() {
  const form = inject(FORM_KEY)
  if (!form) throw new Error('useInjectUserForm must be used inside a FormProvider')
  return form
}

这种方式下,同一页面可以存在多个独立的表单实例,互不干扰。比Pinia更适合局部作用域的状态共享。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3composables-she-ji-shi-zhan-cong-luo-ji-fu-yong-dao-zu/

(0)
小编小编
上一篇 11小时前
下一篇 10小时前

相关推荐