首页 / Vue3 入门教程 / 组件最佳实践

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.vueAppHeader.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、过度抽取等反模式
  • 写组件前先想清楚”这个组件负责什么”