Vue 3 Composition API 与 script setup
Vue 3 Composition API 与 <script setup>
系列导航:Vue 3 现代开发指南
上一篇:Vue 3 现代工程搭建
下一篇:Vue 3 响应式基础
Composition API 的核心价值不是“代码更新”,而是把同一业务能力相关的状态、派生值、副作用和操作组织在一起,并将它们抽成可复用的组合式函数。Options API 在 Vue 3 中仍然受支持;小组件使用哪一种都可以,但新建 TypeScript 项目通常优先采用 <script setup>。
一、两种 API 的组织方式
Options API 按选项类型组织代码:
<script lang="ts">
export default {
data() {
return { count: 0 }
},
computed: {
doubled(): number {
return this.count * 2
},
},
methods: {
increment() {
this.count++
},
},
}
</script>
Composition API 按功能组织代码:
<script setup lang="ts">
import { computed, ref } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
</script>
后者在功能复杂、需要复用逻辑或类型较多时更容易维护。它并不意味着所有代码都要塞进一个巨大 setup();逻辑变长时,应抽成 useXxx() 组合式函数。
二、普通 setup() 做了什么
理解底层形式有助于读旧代码:
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'CounterPanel',
setup() {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
},
})
</script>
setup() 返回的属性可以在模板中使用。在 setup() 里不要依赖组件实例 this;Composition API 通过导入函数和显式参数访问能力。
<script setup> 是编译时语法糖,它自动把顶层绑定暴露给模板:
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button type="button" @click="increment">
{{ count }}
</button>
</template>
这不是运行时把所有变量挂到组件实例上,而是编译器直接生成对应代码,因此更简洁,也有更好的类型推断。
三、组件名怎么处理
组件通常会根据 .vue 文件名自动推断名称。例如 UserProfile.vue 会得到 UserProfile 名称,所以大多数文件不需要额外声明。
确实需要显式选项时,使用内置宏 defineOptions():
<script setup lang="ts">
defineOptions({
name: 'UserProfile',
inheritAttrs: false,
})
</script>
Vue 3.3 起已经内置 defineOptions(),不需要 vite-plugin-vue-setup-extend,也不应为了写组件名再增加一个普通 <script> 块。
四、编译器宏不需要导入
以下常用 API 是 <script setup> 编译器宏:
defineProps()defineEmits()defineModel()defineExpose()defineOptions()defineSlots()withDefaults()
它们只在 SFC 编译阶段存在,不要写:
// 不需要,也不应该这样导入
import { defineProps, defineEmits } from 'vue'
而 ref、computed、watch、onMounted、provide 等是运行时 API,必须从 vue 导入。
五、一个完整的 TypeScript 组件
下面的搜索面板同时展示 Props、Emits、响应式状态和派生值:
<script setup lang="ts">
import { computed, ref } from 'vue'
interface Props {
initialQuery?: string
minLength?: number
}
const { initialQuery = '', minLength = 2 } = defineProps<Props>()
const emit = defineEmits<{
search: [query: string]
clear: []
}>()
const query = ref(initialQuery)
const normalizedQuery = computed(() => query.value.trim())
const canSubmit = computed(
() => normalizedQuery.value.length >= minLength,
)
function submit() {
if (canSubmit.value) {
emit('search', normalizedQuery.value)
}
}
function clear() {
query.value = ''
emit('clear')
}
</script>
<template>
<form @submit.prevent="submit">
<input v-model="query" type="search" aria-label="搜索关键词" />
<button type="submit" :disabled="!canSubmit">搜索</button>
<button type="button" @click="clear">清空</button>
</form>
</template>
Vue 3.5 中,从 defineProps() 解构得到的变量会保持响应式。默认值也可以直接写在解构表达式中;这比旧教程一律使用 withDefaults() 更自然。
六、什么时候抽组合式函数
当一段逻辑满足以下任一条件时,可以抽成 useXxx():
- 被两个以上组件复用;
- 同时包含状态、计算属性、监听和清理逻辑;
- 组件已经难以一眼看出 UI 事件和业务流程;
- 逻辑可以独立测试。
例如:
// composables/useCounter.ts
import { computed, ref } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
function increment(step = 1) {
count.value += step
}
return { count, doubled, increment }
}
组件中直接使用:
<script setup lang="ts">
import { useCounter } from '@/composables/useCounter'
const { count, doubled, increment } = useCounter(1)
</script>
<template>
<p>{{ count }} × 2 = {{ doubled }}</p>
<button type="button" @click="increment()">加一</button>
</template>
组合式函数以 use 开头是生态约定,不是 Vue 的强制语法。
七、常见误区
1. 把 Composition API 当成必须重写的理由
Options API 没有被弃用。稳定的旧组件不必为了“现代化”全部重写。优先在新增功能、难复用逻辑和类型问题明显的区域渐进迁移。
2. 在模板里写过多业务逻辑
模板适合声明视图。长条件、数组处理和多步表达式应放进 computed 或函数中。
3. 滥用响应式
不会影响模板或其他副作用的数据用普通 const 即可。并非所有变量都需要 ref()。
4. 一个组合式函数做所有事情
usePage() 如果同时管理请求、权限、弹窗、表格和埋点,只是把大组件搬到了大函数里。组合式函数仍应有清晰、单一的职责。
小结
<script setup> 是当前 Vue 3 + TypeScript 项目的默认表达方式。组件名通常由文件名推断,编译器宏无需导入;功能变复杂时,用组合式函数按业务能力拆分,而不是按 data、methods、watch 的类别拆分。