基于 Schema(DSL) 的动态组件渲染 ------ 学习心得体会
一、DSL概述(一句话)
把"页面"从代码里抽出来,变成纯数据(DSL),用一份 Schema 描述业务字段,前端引擎读取 Schema 自动渲染出 Table / Search / CreateForm / EditForm / DetailPanel,后端用同一份 Schema 做参数校验。
传统模式是"一个页面 = 一堆重复的 .vue 文件";这个分支的目标是"一次配置 Schema,全场景复用"。
二、整体架构:一份 Schema,两层消费
bash
┌─────────────────────────────────────────┐
│ docs/dashboard-model.js (DSL 唯一事实源) │
│ schemaConfig: { api, schema, │
│ tableConfig, searchConfig, │
│ componentConfig } │
└───────────────┬─────────────────────────┘
│ 读取
┌──────────────────┴──────────────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│ 前端渲染引擎 │ │ 后端动态接口 │
│ schema-view.vue │ │ router-schema + │
│ 组合 + 事件编排 │ │ controller/business │
└─────────────────┘ └─────────────────────┘
三层结构拆解
| 层 | 职责 |
|---|---|
| DSL 层 | 定义业务模型的 Schema 与该字段在各场景下的渲染配置 |
| 编排引擎层 | 读取 DSL → 裁剪出各场景 DTO → 组装组件 → 统一协调事件 |
| 可复用组件层 | 只认 Schema 输入,无业务耦合,纯"渲染器" |
三、关键技术设计
1. DSL 设计:一种字段,多种视图表达
在每个字段 properties[key] 下,按渲染场景拆分 Option,实现了"一个字段定义 + 多场景配置":
js
properties: {
inventory: {
type: 'string',
label: '库存',
tableOption: { ...elTableColumnConfig, toFixed: 0 }, // 表格列
searchOption: { comType: 'input', default: '' }, // 搜索项
createFormOption: { // 新增表单
comType: 'inputNumber', enumList: [], visible: true, disabled: false
},
detailPannelOption: { ... } // 详情面板
}
}
心得:把"我是谁(字段定义)"和"我在哪、长什么样(场景化配置)"分离,是这份 DSL 最巧妙的地方。字段语义只写一次,视图差异走同层 Option,扩展新场景不需要动字段本体。
2. buildDtoSchema:按场景"裁剪" Schema
针对每种渲染类型(table/search/createForm/editForm/detailPanel)遍历字段,只保留定义了对应 xxxOption 的字段 ,并把这个 Option 单独抽出为 option 字段,同时把 required 同步进 option.required:
js
const buildDtoSchema = (schema, type) => {
for (const key in schema.properties) {
if (props[`${type}Option`]) { // 只有配置了该场景才渲染
// 剥离 xxxOption,把 Option 归一化为 option,合并 required
dtoProps = Object.assign({}, dtoProps, { option: props[`${type}Option`] });
}
}
};
心得 :这本质上是一个"按视图投影字段 "的 AOP 思想。同一份原始 Schema 投影出 N 个视图 DTO,各场景互不干扰;新增一个"是否显示"只需给某场景配置里加一行,而不是到处 v-if。
3. 动态渲染核心:<component :is> + 配置映射表
真正让"增删组件无需改页面"的关键两点:
- 引擎侧 :
schema-view.vue用<component :is>遍历components,根据component-config.js找到组件本体渲染;schema-form/schema-search-bar按comType从form-item-config.js/schema-item-config.js找字段组件。 - 组件侧 :所有动态组件、字段组件统一暴露接口约定(
name/onShow/getValue/validate/reset),并统一通过defineExpose暴露给父级用 ref 调用。
js
// schema-form.vue ------ 遍历 Schema,按 comType 动态解析字段组件
<component
:is="FormItemConfig[schemaItem.option?.comType]?.component"
:schema="schemaItem" :schema-key="key" :model="model?.[key]" />
// form-item-config.js ------ 新组件只需加一行映射
const FormItemConfig = {
input: { component: Input },
inputNumber:{ component: InputNumber },
select: { component: Select },
};
心得 :这是"面向扩展开放,面向修改关闭"(OCP)最典型的落地。要支持一种新控件?写一个字段组件实现统一接口,往配置表加一行即可;页面容器、表单容器、SCHEMA 一行都不用改。
4. 数据与事件两套通信
- provide / inject :
schema-view.vue用provide('schemaViewData', {...})注入整份共享数据(api、各场景 schema、components),孙组件inject即可拿到,彻底解决深层 prop 钻透。 - White-list 事件编排 :所有子组件的交互都通过统一的
command/operate事件上抛,由父级EventHandlerMap白名单式分发:
js
const EventHandlerMap = {
showComponent: showComponent, // 打开动态组件抽屉
refreshTable: refreshTable, // 刷新表格
remove: removeData, // 删除行
};
心得 :动态组件之间永远不直接互相调用 ,只向父级发事件、由父级编排。创建成功后发 { event: 'refreshTable' },编辑/详情打开由表格行按钮的 showComponent 分发。这种事件总线 + 白名单注册,让一个随时增减的组件集合仍然高度可控、可预测。
5. 前后端同 Schema 同源
- 前端:表单项用字段的
type/minLength/pattern作为校验规则,直接用ajv.compile(schema)做 JS 侧校验。 - 后端:
app/router-schema/bussiness.js用同样的 JSON Schema 做 AJV 参数校验(api-params-verify中间件)。 - 接口约定一致为 RESTful:
GET /list、GET(详情)、POST(新增)、PUT(更新)、DELETE(删除),前端动态组件只需要一个api字段即可跑通增删改查。
js
// input.vue ------ Schema 即校验规则
const validate = ajv.compile(schema); // 用 Schema 编译校验函数
// 后端同样的 schema 注册进 AJV 中间件,参数不合法直接拦下
心得 :同一份 Schema 既驱动 UI 又驱动校验 ,前端校验错误提示能细化到 maxLength/minLength/pattern/type 的友好文案。校验规则与接口签名"一次定义、多处生效",大幅降低前后端契约漂移。
四、踩坑与可优化点(体会)
- 配置直接硬编码在文档里 :
docs/dashboard-model.js目前更像"样例",真实场景应把 schemaConfig 存到数据库/配置文件,前端按项目与菜单动态拉取。 showComponent用组件name字符串查找 :comListRef.value.find(item => item.name === comName),组件一致性靠命名约定保证,后期可引入统一的组件注册中心 + 类型校验。- AJV 实例每个表单组件单独
inject:schema-form.vue里provide('ajv', new Ajv()),多个表单可各自判逻辑,但每次编译 Schema 有一定开销,可考虑缓存编译结果。 - 字段组件校验逻辑有重复 :
input.vue与select.vue的validate结构高度相似,未来可抽象一个高阶字段组件 / composable,收敛必填、空值、AJV 校验的公共逻辑。 - 事件白名单是硬编码对象 :
EventHandlerMap集中在一个文件里,组件多了会膨胀;后续可按业务域拆分或做成可注册的插件机制。