设计 Vue 组件的 10 个技巧

废话少叙,直接正题

1. 利用 TypeScript 定义 Props

使用 TypeScript 为组件 props 定义类型,以实现完全的类型安全、精准的自动补全,并创建自文档化的组件 API。

<!-- UserProfileCard.vue -->
<script setup lang="ts">
// 为你的 props 定义一个类型
interface UserProfileProps {
  userProfile: User
  showAvatar?: boolean
  avatarSize?: 'small' | 'medium' | 'large'
}

// 使用 withDefaults 设置默认值
const props = withDefaults(defineProps<UserProfileProps>(), {
  showAvatar: true,
  avatarSize: 'medium'
})
</script>

2. 为 Emits 添加类型以获得更好的开发体验

使用 TypeScript 为事件载荷添加类型,使响应事件变得直观且类型安全。

<!-- UsersList.vue -->
<script setup lang="ts">
// 定义事件类型
defineEmits<{
  'update:selected': [id: number] // 更新:选中
  'profile-view': [user: User] // 查看个人资料
}>()

// 当触发事件时
const emit = defineEmits(['update:selected', 'profile-view'])
function selectUser(user: User) {
  emit('update:selected', user.id)
  emit('profile-view', user)
}
</script>

3. 使用组合式函数 (Composables) 实现可复用逻辑

使用组合式函数提取和复用逻辑。

<!--UserProfileCard.vue-->
<script setup lang="ts">
// 包含稍复杂的逻辑
// 但可在任何有用户对象的地方使用

import { useUserStatus } from '@/composables/useUserStatus'

const props = defineProps<{ user: User }>()
const { isOnline, statusText } = useUserStatus(props.user) // 用户状态文本
</script>

这种方法创建了功能集中的模块,可在多个组件中使用,并有助于更好地聚焦组件定义。

4. 保持组件小巧且职责单一

将较大的组件拆分成具有明确职责的较小组件:

<!--UserDashboard.vue - 组合较小组件的父组件-->
<template>
  <div class="dashboard">
    <UserHeader :user="user" />
    <UserStats :statistics="userStats" /> <!-- 用户统计数据 -->
    <RecentActivity :activities="recentActivities" /> <!-- 最近活动 -->
  </div>
</template>

<script setup lang="ts">
import UserHeader from './UserHeader.vue'
import UserStats from './UserStats.vue'
import RecentActivity from './RecentActivity.vue'
import { useUserData } from '@/composables/useUserData'

const { user, userStats, recentActivities } = useUserData()
</script>

较小的组件更易于理解、测试和维护。

5. 使用 TypeScript 接口定义组件状态

为组件状态定义接口以提高类型安全性。

<script setup lang="ts">
import { ref } from 'vue'

interface TaskItem { // 任务项
  id: number
  title: string
  completed: boolean // 已完成
  dueDate?: Date // 截止日期
  priority: 'low' | 'medium' | 'high' // 优先级: 低 | 中 | 高
}

const tasks = ref<TaskItem[]>([])

function addTask(title: string, priority: TaskItem['priority'] = 'medium') {
  tasks.value.push({
    id: Date.now(),
    title,
    completed: false,
    priority
  })
}
</script>

这种方法能及早捕获错误并充当文档。

6. 使用计算属性 (Computed) 处理派生数据

优先使用计算属性,避免在模板中进行计算。

<script setup>
const user = ref({...})
const fullName = computed(()=> `${user.value.firstName} ${user.value.lastName}`) // 全名
</script>

<template>
<!-- ? 模板中的逻辑 -->
<div>{{ user.firstName + ' ' + user.value.lastName }}</div>

<!-- ? 使用计算属性 -->
<div>{{ fullName }}</div>
</template>

计算属性会被缓存,并且比模板表达式更具可读性。

7. 使用错误和加载状态处理异步操作

创建能处理所有状态的健壮组件。这种模式确保无论请求结果如何,都能提供良好的用户体验。

<script setup lang="ts">
import { ref } from 'vue'
import type { User } from '@/types'

const user = ref<User | null>(null)
const loading = ref(true) // 加载中
const error = ref<Error | null>(null) // 错误

