返回博客
setup 与
技术 2025年3月6日 7 分钟阅读 · 1590 字
Vue 3 组件设计与 Composition API 最佳实践
从组件通信到组合式函数,构建可复用、可维护的 Vue 3 组件体系
#Vue
#组件设计
#Composition API
本文由 AI 辅助生成,经人工审核发布
Composition API vs Options API:范式之争
Vue 3 引入 Composition API 并非要取代 Options API,而是解决后者在复杂组件中的痛点。Options API 按 data、methods、computed 组织代码,同一逻辑的代码被拆散到不同选项中;Composition API 则允许按逻辑关注点聚合代码。
// Options API:同一逻辑被拆散
export default {
data() {
return { searchQuery: '', results: [] }
},
computed: {
filteredResults() {
return this.results.filter(r => r.name.includes(this.searchQuery))
}
},
methods: {
async fetchResults() {
this.results = await api.search(this.searchQuery)
}
}
}
// Composition API:逻辑聚合在一起
import { ref, computed } from 'vue'
function useSearch() {
const searchQuery = ref('')
const results = ref([])
const filteredResults = computed(() =>
results.value.filter(r => r.name.includes(searchQuery.value))
)
async function fetchResults() {
results.value = await api.search(searchQuery.value)
}
return { searchQuery, filteredResults, fetchResults }
}
两种 API 的对比:
| 维度 | Options API | Composition API |
|---|---|---|
| 代码组织 | 按选项类型分散 | 按逻辑关注点聚合 |
| 逻辑复用 | Mixin(命名冲突风险) | 组合式函数(清晰明确) |
| TypeScript 支持 | 类型推导较弱 | 完整类型支持 |
| 学习曲线 | 平缓,上手快 | 需理解响应式原理 |
| 适用场景 | 中小型项目 | 大型复杂项目 |
setup 与 <script setup>:从冗长到简洁
Vue 3 早期需要显式定义 setup 函数并手动返回模板需要的内容。<script setup> 是编译期语法糖,大幅简化写法:
<script setup>
import { ref, computed } from 'vue'
import UserProfile from './UserProfile.vue'
// 自动暴露给模板,无需 return
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++
</script>
<template>
<button @click="increment">{{ count }} / {{ double }}</button>
<UserProfile />
</template>
<script setup> 的核心优势:
- 组件导入即用:导入的组件直接在模板中使用,无需注册
- 自动暴露:顶层变量自动暴露给模板
- 更好的性能:编译器可做静态优化
- 更友好的 IDE 支持:类型推导和自动补全更准确
Props 与 Emits 类型声明
在 <script setup> 中使用 defineProps 和 defineEmits 进行类型声明,这是构建健壮组件的基石。
<script setup lang="ts">
// 运行时声明方式
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 },
items: { type: Array, default: () => [] },
callback: { type: Function as PropType<(id: number) => void> }
})
// TypeScript 类型方式(推荐)
interface Props {
title: string
count?: number
items?: Item[]
status?: 'idle' | 'loading' | 'success' | 'error'
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => [],
status: 'idle'
})
// Emits 类型声明
const emit = defineEmits<{
(e: 'update', value: string): void
(e: 'delete', id: number): void
(e: 'select', item: Item): void
}>()
</script>
Props 校验最佳实践
const props = defineProps({
// 带自定义校验器
email: {
type: String,
required: true,
validator: (value) => /^[^@]+@[^@]+\.[^@]+$/.test(value)
},
// 多类型
value: [String, Number],
// 对象深层校验
user: {
type: Object as PropType<{ name: string; age: number }>,
required: true
}
})
provide / inject:跨层级通信
当组件嵌套很深时,逐层传递 props 既繁琐又脆弱。provide / inject 允许祖先组件向所有后代组件注入数据。
// 父组件:使用 provide 提供响应式数据
import { provide, ref, readonly } from 'vue'
const theme = ref('dark')
const toggleTheme = () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
// 只读注入,防止后代意外修改
provide('theme', readonly(theme))
provide('toggleTheme', toggleTheme)
// 后代组件:任意层级均可注入
import { inject } from 'vue'
const theme = inject('theme', 'light') // 带默认值
const toggleTheme = inject('toggleTheme', () => {})
类型安全的 provide/inject
import type { InjectionKey, Ref } from 'vue'
// 定义注入键,携带类型信息
export const ThemeKey: InjectionKey<Ref<string>> = Symbol('theme')
// 提供方
provide(ThemeKey, theme)
// 注入方 —— 自动获得正确类型
const theme = inject(ThemeKey) // Ref<string> | undefined
组合式函数(Composables):逻辑复用的终极方案
组合式函数是 Composition API 的杀手锏。它是一个普通函数,封装可复用的响应式逻辑,替代了 Vue 2 中问题重重的 Mixin。
标准组合式函数示例:鼠标位置追踪
// composables/useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMousePosition() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
<script setup>
import { useMousePosition } from '@/composables/useMousePosition'
const { x, y } = useMousePosition()
</script>
<template>
<p>鼠标位置:{{ x }}, {{ y }}</p>
</template>
异步数据获取组合式函数
import { ref, watchEffect } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
async function doFetch() {
loading.value = true
error.value = null
try {
const res = await fetch(url.value)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
watchEffect(doFetch)
return { data, error, loading, retry: doFetch }
}
组合式函数命名约定
| 规则 | 说明 | 示例 |
|---|---|---|
| 命名前缀 | 以 use 开头 | useMousePosition |
| 返回值 | 返回普通对象(非 ref 数组) | { x, y } 而非 [x, y] |
| 副作用清理 | 在 onUnmounted 中清理 | 移除事件监听器、定时器 |
| 参数灵活性 | 接受 ref 或原始值 | url 可以是 ref 或 string |
组件设计原则
1. 单一职责原则
每个组件只做一件事。如果一个组件超过 300 行,通常意味着需要拆分。
<!-- ❌ 不好的设计:用户列表组件做了太多事 -->
<template>
<div>
<input v-model="search" />
<ul>
<li v-for="user in filteredUsers" @click="edit(user)">
{{ user.name }}
</li>
</ul>
<UserEditForm v-if="editing" :user="selectedUser" />
<Pagination :total="total" @change="fetchUsers" />
</div>
</template>
<!-- ✅ 好的设计:拆分为职责单一的组件 -->
<template>
<div>
<SearchBar v-model="searchQuery" />
<UserList :users="filteredUsers" @select="onSelect" />
<UserEditDialog v-if="selectedUser" :user="selectedUser" />
<Pagination :total="total" @change="onPageChange" />
</div>
</template>
2. 合理的组件粒度
| 粒度类型 | 说明 | 示例 |
|---|---|---|
| 页面组件 | 路由对应完整页面 | UserPage.vue |
| 业务组件 | 特定业务功能块 | UserList.vue |
| 通用组件 | 跨业务复用 | BaseButton.vue |
| 基础组件 | UI 原子组件 | Icon.vue |
3. 通信方式选择指南
| 场景 | 推荐方案 |
|---|---|
| 父→子 | props |
| 子→父 | emits |
| 兄弟组件 | 状态提升 + props/emits 或事件总线 |
| 跨层级 | provide / inject |
| 全局状态 | Pinia |
| 模板 ref 操作 | defineExpose |
4. defineExpose 的使用
<script setup> 默认不暴露内部方法。需要父组件通过 ref 调用子组件方法时,使用 defineExpose:
<!-- 子组件 -->
<script setup>
import { ref } from 'vue'
const isOpen = ref(false)
const open = () => { isOpen.value = true }
const close = () => { isOpen.value = false }
defineExpose({ open, close, isOpen })
</script>
<!-- 父组件 -->
<script setup>
import { ref, onMounted } from 'vue'
import ChildDialog from './ChildDialog.vue'
const dialogRef = ref(null)
onMounted(() => {
dialogRef.value.open()
})
</script>
<template>
<ChildDialog ref="dialogRef" />
</template>
总结
Vue 3 的 Composition API 带来了更灵活的代码组织方式和更强大的逻辑复用能力。核心要点:
- 优先使用
<script setup>:语法简洁,性能更优 - 用 TypeScript 声明 Props/Emits:类型安全是大型项目的基础
- 掌握 provide/inject:解决跨层级通信,配合 InjectionKey 保证类型安全
- 提取组合式函数:替代 Mixin,实现清晰的逻辑复用
- 遵循组件设计原则:单一职责、合理粒度、选择恰当的通信方式
这套实践体系能让 Vue 3 项目在面对复杂业务需求时保持代码的可维护性和可扩展性。