一、二次封装的核心方法与技术
1.1 属性透传(Prop Passing)
vue
<!-- BaseButton.vue -->
<template>
<button
:class="['custom-btn', sizeClass]"
v-bind="$attrs"
>
<slot />
</button>
</template>
<script>
export default {
inheritAttrs: false, // 禁止自动绑定到根元素
props: {
size: {
type: String,
default: 'medium',
validator: val => ['small', 'medium', 'large'].includes(val)
}
},
computed: {
sizeClass() {
return `btn-${this.size}`;
}
}
};
</script>
使用示例:
vue
<BaseButton
size="large"
class="primary-btn"
@click="handleClick"
data-test="submit-button"
>
提交
</BaseButton>
1.2 事件转发(Event Forwarding)
vue
<!-- EnhancedInput.vue -->
<template>
<el-input
v-bind="$attrs"
v-on="inputListeners"
/>
</template>
<script>
export default {
computed: {
inputListeners() {
return {
...this.$listeners,
input: event => {
this.$emit('input', event.target.value);
this.$emit('custom-change', event.target.value);
}
};
}
}
};
</script>
1.3 插槽透传(Slot Forwarding)
vue
<!-- WrapperCard.vue -->
<template>
<div class="card-wrapper">
<el-card v-bind="$attrs">
<!-- 透传所有插槽 -->
<template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps" />
</template>
</el-card>
</div>
</template>
1.4 组件继承(Component Inheritance)
javascript
// SmartTable.js
import { ElTable } from 'element-plus';
export default {
name: 'SmartTable',
extends: ElTable,
props: {
// 扩展新属性
autoHeight: {
type: Boolean,
default: true
}
},
mounted() {
if (this.autoHeight) {
this.fitToParent();
}
},
methods: {
fitToParent() {
// 实现自适应高度逻辑
}
}
};
1.5 组合式封装(Composition API)
vue
<!-- SearchTable.vue -->
<template>
<div>
<el-input
v-model="searchValue"
placeholder="搜索..."
/>
<el-table
:data="filteredData"
v-bind="$attrs"
>
<slot />
</el-table>
</div>
</template>
<script>
import { computed, ref } from 'vue';
export default {
props: ['data'],
setup(props) {
const searchValue = ref('');
const filteredData = computed(() => {
return props.data.filter(item =>
Object.values(item).some(val =>
String(val).toLowerCase().includes(searchValue.value.toLowerCase())
);
});
return { searchValue, filteredData };
}
};
</script>
二、二次封装的常见问题及解决方案
2.1 属性传递问题
问题场景:
vue
<EnhancedInput disabled placeholder="请输入" />
在基础组件中,disabled
属性未正确传递
解决方案:
vue
<template>
<el-input
v-bind="filteredAttrs"
v-on="$listeners"
/>
</template>
<script>
export default {
computed: {
filteredAttrs() {
const { class: _, style: __, ...rest } = this.$attrs;
return rest;
}
}
};
</script>
2.2 事件冲突问题
问题场景:
vue
<EnhancedInput @change="handleChange" />
基础组件和封装组件都有 change
事件,导致冲突
解决方案:
javascript
export default {
methods: {
handleNativeChange(event) {
this.$emit('input-change', event.target.value);
this.$emit('native-change', event);
}
}
}
2.3 插槽作用域问题
问题场景:
vue
<SmartTable :data="users">
<template #default="scope">
{{ scope.row.name }} <!-- 无法访问基础组件的 row 属性 -->
</template>
</SmartTable>
解决方案:
vue
<template>
<el-table v-bind="$attrs">
<template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps" />
</template>
</el-table>
</template>
2.4 样式污染问题
问题场景:
css
/* 封装组件样式 */
.card-wrapper .el-card__body {
padding: 0; /* 影响所有使用该组件的卡片 */
}
解决方案:
vue
<template>
<div class="custom-card">
<el-card :class="[$attrs.class]">
<slot />
</el-card>
</div>
</template>
<style scoped>
/* 使用深度选择器限定样式 */
.custom-card::v-deep .el-card__body {
padding: 10px;
}
</style>
2.5 组件引用问题
问题场景:
vue
<template>
<EnhancedForm ref="formRef">
<!-- 表单内容 -->
</EnhancedForm>
</template>
<script>
export default {
methods: {
submit() {
this.$refs.formRef.validate(); // 无法访问基础表单的 validate 方法
}
}
}
</script>
解决方案:
javascript
export default {
methods: {
validate() {
return this.$refs.baseForm.validate();
}
}
}
三、高级封装模式
3.1 高阶组件模式(HOC)
javascript
// withLoading.js
export default function withLoading(WrappedComponent) {
return {
name: `WithLoading${WrappedComponent.name}`,
props: WrappedComponent.props,
data() {
return {
isLoading: false
};
},
methods: {
async loadData() {
this.isLoading = true;
try {
await this.$refs.wrapped.loadData();
} finally {
this.isLoading = false;
}
}
},
render(h) {
return h('div', { class: 'with-loading' }, [
h(WrappedComponent, {
ref: 'wrapped',
props: this.$props,
attrs: this.$attrs,
on: this.$listeners
}),
this.isLoading && h(LoadingSpinner)
]);
}
};
}
3.2 渲染代理模式
vue
<!-- ProxyTable.vue -->
<script>
export default {
render() {
return this.$scopedSlots.default({
table: this.$refs.table,
columns: this.columns,
data: this.processedData
});
}
}
</script>
使用示例:
vue
<ProxyTable :data="rawData">
<template #default="{ table, columns, data }">
<el-table ref="table" :data="data">
<el-table-column
v-for="col in columns"
:key="col.prop"
:prop="col.prop"
:label="col.label"
/>
</el-table>
</template>
</ProxyTable>
3.3 复合组件模式
vue
<!-- DataGrid.vue -->
<template>
<div class="data-grid">
<DataGridHeader @search="handleSearch" />
<DataGridBody :data="filteredData">
<slot />
</DataGridBody>
<DataGridFooter @page-change="handlePageChange" />
</div>
</template>
<script>
import DataGridHeader from './DataGridHeader.vue';
import DataGridBody from './DataGridBody.vue';
import DataGridFooter from './DataGridFooter.vue';
export default {
components: { DataGridHeader, DataGridBody, DataGridFooter },
props: ['data'],
data() {
return {
searchQuery: '',
currentPage: 1
};
},
computed: {
filteredData() {
// 过滤和分页逻辑
}
}
};
</script>
四、二次封装最佳实践
4.1 设计原则
- 单一职责原则:每个封装组件只解决一个特定问题
- 开闭原则:对扩展开放,对修改封闭
- 最少知识原则:封装组件不应暴露内部实现细节
- 一致性原则:保持与基础组件的API一致性
4.2 性能优化技巧
javascript
export default {
watch: {
data: {
handler(newVal) {
// 使用防抖处理大数据量
this.debouncedFilter(newVal);
},
deep: true,
immediate: true
}
},
created() {
this.debouncedFilter = _.debounce(this.doFilter, 300);
},
methods: {
doFilter(data) {
// 实际过滤逻辑
}
}
}
4.3 可测试性设计
javascript
// SmartForm.test.js
describe('SmartForm', () => {
it('应该正确透传属性', async () => {
const wrapper = mount(SmartForm, {
propsData: { disabled: true },
slots: { default: '<input type="text">' }
});
const input = wrapper.find('input');
expect(input.attributes('disabled')).toBe('disabled');
});
it('应该触发自定义验证', async () => {
const wrapper = mount(SmartForm);
await wrapper.vm.validate();
expect(wrapper.emitted('custom-validate')).toBeTruthy();
});
});
4.4 文档化示例
markdown
## SmartTable 组件
### 基本用法
```vue
<SmartTable :data="tableData">
<el-table-column prop="name" label="姓名" />
<el-table-column prop="age" label="年龄" />
</SmartTable>
高级功能
自动分页:
vue
<SmartTable :data="largeData" pagination :page-size="20" />
自定义空状态:
vue
<SmartTable :data="[]">
<template #empty>
<div class="custom-empty">暂无数据</div>
</template>
</SmartTable>
text
## 五、复杂场景案例分析:企业级表格封装
### 5.1 需求分析
- 支持大数据量虚拟滚动
- 集成列配置管理
- 内置复杂筛选功能
- 支持多级表头
- 可定制的空状态
### 5.2 组件结构
```mermaid
classDiagram
class EnterpriseTable {
+columns: ColumnConfig[]
+data: any[]
+loading: boolean
+pagination: PaginationConfig
+showHeader: boolean
+rowKey: string
+getRowClass(): string
+refresh(): void
}
class ColumnConfig {
+prop: string
+label: string
+width: number
+sortable: boolean
+filterable: boolean
+formatter: Function
}
EnterpriseTable "1" *-- "*" ColumnConfig
5.3 完整实现
vue
<template>
<div class="enterprise-table">
<!-- 表格工具栏 -->
<TableToolbar
:columns="columns"
@column-change="handleColumnChange"
@filter="handleFilter"
/>
<!-- 虚拟滚动容器 -->
<VirtualContainer
:height="tableHeight"
:item-size="rowHeight"
:item-count="processedData.length"
>
<template #default="{ index, style }">
<TableRow
:row="processedData[index]"
:columns="visibleColumns"
:style="style"
@row-click="handleRowClick"
/>
</template>
</VirtualContainer>
<!-- 分页器 -->
<div v-if="pagination" class="table-footer">
<TablePagination
:total="total"
:current="currentPage"
@page-change="handlePageChange"
/>
</div>
<!-- 空状态 -->
<TableEmpty v-if="showEmpty" :config="emptyConfig" />
</div>
</template>
<script>
import { ref, computed } from 'vue';
import TableToolbar from './TableToolbar.vue';
import TableRow from './TableRow.vue';
import TablePagination from './TablePagination.vue';
import TableEmpty from './TableEmpty.vue';
import VirtualContainer from './VirtualContainer.vue';
export default {
components: { TableToolbar, TableRow, TablePagination, TableEmpty, VirtualContainer },
props: {
data: Array,
columns: Array,
rowKey: String,
pagination: [Boolean, Object],
height: Number,
emptyText: String
},
setup(props, { emit }) {
// 响应式状态管理
const currentPage = ref(1);
const pageSize = ref(20);
const visibleColumns = ref([...props.columns]);
const filters = ref({});
const sortState = ref({ prop: null, order: null });
// 计算属性
const processedData = computed(() => {
let result = [...props.data];
// 筛选处理
if (Object.keys(filters.value).length > 0) {
result = result.filter(row =>
Object.entries(filters.value).every(([key, value]) =>
String(row[key]).includes(value)
);
}
// 排序处理
if (sortState.value.prop) {
const { prop, order } = sortState.value;
result.sort((a, b) => {
const aVal = a[prop];
const bVal = b[prop];
return order === 'ascending'
? aVal.localeCompare(bVal)
: bVal.localeCompare(aVal);
});
}
// 分页处理
if (props.pagination) {
const start = (currentPage.value - 1) * pageSize.value;
return result.slice(start, start + pageSize.value);
}
return result;
});
// 事件处理
const handleColumnChange = (newColumns) => {
visibleColumns.value = newColumns;
emit('columns-change', newColumns);
};
const handleFilter = (newFilters) => {
filters.value = newFilters;
currentPage.value = 1;
};
return {
currentPage,
pageSize,
visibleColumns,
processedData,
handleColumnChange,
handleFilter
};
}
};
</script>
5.4 关键问题解决
- 性能优化:使用虚拟滚动处理大数据量
- 状态管理:集中管理筛选、排序、分页状态
- API 设计:提供统一的配置接口
- 可扩展性:通过插槽支持自定义行、单元格、空状态
- 响应式设计:自动适应不同屏幕尺寸
六、总结与最佳实践
6.1 封装决策树
6.2 核心原则
- 保持透明:尽量保持基础组件的API不变
- 明确边界:封装组件应明确责任范围
- 版本兼容:处理基础组件版本升级问题
- 文档驱动:为封装组件提供完善文档
- 测试覆盖:确保封装组件的质量
6.3 典型应用场景
场景 | 封装方式 | 示例 |
---|---|---|
UI规范统一 | 样式封装 | 企业主题按钮 |
功能增强 | 组合封装 | 带搜索的表格 |
复杂交互 | 高阶组件 | 可编辑表格行 |
业务模块 | 复合组件 | 用户管理卡片 |
性能优化 | 渲染代理 | 虚拟滚动列表 |
通过合理的二次封装,可以:
- 提高代码复用率(减少30%-50%重复代码)
- 统一UI/UX规范(保证产品一致性)
- 简化复杂组件的使用(降低使用门槛)
- 优化性能(集中处理通用优化逻辑)
- 增强可维护性(核心逻辑集中管理)
但同时要注意避免:
- 过度封装导致灵活性下降
- 多层封装造成性能损耗
- 抽象泄漏暴露实现细节
- 版本耦合增加维护成本
掌握好封装粒度,平衡灵活性与便利性,是UI组件二次封装成功的关键。