前言
在里程碑 3 中,我们通过"DSL 配置 + 模板页 + 领域模型"实现了快速建站:一份 schema 配置就能驱动 schema-table 渲染出列表页。但一个中后台系统远不止列表------新增、编辑、详情、搜索,这些表单类功能同样是高频的重复劳动。
以商品管理为例,除了商品列表,还需要"新增商品"表单、"编辑商品"表单、"商品详情"面板。如果这些也逐个手写,建站框架的价值就打了折扣。这个里程碑的内容------动态组件扩展设计,就是解决这个问题:把表单、详情等组件也纳入 DSL 配置体系,通过配置动态注册和渲染,让生成的系统更具自由度和灵活度。
设计思路
先回顾 SchemaView 的结构:它维护一份 JSONSchema 配置,存放字段在不同组件中的配置信息(schema.properties),同时在 schemaConfig 中存放对应的组件配置。里程碑 3 只实现了其中 search 和 table 两种形态,这个里程碑把能力扩展到"任意组件"。
动态组件设计的关键在于两个配置入口 + 一个解析器:
componentConfig(组件级配置) :在schemaConfig中声明系统用到的动态组件,如createForm(新增表单)、editForm(编辑表单)、detailPanel(详情面板),并配置它们的标题、按钮文案、主键等信息;- 字段级
xxxOption:在schema.properties的每个字段上,按组件名挂载对应的配置(如createFormOption、editFormOption、detailPanelOption),用comType声明该字段在该组件中使用的控件类型; - 组件解析器 :读取
componentConfig动态加载组件,通过showComponent()按组件名渲染;组件内部再通过控件注册表把comType映射到具体的子组件。
整体流程如下图:

