Skip to content

组织 Composition API 代码

Composition API(组合式 API)非常强大,但它带来一个 Options API 时代不存在的新难题:代码该怎么组织? Options API 有一套天然的结构约束 datacomputedmethodswatch 各就各位,你几乎不用思考「放哪里」。

而 Composition API 把这份自由完全交给了开发者,反而容易产生选择疲劳(choice fatigue)和结构混乱。

很多从 Options API 转过来的人会下意识地把 <script setup> 里的代码「按选项类型」堆放,所有 ref 放一起、所有 computed 放一起、所有函数放一起。

这恰恰是最常见、也最容易埋雷的错误。

本文围绕这个痛点,展示如何一步步重构成可维护、可复用的结构。

Options API vs Composition API

两者的核心差异不在语法,而在代码的组织维度

维度Options APIComposition API
组织方式按「选项类型」强制分组由开发者自由决定
同一功能的代码被拆散到 data / methods / computed可以聚在一起
复用逻辑mixins(易冲突)composable(清晰可组合)
上手心智负担低(有固定套路)高(需要自己定规矩)

Composition API 的最大优势本应是「把同一个功能相关的代码聚拢在一起」,但如果沿用 Options API 的思维去摆放代码,这个优势就被彻底浪费了。

常见错误:按「选项」分组

假设有一个消息(message)相关的功能,同时还带有一个开关状态。按 Options API 惯性写出来的 <script setup> 往往长这样:

vue
<script setup lang="ts">
// ❌ 所有 ref 堆在一起
const message = ref('')
const messages = ref<string[]>([])
const isOpen = ref(false)

// ❌ 所有 computed 堆在一起
const messageCount = computed(() => messages.value.length)
const hasMessages = computed(() => messages.value.length > 0)

// ❌ 所有函数堆在一起
function addMessage() {
  messages.value.push(message.value)
  message.value = ''
}
function toggle() {
  isOpen.value = !isOpen.value
}
</script>

当组件功能变多时,一个功能相关的状态、计算属性和方法被割裂到三个不同区域

想读懂「消息」这块逻辑,必须在文件里上下反复横跳;而「开关」逻辑也和消息逻辑交错在一起。

文件越长,这种「意大利面式代码(spaghetti code)」的维护成本越高。

这种分组方式让「相关的东西」离得很远,「无关的东西」却挨在一起,与 Composition API 的设计初衷背道而驰。

重构第一步:按逻辑关注点分组

第一步不需要任何新 API,只是把代码按功能(feature)而非按选项类型重新排列,让相关的 ref、computed、函数聚在一块:

vue
<script setup lang="ts">
// 消息相关
const message = ref('')
const messages = ref<string[]>([])
const messageCount = computed(() => messages.value.length)
const hasMessages = computed(() => messages.value.length > 0)

function addMessage() {
  messages.value.push(message.value)
  message.value = ''
}

// 开关相关
const isOpen = ref(false)
function toggle() {
  isOpen.value = !isOpen.value
}
</script>

仅仅是重新分区,可读性就已明显提升:想改消息逻辑,只看第一块即可;想改开关,只看第二块。这一步其实就已经解决了大部分混乱问题,但还没到终点。

重构第二步:抽取成 composable

当某块逻辑足够独立、甚至可能被其他组件复用时,就可以把它抽成一个 composable。以消息逻辑为例:

ts
// composables/useMessages.ts
export function useMessages() {
  const message = ref('')
  const messages = ref<string[]>([])

  const messageCount = computed(() => messages.value.length)
  const hasMessages = computed(() => messages.value.length > 0)

  function addMessage() {
    messages.value.push(message.value)
    message.value = ''
  }

  return {
    message,
    messages,
    messageCount,
    hasMessages,
    addMessage
  }
}

组件里就变得非常干净:

vue
<script setup lang="ts">
const { message, messages, messageCount, hasMessages, addMessage } = useMessages()
</script>

至于开关这类通用逻辑,甚至无需自己写,直接用 VueUse 的 useToggle

ts
import { useToggle } from '@vueuse/core'

const [isOpen, toggle] = useToggle(false)

抽 composable 的判断标准通常是「这段逻辑是否有清晰的职责边界」和「是否可能被复用或独立测试」。

并非所有逻辑都值得抽,过度拆分同样会增加跳转成本。

内联 composable(in-line composable)

抽 composable 有时会有个副作用:为了一小段只在当前组件用一次的逻辑,专门新建一个文件,反而增加了跳转成本。

下面介绍了一个来自 Evan You 的模式,内联 composable,即把 composable 直接定义在同一个组件文件内,而不抽到外部文件。

vue
<script setup lang="ts">
// 直接在组件内定义,不导出、不建新文件
function useMessages() {
  const message = ref('')
  const messages = ref<string[]>([])
  const messageCount = computed(() => messages.value.length)

  function addMessage() {
    messages.value.push(message.value)
    message.value = ''
  }

  return { message, messages, messageCount, addMessage }
}

function useToggleState() {
  const isOpen = ref(false)
  const toggle = () => { isOpen.value = !isOpen.value }
  return { isOpen, toggle }
}

// 在 setup 顶层调用,组装组件
const { message, messages, messageCount, addMessage } = useMessages()
const { isOpen, toggle } = useToggleState()
</script>

这个模式的妙处在于兼顾了两端:

  • 逻辑聚合:每个功能的状态与方法被自然封装在一个函数里,边界清晰;
  • 无跳转成本:逻辑仍在同一文件,不必为一次性使用而新建文件;
  • 易于演进:当某个内联 composable 确实需要复用时,把它整段剪切到独立文件即可,几乎零改动。

常见案例:组织 Composition API 代码的可执行步骤

  1. 停止「按选项类型分组」不要再把所有 ref / computed / 函数各自扎堆。
  2. 先按逻辑关注点重排代码,让同一功能的状态、计算、方法聚在一起。
  3. 用注释或空行为每个功能块划分清晰边界。
  4. 对职责独立、可能复用的逻辑,抽取为外部 composable(如 useMessages)。
  5. 通用逻辑优先复用现成方案,如 VueUse 的 useToggle
  6. 对「只在本组件用一次」的逻辑,采用内联 composable,定义在组件文件内。
  7. 当内联 composable 需要复用时,再整段迁移到独立文件。

注意事项

事项说明
别照搬 Options API 思维按选项类型分组会割裂同一功能的代码,制造意大利面
分组优先,抽取其次仅重排逻辑就能解决大部分可读性问题,未必都要抽 composable
内联 composable 不必导出它只是组织手段,定义在组件内即可,需要复用再外移
避免过度拆分为极小逻辑滥建文件会增加跳转与心智成本
善用 VueUseuseToggle 等现成 composable 能省掉重复样板代码
命名遵循 useXxx与 composable 约定保持一致,也便于团队识别

组织 Composition API 代码的核心心法其实只有一句:让相关的代码待在一起,让无关的代码彼此分开。