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 并在组件悬停时显示。