[学习笔记] Elpis 框架 DSL 设计总结

学习了一个阶段的Elpis框架,现在进行一个阶段性总结

整体架构与数据流

启动顺序 middleware → routerSchema → controller → service → config → extend → router

Schema 设计理念

model/buiness/model.js奠定基类,后续的继承放在model/buiness/project/***.js

javascript 复制代码
// model/buiness/model.js
module.exports = {
  model: 'dashboard',
  name: '电商系统',
  menu: [
    {
      key: 'product',
      name: '商品管理',
      menuType: 'module',
      moduleType: 'schema',
      schemaConfig: {
        api: '/api/proj/product',
        schema: {
          type: 'object',
          properties: {
            product_id: {
              type: 'string',
              label: '商品ID',
              tableOption: {
                width: 300,
                'show-overflow-tooltip': true
              }
            },
            product_name: {
              type: 'string',
              label: '商品名称',
              tableOption: {
                width: 200
              },
              searchOption: {
                comType: 'dynamicSelect',
                api: '/api/proj/product_enum/list'
              }
            },
            price: {
              type: 'number',
              label: '价格',
              tableOption: {
                width: 200
              },
              searchOption: {
                comType: 'select',
                enumList: [
                  {
                    label: '全部',
                    value: -999
                  },
                  {
                    label: '¥39.9',
                    value: 399
                  },
                  {
                    label: '¥199',
                    value: 200
                  }
                ]
              }
            },
            inventory: {
              type: 'number',
              label: '库存',
              tableOption: {
                width: 200
              },
              searchOption: {
                comType: 'input'
              }
            },
            create_time: {
              type: 'string',
              label: '创建时间',
              tableOption: {},
              searchOption: {
                comType: 'dateRange'
              }
            }
          }
        },
        tableConfig: {
          headerButtons: [
            {
              label: '新增商品',
              eventKey: 'showComponent',
              type: 'primary',
              plain: true
            }
          ],
          rowButtons: [
            {
              label: '修改',
              eventKey: 'showComponent',
              type: 'warning'
            },
            {
              label: '删除',
              eventKey: 'remove',
              eventOption: {
                params: {
                  product_id: 'schema::product_id'
                }
              },
              type: 'danger'
            }
          ]
        },
        path: '/todo'
      }
    },
    {
      key: 'order',
      name: '订单管理',
      menuType: 'module',
      moduleType: 'custom',
      customConfig: {
        path: '/todo'
      }
    },
    {
      key: 'client',
      name: '客户管理',
      menuType: 'module',
      moduleType: 'custom',
      customConfig: {
        path: '/todo'
      }
    }
  ]
}

实现路径model/index.js

javascript 复制代码
// project 继承 model 方法
const projectExtendModel = (model, project) => {
  return _.mergeWith({}, model, project, (modelValue, projectValue) => {
    // 处理数组合并的特殊情况
    if (_.isArray(modelValue) && _.isArray(projectValue)) {
      let result = [];

      // 因为 project 继承 model, 所以需要处理修改和新增内容的情况
      // project有的键值, model也有 => 修改(重载)
      // project有的键值, model没有 => 新增(拓展)
      // model有的键值, project没有 => 保留(继承)

      // 处理修改和保留
      for (let i = 0; i < modelValue.length; i++) {
        let modelItem = modelValue[i];
        const projectItem = projectValue.find(
          (item) => item.key === modelItem.key
        );
        // project 有, model 也有 =>递归调用 projectExtendModel 方法覆盖修改
        result.push(
          projectItem ? projectExtendModel(modelItem, projectItem) : modelItem
        );
      }

      // 处理新增
      for (let i = 0; i < projectValue.length; i++) {
        const projectItem = projectValue[i];
        if (!modelValue.find((item) => item.key === projectItem.key)) {
          result.push(projectItem);
        }
      }

      return result;
    }
  });
};

Router-Schema 接口契约 DSL

一份声明 API 入参形态 的 JSON-Schema 映射表。你不需要写校验函数,只要声明"这个接口期望收到什么",框架自动校验。 位置:app/router-schema/*.js

javascript 复制代码
// app/router-schema/business.js
module.exports = {
  '/api/proj/product/list': {
    get: {
      query: {                       // 校验 URL query
        type: 'object',
        properties: { page: { type: 'string' }, size: { type: 'string' } },
        required: ['page', 'size']
      }
    }
  },
  '/api/proj/product': {
    delete: {
      body: {                        // 校验请求体
        type: 'object',
        properties: { product_id: { type: 'string' } },
        required: ['product_id']
      }
    }
  }
}

一份配置描述一个完整 CRUD 页面

css 复制代码
// model/buiness/model.js
module.exports = {
  model: 'dashboard',          // 模型标识(分组维度)
  name: '电商系统',             // 项目/模型展示名
  menu: [
    {
      key: 'product',
      name: '商品管理',
      menuType: 'module',      // module | group | sider
      moduleType: 'schema',    // schema(配置驱动) | custom(自定义页)
      schemaConfig: {          // ------ 页面编排的核心 DSL ------
        api: '/api/proj/product',
        schema: {
          type: 'object',
          properties: {
            product_id:   { type:'string', label:'商品ID',   tableOption:{ width:300 } },
            product_name: { type:'string', label:'商品名称',
                            tableOption:{ width:200 },
                            searchOption:{ comType:'dynamicSelect', api:'/api/proj/product_enum/list' } },
            price:        { type:'number', label:'价格',
                            searchOption:{ comType:'select', enumList:[...] } },
            create_time:  { type:'string', label:'创建时间',
                            searchOption:{ comType:'dateRange' } }
          }
        },
        tableConfig: {
          headerButtons: [ { label:'新增商品', eventKey:'showComponent', type:'primary' } ],
          rowButtons: [
            { label:'修改', eventKey:'showComponent', type:'warning' },
            { label:'删除', eventKey:'remove',
              eventOption:{ params:{ product_id:'schema::product_id' } }, type:'danger' }
          ]
        },
        path: '/todo'
      }
    },
    { key:'order', name:'订单管理', menuType:'module', moduleType:'custom', customConfig:{ path:'/todo' } }
  ]
}

这份配置声明了:

字段 含义
menuType module(具体页面)/ group(菜单分组)/ sider(带侧边栏)
moduleType schema(由 schemaConfig 自动生成 CRUD 页)/ custom(走自定义 Vue 页)
schemaConfig.api 数据来源接口,组件会自动拼 /list、DELETE 用它本体
schema.properties[key] 一个字段的"多面描述":表格列、搜索项、类型、中文名
tableOption 该字段在表格 中的展现(透传给 el-table-column
searchOption 该字段在搜索栏 中的展现(comType 决定输入控件)
tableConfig.headerButtons/rowButtons 表格顶部/行内按钮,eventKey 绑定行为

分层继承:model 与 project

同一套模型可以被多个"项目"复用,项目对模型做局部覆盖。这是 elpis 的第二层 DSL 能力。

目录约定:

复制代码
model/
  buiness/
    model.js                 ← 基础模型(通用配置)
    project/
      jd.js                  ← 京东项目覆盖
      taobao.js              ← 淘宝项目覆盖
      pdd.js                 ← 拼多多项目覆盖

model/index.jsprojectExtendModel 实现了"按 key 深度合并"的继承语义:

  • model 有、project 也有(同 key) → 递归覆盖(修改);
  • project 有、model 没有 → 追加(扩展);
  • model 有、project 没有 → 保留(继承)。
scss 复制代码
// model/index.js 的核心合并逻辑
modelList.forEach((item) => {
  const { model, project } = item
  for (const key in project) {
    project[key] = projectExtendModel(model, project[key])  // project 继承 model
  }
})

数组按 item.key 对齐后再递归合并,保证"同 key 覆盖、异 key 新增"而非简单拼接。

后端如何下发这份配置

app/service/project.js 直接 require('../../model/index') 拿到已继承好的 modelListapp/controller/project.js 暴露三个接口把配置变数据:

接口 作用
GET /api/project?proj_key= 取某个项目(projKey)的完整配置
GET /api/project/list 取项目列表(用于项目切换器)
GET /api/project/model_list 取所有 model + project 的结构化数据

前端 dashboard.vueonMounted 拉取 /api/project → 写入 menuStore,菜单点击后根据 moduleType 路由到 /view/dashboard/schema 或自定义页

Schema UI 渲染 DSL

前端不需要为每个页面写表格/表单,而是用"解释器组件"把上层的 schemaConfig 渲染出来。

schema-view 拆分配置

app/pages/dashboard/complex-view/schema-view/hook/schema.js 是关键的"翻译层":它从 menuStore 取到 schemaConfig,用 buildDtoSchema 把同一份 properties 拆成两份视图 schema:

ini 复制代码
// 只抽取 tableOption → 重命名为 option → 喂给表格
tableSchema = buildDtoSchema(configSchema, 'table')
// 只抽取 searchOption → 重命名为 option → 喂给搜索栏
searchSchema = buildDtoSchema(configSchema, 'search')

buildDtoSchema 的规则:遍历 properties,只保留带 ${comName}Option 的字段,把非 Option 属性(如 type/label)连同 option 一起输出。这样同一字段在表格和搜索栏可以有完全不同的表现,但只声明一次。

schema-table:配置即表格

app/pages/widgets/schema-table/schema-table.vue 直接遍历 schema.properties 生成列:

ini 复制代码
<template v-for="(schemaItem, key) in schema.properties">
  <el-table-column
    v-if="schemaItem.option.visible !== false"
    :prop="key" :label="schemaItem.label"
    v-bind="schemaItem.option"   <!-- tableOption 直接透传给 el-table-column -->
  />
</template>

它还会自动:拼 ${api}/list?page&size 拉数据、res.metadata.total 做分页、支持 option.toFixed 格式化数字。

schema-search-bar:配置即表单

app/pages/widgets/schema-search-bar/schema-search-bar.vue 遍历 properties,按 option.comType 选择具体输入控件:

ruby 复制代码
<component
  :is="SearchItemConfig[schemaItem.option?.comType].component"
  :schema-key="key" :schema="schemaItem"
/>

app/pages/widgets/schema-search-bar/schema-item-config.js 维护控件映射表:

css 复制代码
const SearchItemConfig = {
  input:          { component: input },
  select:         { component: select },
  dynamicSelect:  { component: dynamicSelect },   // 下拉项来自远端 api
  dateRange:      { component: dateRange }
}

设计要点comType 是一个有限枚举的词汇表------这正是 DSL 的"语法"特征:只能从约定的控件集里选,组件负责把它落成真实 UI。

按钮与事件的声明式绑定

tableConfig.rowButtons 里的 eventKey 是行为的"动词表":

  • showComponent → 打开弹窗表单(自定义实现);
  • remove → 删除,eventOption.params'schema::product_id' 这样的占位语法,从当前行数据取值后发 DELETE 请求(table-panel.vue 解析 schema:: 前缀)。

设计思想小结

elpis 的 DSL 体系有四条主线:

  1. 声明式优于命令式:接口校验、页面表格、搜索表单都用"描述意图"代替"编写过程"。
  2. 配置即数据:model/project 本质上是 JSON 配置,可被加载、继承、下发、解释,和代码解耦。
  3. 分层继承(model → project) :用"覆盖式合并"实现"一套模型、多项目复用",是低代码里"模板 + 实例"的经典模式。
  4. 解释器模式贯穿始终:Loader 解释声明 → 后端中间件解释契约 → 前端 widget 解释 schema。每一层都只认"数据结构",不认具体业务。
相关推荐
zhangjw341 小时前
第41篇:微服务入门:架构理念+核心组件,理解微服务本质
微服务·云原生·架构
hey you~2 小时前
北京连锁零售门店云客服系统搭建实战指南:从架构选型到落地验收
架构·零售·连锁零售·云客服搭建·门店数字化·通信原生架构
三品PLM系统2 小时前
PLM平台在制造业档案治理中的技术落地:OPPO设备后勤部架构、编码与权限解析 | 三品软件
架构·软件工程·plm·工程文档管理·制造业档案治理·三品软件
阿图灵3 小时前
Agentic AI 架构入门(十二·完结):ADLC、AgentOps 与企业级平台蓝图
人工智能·架构·ai agent·智能体·agentops·agentic ai·adlc
Warren2Lynch3 小时前
从文本到架构:Visual Paradigm AI Chatbot V2 深度评测与实战指南引言
人工智能·架构
cxr8283 小时前
D2E 深度剖析 时序工具链
人工智能·架构
k4m7v2pz3 小时前
Rust 高并发 WebSocket 连接管理:从线程地狱到 tokio 异步架构
websocket·架构·rust·并发编程·tokio
cxr8284 小时前
上下文工程框架之11 模块与优先级链和冲突消解、淘汰与版本
人工智能·架构
隔窗听雨眠4 小时前
湖仓一体架构深度解读:Apache Doris如何打破数据边界实现查询提速30倍
架构·apache