elpis : DSL动态组件学习

基于 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-barcomTypeform-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 / injectschema-view.vueprovide('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 /listGET(详情)、POST(新增)、PUT(更新)、DELETE(删除),前端动态组件只需要一个 api 字段即可跑通增删改查。
js 复制代码
// input.vue ------ Schema 即校验规则
const validate = ajv.compile(schema);   // 用 Schema 编译校验函数
// 后端同样的 schema 注册进 AJV 中间件,参数不合法直接拦下

心得 :同一份 Schema 既驱动 UI驱动校验 ,前端校验错误提示能细化到 maxLength/minLength/pattern/type 的友好文案。校验规则与接口签名"一次定义、多处生效",大幅降低前后端契约漂移。


四、踩坑与可优化点(体会)

  1. 配置直接硬编码在文档里docs/dashboard-model.js 目前更像"样例",真实场景应把 schemaConfig 存到数据库/配置文件,前端按项目与菜单动态拉取。
  2. showComponent 用组件 name 字符串查找comListRef.value.find(item => item.name === comName),组件一致性靠命名约定保证,后期可引入统一的组件注册中心 + 类型校验。
  3. AJV 实例每个表单组件单独 injectschema-form.vueprovide('ajv', new Ajv()),多个表单可各自判逻辑,但每次编译 Schema 有一定开销,可考虑缓存编译结果。
  4. 字段组件校验逻辑有重复input.vueselect.vuevalidate 结构高度相似,未来可抽象一个高阶字段组件 / composable,收敛必填、空值、AJV 校验的公共逻辑。
  5. 事件白名单是硬编码对象EventHandlerMap 集中在一个文件里,组件多了会膨胀;后续可按业务域拆分或做成可注册的插件机制。
相关推荐
用户921080262863 小时前
前端 Vue 专栏 06:异步更新机制、任务队列与 nextTick
前端
晴天164 小时前
Chrome DevTools 深度调试指南
前端·chrome·chrome devtools
林冠宏_指尖下的幽灵4 小时前
AI发展下的后编程时代思考
前端·人工智能·后端
_山海5 小时前
Bun入门指南
前端·javascript·后端
想睡懒觉5 小时前
我用 CSS 3D 做了个星空隧道 Loading,没有 Three.js
前端
labixiong5 小时前
transition 写在 CSS 变量上为什么不生效?给变量补张「类型身份证」
前端·css·html
YHL5 小时前
🧠 Agent 记忆进阶:总结压缩与向量检索 —— 从截断到 Milvus 长期记忆
前端·人工智能
用户921080262865 小时前
前端 Vue 专栏 05:从 Object.defineProperty 到 Proxy,彻底理解 Vue 响应式原理
前端
Fluxart.ai5 小时前
GPT Image 2.5 有使用次数限制吗?额度、并发、频率限制说明
服务器·前端·gpt