一、为什么要抽象组件?
中后台系统开发过程中,有两个关键词始终绕不过去:表格 和 表单。
广告平台项目上线初期,我们发现大量页面的结构都是“筛选 + 表格 + 操作弹窗”,大多数逻辑重复率极高,于是我主导抽象了两个核心组件:
<DataTable />:用于列表展示、分页、筛选等<FormModal />:用于弹窗表单,支持动态字段、回填、提交反馈
这两个组件帮团队节省了大量重复开发时间。但在抽象过程中,也踩了不少坑,本文详讲实战经验。
二、DataTable 的抽象核心逻辑
✅ 目标:支持配置化生成业务列表页面
使用方式:
<DataTable
:columns="columns"
:search-schema="searchSchema"
:request="fetchList"
row-key="id"
/>
组件内部设计如下:
<template>
<n-form :model="searchParams">
<n-grid :cols="3">
<n-form-item v-for="item in searchSchema" :label="item.label" :key="item.field">
<component :is="item.component" v-model:value="searchParams[item.field]" v-bind="item.props" />
</n-form-item>
</n-grid>
<n-button @click="loadData">查询</n-button>
</n-form>
<n-data-table :columns="columns" :data="data" :loading="loading" :row-key="rowKey" />
<n-pagination :page="page" :page-size="pageSize" @update:page="onPageChange" />
</template>
逻辑部分封装在 setup:
const searchParams = reactive({})
const data = ref([])
const page = ref(1)
const pageSize = ref(10)
const loading = ref(false)
const loadData = async () => {
loading.value = true
const res = await props.request({
...toRaw(searchParams),
page: page.value,
pageSize: pageSize.value
})
data.value = res.list
loading.value = false
}
三、抽象的陷阱:越通用越不通用?
在实际应用中我们发现:
- 表头有复杂渲染需求(比如单元格中包含图标、tooltip、按钮)
- 某些页面需要批量操作 checkbox,但有些不需要
- 筛选区域有时要嵌套 tabs、折叠面板
于是抽象组件变成了“定制化魔改”:
<DataTable :custom-filter-slot="true">
<template #filter>
<!-- 重新写一套复杂筛选 UI -->
</template>
</DataTable>
🚨教训:不应追求“万能组件”,而是80% 通用 + 20% 灵活扩展。
解决方案:引入 slot + 插件式 hooks 扩展机制。
四、FormModal:弹窗表单的典范设计
目标是让使用者这样写:
<FormModal
:model="formModel"
:schema="[
{ label: '广告名称', field: 'name', component: 'n-input', rules: [{ required: true }] },
{ label: '投放时间', field: 'time', component: 'n-date-picker', props: { type: 'daterange' } }
]"
:on-submit="handleSubmit"
/>
内部实现核心代码:
<n-modal v-model:show="visible">
<n-form :model="model" :rules="rules" ref="formRef">
<n-form-item v-for="item in schema" :label="item.label" :path="item.field">
<component :is="item.component" v-model:value="model[item.field]" v-bind="item.props" />
</n-form-item>
</n-form>
<n-space>
<n-button @click="onCancel">取消</n-button>
<n-button type="primary" @click="onConfirm">提交</n-button>
</n-space>
</n-modal>
表单校验逻辑:
const onConfirm = async () => {
const form = formRef.value
if (!form) return
try {
await form.validate()
emit('submit', model)
visible.value = false
} catch (e) {
// 校验失败
}
}
五、组件抽象的边界策略
- 抽象重复逻辑,保留可插拔能力:比如加载数据、分页、表单校验这类逻辑可以固定,但 UI 应该开放插槽
- 保持 schema 简洁但可扩展:
// 支持动态渲染组件
{
field: 'gender',
label: '性别',
component: 'n-select',
props: {
options: [ { label: '男', value: 1 }, { label: '女', value: 2 } ]
}
}
- 状态外置,逻辑内聚:所有组件状态由外部传入(如 model、visible),组件内部只负责行为逻辑
- 插槽优先,配置次之,逻辑最后兜底:slot > schema > props
六、总结:组件抽象的本质是「降低重复成本 + 提高上下文适应力」
真正有价值的组件抽象,不是写个超大配置表,而是:
- 重复性逻辑统一封装,减少冗余
- 样式和结构保持一致性,提高用户体验
- 对不确定性内容保持开放,利于团队协作与业务扩展