Vue3 入门教程
组件最佳实践
本教程共 40 篇 · 第 28 篇 · 更新于 2026-07-29 · 约 6 分钟阅读
VueVue3 入门教程最佳实践组件设计单一职责命名规范
28. 组件最佳实践
本节目标:总结组件设计的核心原则,写出高质量、易维护的 Vue 组件。
单一职责原则
一个组件只做一件事。如果你发现一个组件的模板超过 200 行,或者 props/emits 超过 5 个,可能就该拆了。
<!-- ❌ 一个组件管太多 -->
<UserOrderList>
- 用户信息展示
- 订单列表渲染
- 分页逻辑
- 搜索过滤
- 导出功能
</UserOrderList>
<!-- ✅ 拆成职责清晰的小组件 -->
<UserProfile :user="user" />
<OrderList :orders="orders" />
<Pagination :total="total" v-model:page="page" />
<SearchBar v-model:keyword="keyword" />
拆分不是为了”少写代码”,而是为了”每个组件都好理解”。
合理的拆分维度
1. 按 UI 区域拆
页面 → 布局 → 区块 → 原子组件
2. 按功能职责拆
展示组件(只管渲染) vs 容器组件(管数据和逻辑)
3. 按复用频率拆
一个片段在三个地方用 → 抽成组件 一个片段在一个地方用但很长 → 也可以抽,便于阅读
Tip不要过度拆分。如果一个组件只在一个地方用且只有十几行,拆出来反而增加认知负担。
命名规范
组件文件名
用 PascalCase:UserCard.vue、AppHeader.vue
组件注册名
也用 PascalCase,模板中自动兼容 kebab-case:
app.component('UserCard', UserCard)
<!-- 以下两种都行 -->
<UserCard />
<user-card />
事件名
定义用 camelCase,监听用 kebab-case:
emit('itemClick', item) // 定义
<MyList @item-click="handler" /> // 监听
Props 命名
JavaScript 中 camelCase,模板中 kebab-case:
defineProps({ greetingMessage: String })
<MyComponent greeting-message="hello" />
Props 设计原则
1. 用对象语法,带类型和默认值
// ❌ 太随便
defineProps(['title'])
// ✅ 清晰明确
defineProps({
title: {
type: String,
required: true
},
size: {
type: String,
default: 'medium',
validator: (v) => ['small', 'medium', 'large'].includes(v)
}
})
2. 避免传巨大的对象
<!-- ❌ 传整个用户对象,组件不知道用了什么 -->
<UserCard :user="user" />
<!-- ✅ 只传需要的字段 -->
<UserCard :name="user.name" :avatar="user.avatar" :role="user.role" />
当然,如果确实所有字段都要用,传对象也是合理的。
事件设计原则
1. 事件名要描述发生了什么
// ❌ 太模糊
emit('change')
emit('update')
// ✅ 清晰描述
emit('status-change', newStatus)
emit('item-delete', itemId)
2. 载荷尽量用原始值
// ❌ 传整个组件内部对象
emit('select', internalItem)
// ✅ 传最小必要数据
emit('select', itemId)
插槽设计原则
1. 暴露足够的灵活性
<!-- ❌ 完全不让外部控制 -->
<template>
<div class="card">
<h3>{{ title }}</h3>
<p>{{ content }}</p>
</div>
</template>
<!-- ✅ 用插槽让外部决定 -->
<template>
<div class="card">
<slot name="title">
<h3>{{ title }}</h3> <!-- 后备内容 -->
</slot>
<slot />
</div>
</template>
2. 用作用域插槽暴露内部数据
当子组件持有数据、父组件决定渲染方式时,用作用域插槽。
常见反模式
反模式一:直接修改 Prop
// ❌
props.title = 'new title'
// ✅ 通知父组件修改
emit('update:title', 'new title')
或者父组件用 v-model 让子组件直接改。
反模式二:在子组件里修改父组件状态
// ❌ 子组件直接改父组件数据
const parentData = inject('data')
parentData.value = 'changed' // 改了提供方的数据
// ✅ 调用提供方传入的方法
const { data, updateData } = inject('data')
updateData('changed')
反模式三:万能组件
一个组件通过大量 props 控制各种情况,结果变成”配置地狱”:
<!-- ❌ 太多 props -->
<SmartButton
type="primary"
size="large"
:loading="isLoading"
:disabled="isDisabled"
icon="check"
:round="true"
:ghost="false"
tooltip="提交"
/>
这种情况下考虑拆成多个组件,或者用插槽提供灵活性。
反模式四:过度抽取
<!-- ❌ 不值得拆的组件 -->
<ProfileAvatar :src="user.avatar" />
<!-- 这跟直接用 <img> 没区别,反而多一层 -->
组件文件结构
一个 .vue 文件建议按这个顺序组织:
<script setup>
// 1. 导入
// 2. 定义 props/emits
// 3. composables 调用
// 4. 响应式状态
// 5. 计算属性
// 6. 侦听器
// 7. 方法
// 8. 生命周期钩子
</script>
<template>
<!-- 模板 -->
</template>
<style scoped>
<!-- 样式 -->
</style>
总结
| 原则 | 做法 |
|---|---|
| 单一职责 | 一个组件只做一件事 |
| 合理拆分 | 按区域、职责、复用度拆 |
| 命名规范 | PascalCase 注册、camelCase 定义 |
| Props 清晰 | 对象语法、带类型和默认值 |
| 事件明确 | 描述发生了什么、载荷最小化 |
| 插槽灵活 | 暴露灵活性、用作用域插槽传数据 |
| 避免反模式 | 不直接改 prop、不做万能组件 |
本节回顾
- 单一职责:一个组件只做一件事
- 合理拆分不是越细越好
- 命名遵循 PascalCase + camelCase + kebab-case 约定
- Props 和 Events 要有明确语义
- 避免直接修改 prop、过度抽取等反模式
- 写组件前先想清楚”这个组件负责什么”