基于 el-form 的分层封装实践:从页面手写到可复用基础设施
本文总结我们在 Element Plus 体系下的表单封装实践:不替代 el-form,而是在其之上构建布局层、字段层与场景层,解决中后台高频表单开发中的重复劳动与一致性问题。
一、问题背景
在 B 端场景中,查询表单和编辑表单往往有以下共性问题:
- 多列布局规则复杂(固定列、响应式、跨列)
- 标签宽度与对齐策略难统一
- 操作区(查询/重置/展开)位置容易漂移
- 同类交互在不同页面重复实现,维护成本高
目标是把“页面级重复实现”沉淀为“组件级稳定能力”。
二、先看使用方式(伪代码)
2.1 布局层:el-form -> el-form-layout -> el-form-layout-item
<el-form :model="formModel" :label-width="100">
<el-form-layout :grid="{ breakpoints: [{ name: 'all', min: 0, cols: 3 }], colGap: 16, rowGap: 8 }">
<el-form-layout-item label="姓名" prop="name">
<el-form-renderer
:schema="{ field: 'name', controlType: 'input' }"
v-model="formModel.name"
/>
</el-form-layout-item>
<el-form-layout-item label="部门" prop="dept" :span="2">
<el-form-renderer
:schema="{ field: 'dept', controlType: 'input' }"
v-model="formModel.dept"
/>
</el-form-layout-item>
</el-form-layout>
</el-form>
2.2 查询层:el-form-query(自管 el-form + dense 折叠)
<el-form-query
v-model="query"
:schema="querySchema"
:grid="'element-plus'"
:default-rows-number="1"
:search-handler="onSearch"
:reset-handler="onReset"
:toggle-handler="onToggle"
:scroll-to-error="true"
/>
说明:
searchHandler/resetHandler/toggleHandler存在时可接管默认 emit 流程- 折叠态按钮落位由 query 专用 dense 算法保证
- 操作区可通过 portal 扩展(同一 target + order 混排)
三、Schema 设计:三种类型(field / group / custom)
我们在 schema 层明确了 3 类节点,避免“用 children 是否存在来猜语义”:
type EllFormSchema =
| EllFieldSchema // 普通字段
| EllGroupSchema // 分组块
| EllCustomSchema // 自定义块
3.1 field:标准字段节点
{
type: 'field',
field: 'name',
label: '姓名',
controlType: 'input',
span: 1,
}
3.2 group:结构化分组节点
{
type: 'group',
field: 'addressGroup',
label: '地址信息',
children: [
{ type: 'field', field: 'province', label: '省份', controlType: 'select' },
{ type: 'field', field: 'city', label: '城市', controlType: 'select' },
],
}
3.3 custom:业务自定义节点
{
type: 'custom',
field: 'riskPanel',
render: ({ uid, schema }) => h(RiskPanel, { uid, schema }),
}
3.4 渲染策略(伪代码)
for (const block of parseSchema(schema)) {
if (block.type === 'fields') renderFields(block.items)
else if (block.type === 'group') renderGroup(block.item)
else renderCustom(block.item)
}
这套设计的核心收益是:字段、分组、自定义块都能在同一渲染管线下共存,扩展复杂页面时不会破坏基础模型。
四、三态页面模式:edit / disabled / preview
ElFormRenderer 以 mode 控制渲染行为,保证“同一份 schema”覆盖编辑、禁用和预览页面。
type EllFormRenderMode = 'edit' | 'disabled' | 'preview'
典型用法:
<el-form-builder
v-model="model"
:schema="schema"
:mode="route.meta.readonly ? 'preview' : 'edit'"
/>
行为要点(实现约定):
edit:标准可编辑控件disabled:控件渲染不变,但注入disabled: truepreview:优先previewComponent,其次formatPreview,最后文本回退
这让“详情页”不再是一套独立模板,而是同源渲染链路的不同模式。
五、控件注册与自定义扩展(registerControl)
我们在渲染层提供控件注册机制,业务可以按需扩展私有控件,而不改动核心渲染器。
registerControl('customer-picker', {
component: CustomerPicker,
modelProp: 'modelValue',
modelEvent: 'update:modelValue',
defaultProps: {
clearable: true,
fetchOnMount: true,
},
previewComponent: CustomerPreview,
formatPreview: (value) => value ? `客户ID: ${String(value)}` : '-',
})
schema 侧直接使用:
{
type: 'field',
field: 'customerId',
label: '客户',
controlType: 'customer-picker',
controlConfig: {
tenantId: currentTenantId,
},
}
收益:
- 业务控件接入成本低
- 预览态可定制显示逻辑
- 不破坏统一 schema 协议
六、useForm:组合式表单 API(组件与命令式能力桥接)
useForm 的目标是:保留组件渲染优势,同时提供命令式 API 供复杂交互编排。
const {
Form, // 可直接渲染的表单组件
uid, // 用于 portal 协同
model, // 响应式表单数据
formRef, // el-form 实例
validate,
validateField,
resetFields,
getFieldValue,
setFieldValue,
getControlRef,
getFormItemRef,
submit,
save,
} = useForm({
schema,
initialValues: { name: '张三' },
actions: [{ key: 'submit', to: `${uid}-actions` }],
})
价值:
- 组件渲染与命令式控制同源
- 对话框/抽屉/页面场景可复用同一套 API
- 与
references、portal、handler机制天然协同
七、el-cru-page:页面级超级组件(CRU 标准化)
el-cru-page 是业务复合层,把“页头 + 表单 + 操作区 + handler 链路”标准化为一个页面组件。
<el-cru-page
:header-config="{ title: '供应商入库申请', showBack: true }"
:form-config="{
schema,
grid: { breakpoints: [{ name: 'all', min: 0, cols: 3 }] },
}"
:save-handler="handleSave"
:submit-handler="handleSubmit"
/>
核心收益:
- CRU 页面搭建速度显著提升
- 页头动作区与表单动作区通过 portal 同步编排
- 团队层面形成“页面骨架标准件”
八、useFormDialog / useFormDrawer:弹层场景标准化封装
在业务里,很多新增/编辑并非独立页面,而是弹窗或抽屉。
我们在 useDialog / useDrawer 基础上封装了表单能力,统一接入 el-form-builder:
8.1 useFormDialog
const { open, close } = useFormDialog({
title: '新建客户',
formConfig: {
schema,
grid: { breakpoints: [{ name: 'all', min: 0, cols: 2 }] },
initialValues: { status: 1 },
},
submitHandler: async ({ model, formRef }) => {
await formRef?.validate?.()
await api.createCustomer(model)
close()
},
})
8.2 useFormDrawer
const { open, close } = useFormDrawer({
title: '编辑供应商',
size: 640,
formConfig: {
schema,
fixedColCount: 2,
},
submitHandler: async ({ model }) => {
await api.updateSupplier(model)
close()
},
})
8.3 价值
- 同一套 schema 与渲染引擎覆盖页面、弹窗、抽屉
- 统一校验/提交/重置语义,降低场景切换成本
- 弹层交互保持与页面一致的扩展能力(portal、references、handler)
九、设计原则
1)以 el-form 为核心
保留 el-form 原生能力(上下文、校验、滚动到错误项等),避免新体系与 EP 行为割裂。
2)能力分层
- 布局层:
el-form-layout - 字段层:
el-form-layout-item(包装el-form-item) - 场景层:
el-form-query(查询交互编排)
标准组合关系:
el-form -> el-form-layout -> el-form-layout-item
3)配置优先、实现解耦
布局规则、断点、间距、操作区策略尽量配置化,避免业务页面内散落样式逻辑。
十、动态表单:references + event/dependencies 双路线
4.1 references(命令式)
通过 getControlRef / getFormItemRef,外部可以精准拿到实例并执行命令:
const builderRef = ref()
// 控件实例(由 ElFormRenderer 注册)
const phoneControl = builderRef.value?.getControlRef('phone')
phoneControl?.focus?.()
// 表单项实例(用于清理校验、滚动定位等)
const phoneItem = builderRef.value?.getFormItemRef('phone')
phoneItem?.clearValidate?.()
适用:需要“精确操控某个字段实例”的交互。
4.2 events + dependencies(声明式)
const schema = [
{
field: 'province',
label: '省份',
controlType: 'select',
events: {
change: (_value, ctx) => {
ctx.model.city = undefined
},
},
},
{
field: 'city',
label: '城市',
controlType: 'select',
dependencies: {
triggerFields: ['province'],
show: values => !!values.province,
disabled: values => !values.province,
rules: values => values.province ? [{ required: true, message: '请选择城市' }] : [],
},
options: async ({ model }) => fetchCityList(model.province),
},
]
适用:规则复杂、需要可读性与可维护性的动态联动场景。
十一、三大差异化能力(实现层)
5.1 标签处理体系(Label System)
相比“仅支持 label-width”的常规方案,我们提供可组合策略:
labelWidth/autoLabelWidthlabelPosition(含justify)labelOverflowType(wrap/nowrap/ellipsis)- 字段级
labelConfig覆盖全局配置
const schema = [
{
field: 'customerName',
label: '客户名称',
labelConfig: {
labelPosition: 'justify',
labelOverflowType: 'ellipsis',
autoLabelWidth: false,
},
},
]
收益:标签表现具备可预期性和跨页面一致性。
5.2 portal-vue 编排(Action & Extension)
我们不是“只给一个插槽”,而是做了可编排机制:
- 统一
uid命名体系 - 单 target +
order混排 - 内置动作与业务自定义动作共存
<portal-target :name="`${uid}-actions`" />
<el-form-query :uid="uid">
<template #default="{ actionsPortalName }">
<portal :to="actionsPortalName" :order="6">
<el-button link>导出</el-button>
</portal>
</template>
</el-form-query>
收益:页面头部、表单操作区、分组扩展区可以统一调度。
5.3 dense 折叠算法(Query 专用)
el-form-query 不直接组合 el-form-layout,核心原因是查询场景需要 dense 算法来保证折叠态布局稳定:
const collapsedLayout = simulateDenseFillCollapsed(fieldSpans, colCount, maxRows)
// action 区固定末行末列
const actionCell = collapsedLayout.actionCell
const expandedLayout = simulateDenseFillExpanded(fieldSpans, colCount)
// 展开态按首空列到行尾合并剩余列,action 右对齐
收益:避免“字段折叠后按钮跑位”与“响应式切换布局跳动”。
十二、关键实现思路
12.1 el-form-layout:只做布局
职责边界:
- 只负责 Grid 容器布局(列数、gap、响应式)
- 不承担
el-form的创建职责 - 提供对外可预测的布局行为
收益:布局问题集中治理,不污染表单语义层。
12.2 el-form-layout-item:字段单元标准化
职责边界:
- 封装
el-form-item的常规样板 - 提供
span等网格定位能力 - 保证字段渲染结构统一
收益:字段定义更可读,页面层写法更稳定。
12.3 el-form-query:查询场景专用能力
重点能力:
- 查询/重置/折叠的统一交互流
- 事件模式与 handler 接管模式并存(便于命令式场景)
- 与 portal 插槽机制协作,操作区可扩展
收益:查询区成为标准化构件,不再“每页单独造轮子”。
十三、工程收益
1)一致性
布局、交互、校验反馈都由组件层统一约束,跨页面表现更一致。
2)效率
新页面搭建速度提升,重复代码显著减少。
3)可维护性
需求变更时优先修改组件层,减少全局页面联动改造风险。
4)可演进性
后续可持续扩展 schema、插槽、全局配置与预设断点体系。
十四、适用建议
该方案尤其适合:
- 有大量列表查询页、维护页的中后台系统
- 多人协作、需要统一交互规范的团队
- 追求“可复用组件资产”而非“页面临时可用”的项目
十五、结论
这次实践的核心不在“封了几个组件”,而在于把表单开发从“页面实现问题”升级为“基础设施建设问题”。
当 el-form 能力被分层治理并持续沉淀后,后续业务开发会越来越轻,系统行为也会越来越稳。
附:能力关键词(便于对外传播)
- 分层模型:
el-form -> el-form-layout -> el-form-layout-item - Query 专用布局:dense 折叠 + 操作区稳定落位
- 动态表单双模式:references(命令式)+ event/dependencies(声明式)
- 统一配置入口:grid 预设、断点、间距、标签策略
- 统一扩展机制:portal、handler 接管、schema 驱动