返回博客
技术 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 按 datamethodscomputed 组织代码,同一逻辑的代码被拆散到不同选项中;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 APIComposition 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> 中使用 definePropsdefineEmits 进行类型声明,这是构建健壮组件的基石。

<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 带来了更灵活的代码组织方式和更强大的逻辑复用能力。核心要点:

  1. 优先使用 <script setup>:语法简洁,性能更优
  2. 用 TypeScript 声明 Props/Emits:类型安全是大型项目的基础
  3. 掌握 provide/inject:解决跨层级通信,配合 InjectionKey 保证类型安全
  4. 提取组合式函数:替代 Mixin,实现清晰的逻辑复用
  5. 遵循组件设计原则:单一职责、合理粒度、选择恰当的通信方式

这套实践体系能让 Vue 3 项目在面对复杂业务需求时保持代码的可维护性和可扩展性。