基于 `el-form` 的分层封装实践:从页面手写到可复用基础设施

8 阅读7分钟

基于 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

ElFormRenderermode 控制渲染行为,保证“同一份 schema”覆盖编辑、禁用和预览页面。

type EllFormRenderMode = 'edit' | 'disabled' | 'preview'

典型用法:

<el-form-builder
  v-model="model"
  :schema="schema"
  :mode="route.meta.readonly ? 'preview' : 'edit'"
/>

行为要点(实现约定):

  • edit:标准可编辑控件
  • disabled:控件渲染不变,但注入 disabled: true
  • preview:优先 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
  • referencesportalhandler 机制天然协同

七、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 / autoLabelWidth
  • labelPosition(含 justify
  • labelOverflowTypewrap / 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 驱动

相关链接