高复用性组件的核心原则
1. 单一职责原则
组件应该只专注于一个主要功能
vue
<!-- 好的例子:专注显示头像 -->
<template>
<div class="user-avatar">
<img :src="avatarUrl" :alt="userName" @error="handleError">
<span v-if="showName">{{ userName }}</span>
</div>
</template>
<!-- 不好的例子:同时处理太多功能 -->
<template>
<div>
<img :src="avatarUrl">
<span>{{ userName }}</span>
<button @click="editUser">编辑</button>
<button @click="deleteUser">删除</button>
</div>
</template>
2. 适当的抽象层级
组件应该在通用性和专用性之间找到平衡
vue
<!-- 通用性太强 - 难以使用 -->
<template>
<div :class="containerClass" :style="containerStyle">
<slot name="content"></slot>
</div>
</template>
<!-- 专用性太强 - 难以复用 -->
<template>
<div class="user-card-specific">
<img src="/api/users/123/avatar">
<span>张三</span>
<button>关注</button>
</div>
</template>
<!-- 适当的抽象 -->
<template>
<div class="user-card" :class="size">
<img :src="avatar" :alt="name">
<h3>{{ name }}</h3>
<p v-if="description">{{ description }}</p>
<slot name="actions"></slot>
</div>
</template>
3. 清晰的接口设计
Props、Events、Slots 应该设计得直观易用
vue
<template>
<div class="modal" v-show="visible">
<div class="modal-header">
<h2>{{ title }}</h2>
<button @click="$emit('close')">×</button>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button @click="$emit('confirm')">确认</button>
<button @click="$emit('cancel')">取消</button>
</slot>
</div>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: '提示'
},
visible: {
type: Boolean,
default: false
}
},
emits: ['close', 'confirm', 'cancel'] // Vue 3 风格,Vue 2 可在文档中说明
}
</script>
组件封装的层次结构
基础组件 (Base Components)
vue
<!-- BaseButton.vue -->
<template>
<button
:class="['base-button', type, size, { disabled, loading }]"
:disabled="disabled || loading"
@click="handleClick"
>
<span v-if="loading" class="loading-spinner"></span>
<slot></slot>
</button>
</template>
<script>
export default {
name: 'BaseButton',
props: {
type: {
type: String,
default: 'primary',
validator: value => ['primary', 'secondary', 'danger', 'text'].includes(value)
},
size: {
type: String,
default: 'medium',
validator: value => ['small', 'medium', 'large'].includes(value)
},
disabled: Boolean,
loading: Boolean
},
methods: {
handleClick(e) {
if (!this.disabled && !this.loading) {
this.$emit('click', e)
}
}
}
}
</script>
业务组件 (Business Components)
vue
<!-- SubmitButton.vue -->
<template>
<BaseButton
:type="type"
:size="size"
:disabled="disabled"
:loading="isSubmitting"
@click="$emit('click', $event)"
>
<slot>{{ submitText }}</slot>
</BaseButton>
</template>
<script>
import BaseButton from './BaseButton.vue'
export default {
name: 'SubmitButton',
components: { BaseButton },
props: {
type: {
type: String,
default: 'primary'
},
size: {
type: String,
default: 'large'
},
disabled: Boolean,
isSubmitting: Boolean,
submitText: {
type: String,
default: '提交'
}
}
}
</script>
完整的组件封装最佳实践
1. 完整的 Props 设计
javascript
// 组件Props设计示例
export default {
props: {
// 基本类型
title: String,
// 多种类型
width: [String, Number],
// 带默认值
visible: {
type: Boolean,
default: false
},
// 必需属性
data: {
type: Array,
required: true
},
// 验证器
size: {
type: String,
validator: value => ['small', 'medium', 'large'].includes(value),
default: 'medium'
},
// 复杂对象
config: {
type: Object,
default: () => ({}) // 使用工厂函数避免共享引用
},
// 自定义验证
count: {
type: Number,
validator: value => value >= 0 && value <= 100
}
}
}
2. 完整的事件设计
javascript
export default {
methods: {
handleInput(value) {
// 处理数据
const processedValue = this.processValue(value)
// 发出事件
this.$emit('input', processedValue)
this.$emit('change', processedValue)
// 如果需要,可以添加额外逻辑
if (this.validate(processedValue)) {
this.$emit('valid', processedValue)
} else {
this.$emit('invalid', processedValue)
}
}
}
}
3. 插槽设计
vue
<template>
<div class="card">
<!-- 默认插槽 -->
<div class="card-content">
<slot></slot>
</div>
<!-- 命名插槽 -->
<div class="card-header" v-if="$slots.header">
<slot name="header"></slot>
</div>
<!-- 作用域插槽 -->
<div class="card-footer">
<slot name="footer" :data="footerData" :actions="footerActions">
<!-- 默认内容 -->
<button @click="footerActions.default">确定</button>
</slot>
</div>
</div>
</template>
4. 完整的组件示例
vue
<template>
<div class="smart-list" :class="[size, layout]">
<!-- 头部插槽 -->
<div v-if="$slots.header || title" class="list-header">
<slot name="header">
<h3>{{ title }}</h3>
</slot>
</div>
<!-- 内容区域 -->
<div class="list-content">
<!-- 加载状态 -->
<div v-if="loading" class="loading-state">
<slot name="loading">
<div class="default-loading">加载中...</div>
</slot>
</div>
<!-- 空状态 -->
<div v-else-if="!data || data.length === 0" class="empty-state">
<slot name="empty">
<div class="default-empty">暂无数据</div>
</slot>
</div>
<!-- 数据展示 -->
<template v-else>
<div
v-for="(item, index) in data"
:key="getItemKey(item, index)"
class="list-item"
@click="$emit('item-click', item, index)"
>
<slot name="item" :item="item" :index="index">
<div class="default-item">{{ item }}</div>
</slot>
</div>
</template>
</div>
<!-- 底部插槽 -->
<div v-if="$slots.footer" class="list-footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'SmartList',
props: {
data: {
type: Array,
default: () => []
},
title: String,
loading: Boolean,
size: {
type: String,
default: 'medium',
validator: value => ['small', 'medium', 'large'].includes(value)
},
layout: {
type: String,
default: 'vertical',
validator: value => ['vertical', 'horizontal'].includes(value)
},
itemKey: {
type: [String, Function],
default: 'id'
}
},
emits: ['item-click', 'update:data'],
methods: {
getItemKey(item, index) {
if (typeof this.itemKey === 'function') {
return this.itemKey(item, index)
}
return item[this.itemKey] || index
},
// 提供公共方法
refresh() {
this.$emit('update:data', [...this.data])
},
// 添加项
addItem(item) {
const newData = [...this.data, item]
this.$emit('update:data', newData)
return newData
},
// 移除项
removeItem(index) {
const newData = this.data.filter((_, i) => i !== index)
this.$emit('update:data', newData)
return newData
}
},
// 提供实例方法
mounted() {
// 注册全局方法
if (this.$listeners['register-methods']) {
this.$emit('register-methods', {
refresh: this.refresh,
addItem: this.addItem,
removeItem: this.removeItem
})
}
}
}
</script>
<style scoped>
.smart-list {
border: 1px solid #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.list-header {
padding: 16px;
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
}
.list-content {
min-height: 200px;
}
.loading-state, .empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
}
.list-item {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.2s;
}
.list-item:hover {
background-color: #f9f9f9;
}
.list-item:last-child {
border-bottom: none;
}
.list-footer {
padding: 16px;
background: #f5f5f5;
border-top: 1px solid #e0e0e0;
}
/* 尺寸变体 */
.smart-list.small .list-item {
padding: 8px 12px;
}
.smart-list.large .list-item {
padding: 16px 20px;
}
/* 布局变体 */
.smart-list.horizontal {
display: flex;
flex-wrap: wrap;
}
.smart-list.horizontal .list-item {
flex: 1;
min-width: 200px;
border-right: 1px solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
}
.smart-list.horizontal .list-item:nth-child(even) {
border-right: none;
}
</style>
组件使用文档
为每个组件提供清晰的文档:
markdown
# SmartList 智能列表组件
## 功能
- 支持多种数据状态(加载中、空数据、正常数据)
- 支持自定义渲染
- 提供多种布局和尺寸
- 内置项操作方法
## Props
| 参数 | 说明 | 类型 | 默认值 |
|------|------|------|--------|
| data | 列表数据 | Array | [] |
| title | 列表标题 | String | - |
| loading | 加载状态 | Boolean | false |
| size | 尺寸 | String ('small', 'medium', 'large') | 'medium' |
| layout | 布局 | String ('vertical', 'horizontal') | 'vertical' |
## Slots
| 名称 | 说明 | 作用域参数 |
|------|------|------------|
| default | 默认内容 | - |
| header | 头部内容 | - |
| footer | 底部内容 | - |
| item | 列表项内容 | { item, index } |
| loading | 加载状态 | - |
| empty | 空状态 | - |
## Events
| 事件名 | 说明 | 参数 |
|--------|------|------|
| item-click | 点击列表项 | (item, index) |
| update:data | 数据更新 | (newData) |
## 方法
通过 ref 调用:
- refresh() - 刷新列表
- addItem(item) - 添加项
- removeItem(index) - 移除项
总结
封装高复用性组件的关键点:
- 明确职责:每个组件只做一件事
- 合理抽象:平衡通用性和专用性
- 清晰接口:设计直观的 props、events、slots
- 完整文档:提供清晰的使用说明
- 渐进增强:从简单开始,逐步添加功能
- 一致性:遵循项目规范和设计系统
不是所有组件都需要高度复用,根据实际需求决定封装程度。业务专用组件可以更简单,而基础组件应该设计得更通用和健壮。