DSL 配置
componentConfig:组件级配置
在 schemaConfig 中新增 componentConfig,声明系统需要的动态组件。以商品管理为例,需要新增表单、编辑表单、详情面板三个组件:
js
schemaConfig: {
api: '/api/proj/product',
schema: { /* 字段结构,见下文 */ },
tableConfig: { /* 列表配置(里程碑3已实现) */ },
componentConfig: {
createForm: {
title: '新增商品', // 表单标题
saveBtnText: '新增商品', // 保存按钮文案
},
editForm: {
mainKey: 'product_id', // 表单主键,唯一标识要修改的数据
title: '修改商品',
saveBtnText: '修改',
},
detailPanel: {
mainKey: 'product_id',
title: '商品详情',
},
},
},
组件级配置只描述"这个组件长什么样、操作什么数据",不关心具体字段------字段的呈现由字段级配置决定。
字段级 xxxOption:每个字段独立声明
在 schema.properties 的每个字段上,挂载它参与的各组件的配置。字段在哪个组件里显示、用什么控件、是否禁用、默认值是什么,都由对应的 xxxOption 决定:
js
schema: {
type: 'object',
properties: {
product_id: {
type: 'string',
label: '商品ID',
tableOption: { width: 200, 'show-overflow-tooltip': true },
editFormOption: {
comType: 'input',
disabled: true, // 主键在编辑时不可修改
},
detailPanelOption: {},
},
product_name: {
type: 'string',
label: '商品名称',
minLength: 2,
maxLength: 20,
tableOption: { width: 240 },
searchOption: {
comType: 'dynamicSelect', // 搜索框用动态下拉
api: '/api/proj/product_enum/list', // 通过接口加载枚举
},
createFormOption: {
comType: 'input',
default: '默认商品名称',
},
editFormOption: {
comType: 'input',
},
detailPanelOption: {},
},
price: {
type: 'number',
label: '价格',
minimum: 0,
tableOption: { width: 160, toFixed: 2 },
createFormOption: {
comType: 'input-number',
},
editFormOption: {
comType: 'input-number',
},
detailPanelOption: {},
},
stock: {
type: 'number',
label: '库存',
tableOption: { width: 160 },
createFormOption: {
comType: 'input-number',
},
detailPanelOption: {},
},
create_time: {
type: 'string',
label: '创建时间',
tableOption: {},
searchOption: {
comType: 'dateRange',
},
detailPanelOption: {},
},
},
required: ['product_name', 'price'],
},
可以看到,同一个字段在不同组件中可以有完全不同的呈现:product_name 在新增表单里是输入框(带默认值)、在搜索里是动态下拉、在详情里只读展示。comType 是控件类型声明,常用取值如下:
| comType | 控件 | 说明 |
|---|---|---|
input |
文本输入框 | 最通用的控件 |
select |
下拉选择 | 配合 enumList 配置静态枚举 |
input-number |
数字输入 | 配合 minimum / maximum 做范围校验 |
dateRange |
日期范围选择 | 常用于搜索条件 |
dynamicSelect |
动态下拉 | 配置 api,由接口返回枚举数据 |
组件解析器
动态组件能生效,靠的是组件解析器。它的工作分为两个层面:
1. 动态组件层面 :模板页的按钮触发 showComponent 事件,携带 comName(组件名),解析器从 componentConfig 中取出对应配置并渲染该组件。前面里程碑 3 中表格按钮的配置在这里被真正用上:
js
tableConfig: {
rowButtons: [{
label: '查看详情',
eventKey: 'showComponent',
eventOption: { comName: 'detailPanel' },
}, {
label: '编辑',
eventKey: 'showComponent',
eventOption: { comName: 'editForm' },
}, {
label: '删除',
eventKey: 'remove',
eventOption: {
params: { product_id: 'schema::product_id' },
},
}],
},
2. 表单字段层面 :动态组件内部(如 editForm)渲染时,遍历 schema.properties,读取每个字段对应的 xxxOption,根据 comType 从控件注册表 FormItemConfig 中取出真正的子组件,通过 Vue 的 <component :is> 动态渲染:
js
// components-config:控件注册表
import EditInput from './controls/edit-input.vue';
import EditSelect from './controls/edit-select.vue';
import EditInputNumber from './controls/edit-input-number.vue';
import EditDateRange from './controls/edit-date-range.vue';
import EditDynamicSelect from './controls/edit-dynamic-select.vue';
export const FormItemConfig = {
input: { component: EditInput },
select: { component: EditSelect },
'input-number': { component: EditInputNumber },
dateRange: { component: EditDateRange },
dynamicSelect: { component: EditDynamicSelect },
};
表单组件内部的渲染核心(以 editForm 为例,省略样式与校验细节):
vue
<template>
<el-form v-if="schema?.properties" :model="model" :rules="rules" label-width="100px">
<el-form-item
v-for="(field, key) in schema.properties"
v-show="field[editFormOption]?.visible !== false"
:key="key"
:label="field.label"
:prop="key"
>
<component
:is="FormItemConfig[field[editFormOption]?.comType]?.component"
v-model="model[key]"
v-bind="field[editFormOption]"
/>
</el-form-item>
</el-form>
</template>
<script setup>
import { computed } from 'vue';
import { FormItemConfig } from '@/components-config';
const props = defineProps({
schema: Object, // 字段结构
model: Object, // 表单数据(由外层按 mainKey 注入)
});
// 当前组件对应的字段级配置 key(editForm 时读取 editFormOption)
const editFormOption = 'editFormOption';
// 根据字段配置生成校验规则(json-schema 规则 → el-form rules)
const rules = computed(() => {
const result = {};
for (const [key, field] of Object.entries(props.schema.properties || {})) {
const opt = field[editFormOption] || {};
if (opt.required !== false && props.schema.required?.includes(key)) {
result[key] = [{ required: true, message: `${field.label}不能为空` }];
}
}
return result;
});
</script>
两个关键点:
- 字段配置与渲染解耦:组件不关心业务字段,只负责"遍历 properties,按 comType 找控件渲染",新增字段只是加一段配置;
- 配置天然支持校验 :
schema.properties里的required、minLength、maximum等 json-schema 规则,可以直接映射成 el-form 的校验规则,前端校验和后端参数校验共用一份字段定义(呼应里程碑 3 的服务端能力)。
新增一个动态组件的完整流程
框架内置了 createForm、editForm、detailPanel 三种动态组件,但实际业务中往往需要更多。好在扩展一个动态组件只需要四步,完全不碰框架核心代码:
第一步:DSL 配置
在 componentConfig 中声明新组件,并在字段上挂载对应的 xxxOption:
js
componentConfig: {
importPanel: {
title: '批量导入',
saveBtnText: '开始导入',
},
},
// 字段上挂载
importPanelOption: {
comType: 'upload', // 上传控件
},
第二步:新建组件文件并注册
在 components 目录下新建组件,然后在 components-config 中注册:
js
// components/import-panel/import-panel.vue
// ... 组件实现
// components-config
import ImportPanel from './import-panel/import-panel.vue';
export const ComponentConfig = {
createForm: { component: CreateForm },
editForm: { component: EditForm },
detailPanel: { component: DetailPanel },
importPanel: { component: ImportPanel }, // 新组件注册
};
第三步:修改 model 配置
在对应的 model 文件中,把新组件的 componentConfig 和字段的 xxxOption 补上(即第一步的配置落到具体系统的配置里)。
第四步:补齐接口
在 router-schema、router、controller、service 中新增对应的接口处理逻辑。这一步正好用上里程碑 2 的 elpis-core------它通过 loader 自动加载约定目录下的文件,新增接口不需要改框架,只需要按约定添加文件即可。
至此,一个新的动态组件就完整接入系统了。
灵活性与自由度
对比传统开发和配置驱动开发,差异很直观:
| 维度 | 传统开发 | 动态组件配置驱动 |
|---|---|---|
| 新增一个表单 | 新建页面 + 组件 + 接口 | 四步配置 + 组件文件 |
| 字段增减 | 改代码 | 改配置 |
| 控件类型变化 | 改代码 | 改 comType |
| 新增业务系统 | 重新开发 | 继承领域模型 + 写配置 |
另外,动态组件内部并不是只能使用框架提供的基础控件:它可以使用我们提供的 schema 组件(如 SchemaForm),也可以嵌套其他自定义内容。也就是说,动态组件本身也是一个"容器",自由度可以逐层放大------配置化解决 80% 的标准场景,自定义能力兜住 20% 的特殊场景。
总结
这个里程碑补上了建站框架的重要一环:
- 从"列表页配置化"到"全组件配置化":schema-table 解决列表,动态组件把表单、详情、甚至任意自定义组件都纳入了 DSL 体系;
- 两级配置设计 :
componentConfig管组件级行为(标题、主键、按钮),字段级xxxOption管字段级呈现(控件、显隐、默认值),职责清晰、互不干扰; - 解析器是核心 :
showComponent+FormItemConfig注册表 +<component :is>,让"组件按配置动态渲染"成为可能,扩展新组件不需要改框架; - 前后端配置同源:字段的 json-schema 规则同时驱动前端表单校验和后端参数校验,一份定义多处使用。
回顾四个里程碑:工程化解决了构建问题,elpis-core 沉淀了后端接口流程,建站框架用 DSL + 领域模型生成了系统的骨架,动态组件让这个骨架上的每个"器官"也可以按需配置。到这一步,一个中后台系统从页面到表单,真正做到了"配置优先、代码兜底"。
注:抖音"哲玄前端",《大前端全栈实践课》