async function fetchUserData(userId: string) {
  loading.value = true
  error.value = null

  try {
    const response = await fetch(`/api/users/${userId}`)
    if (!response.ok) throw new Error('获取用户数据失败')
    user.value = await response.json()
  } catch (e) {
    error.value = e instanceof Error ? e : new Error('未知错误')
    user.value = null
  } finally {
    loading.value = false
  }
}
</script>

<template>
  <div>
    <LoadingSpinner v-if="loading" /> <!-- 加载中显示加载图标 -->
    <ErrorMessage v-else-if="error" :message="error.message" @retry="fetchUserData" /> <!-- 出错显示错误信息并提供重试 -->
    <UserProfile v-else-if="user" :user="user" /> <!-- 有用户数据时显示用户资料 -->
    <EmptyState v-else message="无可用用户数据" /> <!-- 无数据时显示空状态 -->
  </div>
</template>

或者,使用 Suspense 组件在组件树中更靠上的单一位置处理加载状态。

8. 使用带插槽属性的类型化插槽 (Typed Slots)

利用类型化插槽实现灵活、类型安全的组件组合:

<!-- DataTable.vue -->
<script setup lang="ts">
interface TableColumn<T> { // 表格列
  key: keyof T
  label: string // 标签
}

defineProps<{
  data: T[]
  columns: TableColumn<T>[] // 列
}>()
</script>

<template>
  <table>
    <thead>
      <tr>
        <th v-for="column in columns" :key="column.key">{{ column.label }}</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="item in data" :key="item.id">
        <td v-for="column in columns" :key="column.key">
          <!-- 允许自定义单元格渲染 -->
          <slot :name="`cell-${String(column.key)}`" :item="item" :value="item[column.key]">
            {{ item[column.key] }}
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

<!-- 用法 -->
<DataTable :data="users" :columns="columns">
  <template #cell-status="{ value, item }"> <!-- 单元格-状态 -->
    <StatusBadge :status="value" :user="item" /> <!-- 状态徽章 -->
  </template>
</DataTable>

这种模式允许父组件自定义子组件内容,同时保持类型安全。

9. 使用 CSS 变量实现组件主题化

使用 CSS 变量使组件的样式具有灵活性:

<style scoped>
.button {
  --button-bg: var(--primary-color, #3490dc); /* 按钮背景色: 主色 */
  --button-text: var(--primary-text-color, white); /* 按钮文字色: 主文字色 */
  --button-radius: var(--border-radius, 4px); /* 按钮圆角: 边框圆角 */

  background-color: var(--button-bg);
  color: var(--button-text);
  border-radius: var(--button-radius);
  padding: 8px 16px;
  border: none;
  cursor: pointer;
}

.button:hover {
  --button-bg: var(--primary-hover-color, #2779bd); /* 悬停时按钮背景色: 主悬停色 */
}
</style>

这种方法允许组件样式适应不同的主题,同时保持组件封装性。或者使用 TailwindCSS 通过灵活的设计令牌(design tokens)来设计组件样式。

10. 使用 TypeScript 和 JSDoc 为组件添加文档

使用 TypeScript 和 JSDoc 创建自文档化的组件:

<script setup lang="ts">
/**
 * 以紧凑卡片格式显示用户信息
 *
 * @example
 * <UserProfileCard
 *   :user="currentUser"
 *   @profile-click="showFullProfile"
 * />
 */
import { type PropType } from 'vue'

/**
 * 用户数据结构
 */
export interface User {
  /** 用户的唯一标识符 */
  id: number
  /** 用户的显示名称 */
  name: string
  /** 用户头像图片的 URL */
  avatarUrl?: string
  /** 用户的当前状态 */
  status: 'online' | 'away' | 'offline' // 在线 | 离开 | 离线
}

const props = defineProps<{
  /** 要显示的用户对象 */
  user: User
  /** 是否显示详细信息 */
  detailed?: boolean
}>()

const emit = defineEmits<{
  /** 当点击个人资料卡片时触发 */
  'profile-click': [userId: number]
}>()
</script>

良好的文档使您的组件更易于维护和使用,并且所有现代 IDE 都会识别 JS Docs 并在组件悬停时显示。