跳过导航,直达内容
2026-04-28 技术团队 10 分钟

Vue 3 Composition API 高级模式与最佳实践

Vue.js前端最佳实践
Vue 3 Composition API 高级模式与最佳实践

Vue 3 已经普及,但在代码评审里我们仍经常看到"用 Composition API 写的 Options API":所有逻辑堆在一个 setup 里、composable 只是换个地方写全局变量、返回值又深又散。这篇讲我们内部评审时的三条标准和几个常踩的坑。

先看三个反模式

一、setup 变成新的 data + methods。把原来 Options API 里的每一项按顺序搬进 setup,只是换了语法,没有换组织方式。判断办法:如果 setup 超过百行、且能按"变量 / 方法 / 生命周期"分块,说明它没有按照"功能"组织。

二、composable 里藏全局状态。在 composable 的模块作用域里写 const state = ref(),看起来是"复用的 composable",实际是所有组件共享同一份状态——谁改了都影响别人,而且测试之间会互相污染。跨组件共享就该显式用 Pinia;只在组件内复用的逻辑,状态要写在函数内部。

三、返回值又深又散。返回一个嵌套对象,调用方要写 const { list } = useProducts(); list.items.value 这类长链,改名时到处爆红。我们的约定是:平铺返回,并在返回的 ref 上明确命名(items、total、isLoading),需要分组时返回多个 composable。

怎么切分 composable

切分依据是功能域,不是代码类型。一个好用的判断办法:假设这个页面要删掉某个功能(比如"导出"),你能只删掉一个 composable 与一处调用吗?能,就说明切得对。

  • useUserAuth:登录态、权限判断、token 刷新
  • useProductList:列表数据、分页、筛选、排序
  • useFormValidation:校验规则、错误状态、提交处理
  • useMediaQuery:断点检测

命名一律 use 前缀(社区约定,也是 ESLint 能自动检查的部分);一个 composable 只做一件事,超过两百行就先想怎么拆。

状态该放哪:一张决策表

并非所有状态都需要中心化 Store。我们的决策顺序是:

  1. 只在本组件用 → ref / reactive
  2. 父子之间传递 → props + emit;层级深但只是透传 → provide / inject
  3. 跨页面、跨组件共享,且有复杂逻辑(缓存、乐观更新、多入口触发)→ Pinia

最常见的反模式是"过早抽象":一开始就把所有状态塞进 Store,结果是"改一个字段要翻三个文件"。延迟决策更好用——先写成组件内的 ref,真的需要共享时再上移。

异步数据:把三态写完整

列表页最容易出问题的不是请求本身,而是"错误闪现"和"无限 Loading"。把三态封装起来,模板就不会各写一套:

// composables/useAsyncData.ts
export function useAsyncData<T>(fetcher: () => Promise<T>) {
  const data = shallowRef<T | null>(null)
  const isLoading = ref(false)
  const error = shallowRef<Error | null>(null)

  async function execute() {
    isLoading.value = true
    error.value = null          // 每次重试前清掉旧错误,否则会"带着错误加载"
    try {
      data.value = await fetcher()
    } catch (e) {
      error.value = e as Error
    } finally {
      isLoading.value = false
    }
  }

  return { data, isLoading, error, execute }
}

更复杂的场景(缓存、去重、后台刷新、依赖变化自动重取),直接用 VueUse 的 useAsyncState 或 TanStack Query 的 Vue 版本,不要自己造。

三个容易漏的坑

  • watch 与监听器没清理。在 composable 里 addEventListener、setInterval、watch 外部数据源,都要在 onScopeDispose(或组件内 onUnmounted)里收尾,否则页面切来切去内存一直涨。
  • 用 shallowRef 存大数组后又直接改元素。浅层响应式改内部字段不会触发更新,要么整体替换、要么用 reactive/ref 并接受深层的开销。
  • 把 props 直接当本地状态改。props 是只读的,需要"可编辑副本"时用 toRef + 本地 ref(或用 defineModel 明确双向绑定)。

💡 评审清单

  • setup 能不能按"功能"而不是"选项"读下来?
  • composable 的模块作用域里有没有可变状态?(有就是全局单例)
  • 返回值是否平铺、命名是否让调用方一眼看懂?
  • 监听器、定时器、事件订阅有没有配对的清理?

我们做企业项目时,前端代码往往要交给客户的团队长期维护,所以更在意"半年后别人能不能改"。如果你手上的 Vue 项目正处在"能跑但没人敢动"的状态,我们在前端重构与规范治理上做过不少这类落地,欢迎联系我们。