Vue 3 大型项目中如何组织 Composition API 逻辑复用代码?

文章导读
接手一个 Vue 3 大型项目时,我通常会先全局搜索组件的 setup 函数。如果发现相同的数据请求逻辑、表单校验或状态管理在不同组件里反复写,这就是需要抽离成 composable 的信号。一个常见的场景是用户登录状态:多个页面都需要获取当前用户信息、检查 token 有效性,每次重复写 watch 和 onMounted 既容易遗漏清理,也让组件代码膨胀到难以维护。
📋 目录
  1. A 先确认现象:逻辑代码在多个组件里重复出现
  2. B 容易误判的地方:把强依赖生命周期的逻辑也抽出去
  3. C 建议的处理顺序:先按业务功能分文件,再命名返回值
  4. D 验证方法:组合调用后看组件代码行数和可测试性
  5. E 回滚和风险:注意 provide/inject 的响应式陷阱
  6. F 后续维护:定期检查 composable 的单一职责
A A

先确认现象:逻辑代码在多个组件里重复出现

接手一个 Vue 3 大型项目时,我通常会先全局搜索组件的 setup 函数。如果发现相同的数据请求逻辑、表单校验或状态管理在不同组件里反复写,这就是需要抽离成 composable 的信号。一个常见的场景是用户登录状态:多个页面都需要获取当前用户信息、检查 token 有效性,每次重复写 watchonMounted 既容易遗漏清理,也让组件代码膨胀到难以维护。

文件按功能模块存放,而非按生命周期。例如,`useUserAuth.ts` 管理登录注册,`useAsyncData.ts` 处理异步请求,`useFormValidation.ts` 做表单校验。每个 composable 只做一件事,返回必要的响应式状态和方法。避免单一 composable 超过 100 行,否则拆成多个子 composable,通过组合调用形成层级结构。

容易误判的地方:把强依赖生命周期的逻辑也抽出去

不少开发者容易把“重复”和“可复用”划等号。比如一个只在 onMounted 执行一次的初始化操作,如果它依赖组件内特定的 DOM 结构或 props,强行抽成 composable 反而会增加复杂度。素材1 里说得很清楚:

当一个状态或副作用在多个组件中重复出现,比如用户登录状态、列表数据加载、表单校验逻辑,就应当将其抽取为独立的 composable。判断条件是:如果同一段代码出现在两个以上组件,且不涉及模板特有逻辑,直接提取。注意,若逻辑强依赖组件生命周期(如仅在 mounted 时执行一次),仍需保留在组件内,但可将内部状态移动出来。

我的习惯是先列出一个“重复清单”,把每个重复片段标出来,再看它是否含有 refreactive 或副作用管理——如果仅仅是一串数值计算,用纯函数就够,不需要 composable。

建议的处理顺序:先按业务功能分文件,再命名返回值

一旦决定抽取,文件结构要按功能模块划分,而不是按生命周期堆砌。素材2 给出了很直观的约定:

文件按功能模块存放,而非按生命周期。例如,useUserAuth.ts 管理登录注册,useAsyncData.ts 处理异步请求,useFormValidation.ts 做表单校验。每个 composable 只做一件事,返回必要的响应式状态和方法。避免单一 composable 超过 100 行,否则拆成多个子 composable,通过组合调用形成层级结构。

我通常会在 composables/ 目录下建子目录,比如 composables/auth/composables/data/,每个子目录下放 index.ts 统一导出。这样寻找逻辑时不用漫无目的地翻文件。命名上必须遵守 use 前缀,返回值规范也容易被忽略,素材3 提醒了:

Vue 3 大型项目中如何组织 Composition API 逻辑复用代码?

统一使用 use 前缀,如 useCounter。返回值如果是单个值直接返回 ref,多个值返回普通对象。避免直接暴露内部 ref 的 .value,而是通过 getter 函数或计算属性暴露只读版本。例如 const count = ref(0); const double = computed(() => count.value * 2); 对外只返回 countdouble,不返回修改方法(方法单独返回)。

这个规范的好处是调用方可以清晰区分状态和方法——状态只读,方法可触发变化,不会出现组件误修改内部状态的情况。

验证方法:组合调用后看组件代码行数和可测试性

重构完成后,我会做两件事:第一,查看组件 setup 代码行数是否降到原本的 60% 以下,原来 200 行的组件应该变成 80 行左右,剩下的只是组装和传递参数。第二,对每个 composable 单独写单元测试。例如 useAsyncData 可以 mock API 接口,测试 loading、error、data 三种状态转换。如果 composable 内部没有混合组件生命周期,测试会非常简洁。第三方库如 @vue/test-utils 配合 vi.mock 就能完成。

回滚和风险:注意 provide/inject 的响应式陷阱

最怕的是抽离后出现状态不同步。特别是跨组件共享状态时,如果用了 provide 但传的是普通对象,子组件拿到的不会被响应。我见过不少项目因为 inject 忘记写默认值导致 undefined 报错。回滚方案很简单:如果发现某个 composable 引起了连锁崩溃,先把它内部的代码原样复制回组件,删除 composable 文件,并修复引用。但更好的办法是提前在 composable 内部添加 onUnmounted 清理监听器,防止内存泄漏。如果 composable 内部有 watchEffect 或定时器,一定要确保每次调用都创建独立作用域,否则切换路由时旧监听还在运行。

后续维护:定期检查 composable 的单一职责

随着项目迭代,composable 容易变成“垃圾桶”:从最初的一个 useUserAuth 慢慢塞进搜索历史、主题切换等功能。我建议每个 sprint 结束时做一次重构,把超过 150 行或职责不清晰的 composable 拆解。例如 useDataSource 可能同时处理分页、排序、筛选,这时就分拆成 usePaginationuseSortinguseFiltering,再用 useDataSource 统一组合。这样每个子 composable 都可以单独测试和复用,不会出现一个 bug 影响整个数据层。

以上是处理过程中比较稳妥的顺序和判断点。你可以根据自己的项目规模先从小范围开始,比如先抽一个 useFormValidation,观察两天再扩到其他模块。遇到边界情况时,宁可保留在组件内也先别硬抽,等重复第三次再动手更安全。