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。我们的决策顺序是:
- 只在本组件用 →
ref/reactive - 父子之间传递 →
props+emit;层级深但只是透传 →provide/inject - 跨页面、跨组件共享,且有复杂逻辑(缓存、乐观更新、多入口触发)→ 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 项目正处在"能跑但没人敢动"的状态,我们在前端重构与规范治理上做过不少这类落地,欢迎联系我们。
