组件开发中的那些坑我替你踩过了
这次的组件封装让我想重新学Vue
最近在重构一个项目的时候,被组件封装给折腾得够呛。说实话,我之前写组件都是能跑就行,也没太在意复用性和扩展性。结果这次遇到的需求稍微复杂一点,之前的写法完全不够用了。
主要是要做一个表格组件,里面要支持排序、筛选、分页这些功能。本来想着挺简单的,结果写着写着发现各种状态管理混乱,父子组件通信也是乱七八糟的。折腾了两天才意识到,我的组件开发思路从根本上就有问题。
父子通信那套老方法真不行
以前我做组件通信就是 props 和 emit 这一套,简单的父子关系还好,稍微复杂一点就懵了。这次做表格组件,里面要嵌套筛选器、分页器,还要把数据和当前状态传回去,直接就乱套了。
一开始我是这样写的:
// 子组件
export default {
emits: ['update-sort', 'update-filter', 'update-page'],
methods: {
handleSort(column) {
this.$emit('update-sort', column)
},
handleFilter(filterData) {
this.$emit('update-filter', filterData)
}
}
}
// 父组件
<template>
<TableComponent
:data="tableData"
@update-sort="handleSort"
@update-filter="handleFilter"
@update-page="handlePageChange"
/>
</template>
这里我踩了个坑,emit 事件多了之后,维护起来特别麻烦。而且当子组件层级更深的时候,props 传递就像链条一样,一层层往下传,代码看起来就很恶心。
Provide/Inject 是个好东西,但得用对
后来试了下 provide/inject,发现这玩意儿在组件通信上是真的好用。特别是对于表格这种有多个子组件的场景,通过 provide 把数据和方法传递下去,子组件可以直接 inject 使用,不用层层传递了。
但这里也有个小问题,就是响应式的处理。最开始我写的代码是这样的:
// 表格主组件
import { ref, reactive, provide } from 'vue'
export default {
setup() {
const tableState = reactive({
data: [],
loading: false,
currentPage: 1,
pageSize: 10,
sortInfo: {},
filterInfo: {}
})
const updateSort = (column) => {
tableState.sortInfo = { field: column.field, order: column.order }
fetchData()
}
const updateFilter = (filter) => {
tableState.filterInfo = filter
fetchData()
}
// 提供给子组件使用
provide('tableContext', {
state: tableState,
updateSort,
updateFilter,
updatePage: (page) => {
tableState.currentPage = page
fetchData()
}
})
}
}
// 排序组件
import { inject } from 'vue'
export default {
setup() {
const { state, updateSort } = inject('tableContext')
const handleSort = (column) => {
updateSort(column)
}
return {
state,
handleSort
}
}
}
刚开始用起来感觉还不错,但后来发现一个问题:如果表格组件被销毁重建,子组件那边的状态可能会出错。折腾了半天发现是因为 provide 的对象引用问题,需要做一些额外的处理来确保每次都是新的实例。
插槽设计让我头疼不已
表格组件最难搞的部分其实是插槽设计。用户可能需要自定义列的内容、头部、底部,还有可能出现复杂的交互逻辑。
最开始我想着把所有地方都做成插槽,结果代码变得特别复杂:
<template>
<div class="table-container">
<div class="table-header">
<slot name="header" :data="state.data">
<!-- 默认头部 -->
</slot>
</div>
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key">
<slot :name="${col.key}-header" :column="col">
{{ col.title }}
</slot>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in state.data" :key="index">
<td v-for="col in columns" :key="col.key">
<slot :name="col.key" :row="row" :value="row[col.key]" :index="index">
{{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
<div class="table-footer">
<slot name="footer" :pagination="state.pagination">
<Pagination
:current="state.currentPage"
:total="state.total"
@change="updatePage"
/>
</slot>
</div>
</div>
</template>
这么写虽然灵活性很高,但使用起来就很麻烦。后来我重新考虑了一下,决定把插槽分为两类:功能性插槽和展示性插槽。功能性插槽提供默认实现,让用户可以覆盖;展示性插槽主要用来定制外观。
事件系统重构花了我一整天
原来的事件系统真的是一团糟,后来我重新设计了一套。主要是把所有的事件都统一管理,然后通过事件名称来区分不同的操作类型。
// 事件管理器
class TableEventManager {
constructor() {
this.listeners = new Map()
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, [])
}
this.listeners.get(event).push(callback)
}
emit(event, payload) {
const callbacks = this.listeners.get(event)
if (callbacks) {
callbacks.forEach(callback => callback(payload))
}
}
off(event, callback) {
if (this.listeners.has(event)) {
const callbacks = this.listeners.get(event)
const index = callbacks.indexOf(callback)
if (index > -1) {
callbacks.splice(index, 1)
}
}
}
}
// 在组件中使用
setup() {
const eventManager = new TableEventManager()
// 注册全局事件监听
eventManager.on('sort-change', handleSort)
eventManager.on('filter-change', handleFilter)
eventManager.on('page-change', handlePage)
provide('eventManager', eventManager)
}
这样做的好处是事件管理更加清晰,而且可以在任何子组件中触发和监听事件,不需要层层传递。缺点是要管理好事件的生命周期,避免内存泄漏。
性能优化不能忽略
组件写完了还得考虑性能。表格数据量大的时候,渲染会很卡。这里我主要做了几个优化:
- 使用虚拟滚动来处理大数据量
- 对计算属性进行缓存
- 避免不必要的响应式更新
// 虚拟滚动实现
const visibleRows = computed(() => {
const start = Math.max(0, state.scrollOffset - bufferSize)
const end = Math.min(
state.filteredData.length,
state.scrollOffset + visibleCount + bufferSize
)
return state.filteredData.slice(start, end).map((item, index) => ({
...item,
_virtualIndex: start + index
}))
})
虚拟滚动这块我还专门研究了一下,主要是要计算可视区域的高度,然后只渲染当前可见的数据行。这部分代码稍微有点复杂,但是效果还是很明显的。
最后的总结
这次组件重构让我对 Vue 的组件开发有了更深的理解。以前写组件就是功能能跑通就行,现在才知道组件设计其实是个大学问。
最重要的是要提前规划好组件的架构,特别是通信机制和状态管理。用好 provide/inject 可以大大简化组件间的数据传递,但要注意响应式的问题。
插槽设计要平衡灵活性和易用性,不能为了追求灵活性把所有地方都做成插槽。事件系统的统一管理也很重要,可以让代码更清晰。
以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。

暂无评论