Vue 3 组合式 API 最佳实践:如何写出可维护的代码

Vue 3 组合式 API 最佳实践:如何写出可维护的代码


核心原则:组合式 API 不是"把选项拆开",而是"按关注点聚合"

很多团队从 Options API 迁移到 Composition API 后,只是把 datamethodscomputed 拆成了零散的 reffunction,代码反而更乱了。

问题不在 API 本身,在于组织方式


一、第一条铁律:按功能聚合,而非按选项类型聚合

❌ 反面教材:Options 思维的平移

javascript 复制代码
js
// 按类型拆分------这是 Options API 的习惯,不是 Composition API 的用法
const name = ref('')
const age = ref(0)
const getFullName = computed(() => `${name.value} ${age.value}`)
const updateName = () => { /* ... */ }
const updateAge = () => { /* ... */ }

✅ 正确做法:按功能模块聚合

javascript 复制代码
js
// useUser.js ------ 一个功能,一个文件
import { ref, computed } from 'vue'

export function useUser() {
  const name = ref('')
  const age = ref(0)

  const fullName = computed(() => `${name.value} ${age.value}`)

  function updateName(v) { name.value = v }
  function updateAge(v) { age.value = v }

  return { name, age, fullName, updateName, updateAge }
}

判断标准:如果你删除这个功能,应该只删一个文件,而不是从五个文件里各删一行。


二、Composables 的命名与边界

规则 说明
必须以 use 开头 useUseruseFetchuseTheme,这是社区约定,也是 Vue 生态的识别标志
只负责逻辑,不负责 UI Composable 返回数据和方法,不返回 JSX 或模板
可组合、可复用 如果逻辑只在一个组件里用,直接写在 <script setup> 里就好,别硬拆
接受参数,不读全局 依赖通过参数传入,不要在 composable 内部读 inject 以外的全局状态

三、<script setup> 的黄金结构

xml 复制代码
vue
<script setup lang="ts">
// 1. 引入 ------ 外部依赖
import { ref, computed, onMounted } from 'vue'
import { useUser } from '@/composables/useUser'
import { useFetch } from '@/composables/useFetch'

// 2. 逻辑层 ------ 所有 composable 调用
const { name, age, fullName } = useUser()
const { data, loading, error } = useFetch('/api/users')

// 3. 组件专属状态 ------ 只属于当前组件的东西
const showModal = ref(false)

// 4. 计算属性
const isAdult = computed(() => age.value >= 18)

// 5. 生命周期与副作用
onMounted(() => {
  console.log('mounted')
})

// 6. 对外暴露 ------ 给父组件用的
defineExpose({ showModal })
</script>

顺序不是装饰,是阅读路径:先看依赖什么,再看逻辑做什么,最后看组件自己干什么。


四、响应式的精准控制

4.1 ref vs reactive:别纠结,记住这个

场景 选它 原因
原始值(string/number/boolean) ref reactive 无法包裹原始值
明确知道是对象且不会整体替换 reactive 不用 .value,写起来干净
需要整体替换(如表单重置) ref ref.value = {} 一步到位
团队统一 ref 一致性 > 便利性,ref 适用所有场景

推荐:全团队统一用 ref,少一个决策点。

4.2 toReftoRefs:解构不丢失响应式

php 复制代码
js
// ❌ 丢失响应式
const state = reactive({ count: 0, name: 'vue' })
const { count } = state  // count 只是个普通数字

// ✅ 保持响应式
const { count } = toRefs(state)  // count 是 ref
// 或
const count = toRef(state, 'count')

五、Props 与 Emits 的类型安全

用 TypeScript 时,永远不要手写 props 定义

typescript 复制代码
ts
// ❌ 手动写,容易和实际不一致
const props = defineProps<{ title: string }>()

// ✅ 用 withDefaults + 泛型,类型推断自动对齐
const props = withDefaults(
  defineProps<{
    title: string
    count?: number
  }>(),
  { count: 0 }
)

// ✅ Emits 也一样
const emit = defineEmits<{
  (e: 'update', value: number): void
  (e: 'delete', id: string): void
}>()

六、副作用的隔离:watchEffect vs watch

API 何时用 特点
watchEffect 副作用依赖多个响应式值,且不关心旧值 自动追踪依赖,立即执行一次
watch 精确控制:特定值变化时执行,需要旧值 懒执行,可选 immediate: true
watch(..., { flush: 'post' }) 需要在 DOM 更新后执行 替代 nextTick 的大多数场景
javascript 复制代码
js
// 场景:name 变化时,同步到 localStorage
watch(name, (newVal) => {
  localStorage.setItem('name', newVal)
}, { flush: 'post' })  // DOM 更新完再存,避免覆盖

七、避坑清单

正确做法
Composable 里用 inject 读取全局状态 通过参数传入,保持纯函数特性
setup 里写几百行逻辑 拆成多个 composable,每个不超过 80 行
所有状态都用 ref 导致 .value 满天飞 确实会满天飞,这是代价,接受它
忘记 defineExpose 导致父组件拿不到方法 明确组件边界:哪些是内部的,哪些是对外的
computed 里写副作用 computed 必须是纯函数,有副作用用 watchEffect
循环依赖的 composable 抽离共享状态到第三个 composable,打破环

八、一个真实的文件结构参考

bash 复制代码
src/
├── composables/
│   ├── useUser.ts          # 用户相关逻辑
│   ├── useAuth.ts          # 认证逻辑
│   ├── useFetch.ts         # 通用请求封装
│   └── useTheme.ts         # 主题切换
├── views/
│   └── UserProfile.vue     # 只负责 UI + 调用 composable
└── App.vue

当你的 views 文件夹里每个文件不超过 100 行时,架构就对了。


结语

组合式 API 的最大价值不是"更灵活",而是让你有能力把代码按人能理解的方式组织起来

refreactive 只是工具,真正的最佳实践是:

每个文件只做一件事,每个函数只改一个地方,每次阅读只需要理解一个模块。

做到这三点,你的 Vue 3 代码就已经超过 90% 的项目了。

相关推荐
wuhen_n1 小时前
LangChain 自定义 Tool 封装:打造专属 AI 能力工具集
前端·langchain·ai编程
长大19881 小时前
彻底搞懂 JavaScript 事件循环
前端
橘猫走江湖1 小时前
Web 前端本地存储:localStorage 与 IndexedDB
前端·javascript·indexeddb
小强19881 小时前
CSS 布局进化史:从 Float 到 Flexbox 再到 Grid
前端
AKA__老方丈1 小时前
删除确认 Hook - 统一管理单删/批量删除的确认弹窗与执行
前端·javascript·vue.js
假如让我当三天老蒯1 小时前
React+TS 项目结构(自学项目用)
前端·react.js
yingyima1 小时前
Celery 分布式任务队列:我差点被这行代码坑死
前端
用户125758524361 小时前
XYGo Admin 即时通讯模块解析:基于 WebSocket 的企业级消息架构实践
前端
铁皮饭盒1 小时前
彩色命令行,Node21自带函数1行实现 ,Bun也兼容, 附Bun.color实现渐变色的代码
前端·后端