[学习笔记] 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。每一层都只认"数据结构",不认具体业务。
相关推荐
用户34346127832026 分钟前
.NET 中任务调度的底层机制与你可能不知道的隐藏陷阱
架构
咖啡无伴侣32 分钟前
2. 从零搭建企业级 Monorepo 工程化模板:ESLint 10 (基础骨架)+ Prettier 配置与避坑指南
前端·架构
Dawson Zhu34 分钟前
从虚拟内存到 Agent 记忆:把大模型的“脑补“变成“查表“
人工智能·语言模型·架构·aigc·agi
Raas10035 分钟前
MAI Gateway(魔芋企业级AI网关)详解:AI网关在架构中的位置,一文读懂企业AI流量治理
大数据·人工智能·架构·gateway·ai网关·mai gateway
陈珙1 小时前
.NET AI 实战:用 MCP 把 Claude Code 变成数据库 DBA
架构·.net·技术
2601_962074581 小时前
大数据-260 实时数仓 - 项目背景与需求 实时数仓架构 需求分析 技术选型 逻辑架构
大数据·架构
AIOps打工人1 小时前
【AIOPS】当运维 Agent 开始自己学:证据补全才是自学习的真门槛
程序员·架构
8731 小时前
在 Uber 规模下高效运行软件工厂
人工智能·架构
Capricorn19882 小时前
防编造架构实战:对比 Gemini Notebook 解析知芽 Notebook Skill 的工程实现
人工智能·笔记·elasticsearch·架构·知识图谱·论文笔记
minhuan3 小时前
AI Infra全栈拆解:大模型应用背后的基础设施,算力集群、网络、存储与服务治理体系26.0
人工智能·架构·ai infra·ai基础设施·ai任务调度