基于 DSL 与领域模型的系统构建方案

197 阅读11分钟

本文为全栈课程学习笔记——基于 DSL 与领域模型的系统构建方案

一、框架核心设计目标与价值

1.1 解决行业痛点:重复性开发困境

在传统企业级系统开发中,存在大量重复性工作。不同项目间 80% 的功能(如列表展示、列表搜索、表单操作等)高度相似,但需反复进行 "复制 - 粘贴 - 修改" 的编码工作。这种模式不仅严重消耗开发效率,更限制了技术团队的能力提升空间。

1.2 Elpis 框架核心目标

通过标准化与模块化设计,将通用功能沉淀为可复用组件与配置体系,实现:

  • 效率提升:使 80% 重复性工作通过配置化完成,开发团队仅需聚焦 20% 定制化需求
  • 一致性保障:通过统一规范确保系统架构与交互体验的标准化
  • 能力聚焦:释放开发人员精力,专注于业务逻辑与技术创新

二、核心技术架构与实现思路

2.1 基础架构

Elpis 框架采用 "多页面 + 单页面" 混合渲染模式,核心技术链路如下: image.png

其中DSL即是模板页面的描述,一份DSL描述对应一个模版页面。

DSL描述文件 + 模板页面 + 模版解析器 ==> 可运行的若干项目应用(a系统、b系统、c系统等)
  • DSL 设计:采用 JSON 格式描述页面结构与行为,实现配置即代码
  • 模板系统:提供标准化 UI 组件与布局模板,支持可视化编辑
  • 解析引擎:将配置转换为可执行的前端代码,支持动态渲染

2.2 领域模型驱动的架构设计

既然DSL描述与模板页是一一对应关系,那么如何做到一份DSL,一份模板页,解析并生成多个相似子系统呢?———领域模型

image.png

基于面向对象思想构建领域模型体系,将a系统、b系统、xxx系统的通用功能抽离并沉淀为领域模型基类DSL配置),基于该领域模型配置派生出各个子类项目配置(子类DSL配置),即项目与模型是继承与被继承的关系。

三、DSL 设计、配置、维护实现

以管理后台常用的列表功能页面为例(以下统称为 Dashboard )。

3.1 Dashboard 模板 DSL 规范

Dashboard核心 DSL 结构如下

{
  mode:'dashboard', // 模板类型,不同模板类型对应不一样的数据结构
  name: '', // 名称
  desc: '', // 描述
  icon: '',
  homePage:'', // 首页(项目配置)
  
  // 头部菜单配置
  menu:[{
	  key:'', // 菜单唯一描述
    name: '', // 菜单名称
    menuType:'', // 枚举值:group / module
    
    // menuType === group 时,可填
    sunMenu: [{
      // 可递归sunMenu
    }, ...]
    
	  // menuType === module 时,可填
    moduleType: '', // 枚举值:sider/iframe/custom/schema
    
    // moduleType === sider 时,用于处理顶部菜单点击后出现左侧菜单的复杂情况
    siderConfig:{
	    menu:[{
		    .. // 可递归menuItem
	    }]
    }, 
    
    // moduleType === iframe 时
    iframeConfig:{}
    
    // moduleType === custom 时
    customConfig:{}
    
     // moduleType === schema 时,用于跳转到通用组件页面
     schemaConfig:{}
  }]
 }

基于 dashboard DSL,经解析器渲染的页面结构及功能如下: image.png

点击头部菜单:

  • 一级菜单(菜单项):展示 SchemView、SiderView(侧边栏)、IframeView、CustomView
  • 二级菜单(菜单组):展示 SchemView、SiderView(侧边栏)、IframeView、CustomView
  • 侧边栏菜单:
    • 菜单项:展示 SchemView、IframeView、CustomView
    • 菜单组:展示 SchemView、IframeView、CustomView

3.2 基于领域模型架构维护DSL

model
    |-- business //电商领域
    |   |-- project
    |   |   |--jd.js
    |   |   |--pdd.js
    |   |   |--taobao.js
    |   |-- model.js
    |-- course // 教育领域
    |   |-- project
    |   |   |--bilibili.js
    |   |   |--douyin.js
    |   |-- model.js  

创建model文件夹,用于存放所有的领域模型配置及领域下的所有项目配置。其中:

  • model.js——存放领域的模型配置,作用是配置公共功能
  • project/xxx.js——存放领域下的所有子项目配置,作用是配置具体项目的定制化部分

这样,添加一个新系统时,只需要在project下添加一份项目配置文件 xxx.js,进行定制化的功能配置即可。

3.3 DSL 解析引擎核心实现

有了领域模型DSL 配置 及 具体项目的DSL配置,如何组织合适的数据结构提供给外部进行引用呢?——>DSL解析引擎

3.3.1 模型数据结构构建

遍历model文件夹下的文件,构造模型数据结构,进行初始化模型和项目的数据结构 modelList

module.exports = (app) => {
  const modelList = [];
  // 遍历当前文件夹,构造模型数据结构,挂载到modelList上
  const modelPath = path.resolve(app.baseDir, `.${sep}model`);
  const fileList = glob.sync(path.resolve(modelPath, `.${sep}**${sep}**.js`));
  fileList.forEach((file) => {
    if (file.indexOf('index.js') > -1) return;

    // 区分配置类型 [model / project]
    const type = file.indexOf('/model.js') > -1 ? 'model' : 'project';

    if (type === 'project') {
      const modelKey = file.match(/\/model\/(.*?)\/project/)?.[1];
      const macReg = file.match(/\/project\/(.*?)\.js/)?.[1]; // mac
      const winReg = file.match(/(?:\/|^)project\/([^\/]*)\.js$/)?.[1]; // window
      const projKey = process.platform === 'win32' ? winReg : macReg;
      let modelItem = modelList.find((item) => item.model?.key === modelKey);
      if (!modelItem) {
        //初始化 model 数据结构
        modelItem = {};
        modelList.push(modelItem);
      }
      if (!modelItem.project) {
        //初始化 project 数据结构
        modelItem.project = {};
      }
      modelItem.project[projKey] = require(path.resolve(file));
      modelItem.project[projKey].key = projKey; // 注入 projectKey
      modelItem.project[projKey].modelKey = modelKey; // 注入 modelKey
    }
    if (type === 'model') {
      const modelKey = file.match(/\/model\/(.*?)\/model\.js/)?.[1];

      let modelItem = modelList.find((item) => item.model?.key === modelKey);
      if (!modelItem) {
        //初始化 model 数据结构
        modelItem = {};
        modelList.push(modelItem);
      }
      modelItem.model = require(path.resolve(file));
      modelItem.model.key = modelKey; // 注入 modelKey
    }
  });

得到的modelList数据结构如下:

  [
    // buiness领域
      {
        model: { model: 'dashboard', name: '电商系统', menu: [Array], key: 'buiness' },
        project: { jd: [Object], pdd: [Object], taobao: [Object]}
      },
      // course领域
      {
        model: { model: 'dashboard', name: '教育系统', menu: [Array], key: 'course' },
        project: {  bilibili: [Object], douyin: [Object] }
      }
    ]

此时project中的项目还只含自身的配置,还未继承model.js中的公共配置。

3.3.2 配置继承与合并机制

  • 基类 DSL 配置:抽离行业通用功能形成领域模型(如电商领域、教育领域)
  • 子类项目 DSL 配置:基于领域模型派生具体项目,仅需配置差异化功能
  • 继承机制:通过合并算法实现配置继承,支持配置的修改、新增、保留三种操作情形

继承机制的处理思路:

  • 子类 DSL 中有键值,基类 DSL 也有 ——> 修改(重载);
  • 子类 DSL 中没有键值,基类 DSL 有键值 ——> 保留(继承);
  • 子类 DSL 中有键值,基类 DSL 没有 ——> 新增(拓展)。
/**
 * 继承 model 配置
 * @param {*} model 模型配置
 * @param {*} project 项目配置
 */
const projectExtendModel = (model, project) => {
  return _.mergeWith({}, model, project, (modelValue, projectValue) => {
    // 处理数据合并的特殊情况
    if (Array.isArray(modelValue) && Array.isArray(projectValue)) {
      let result = [];

      // 处理修改和保留
      for (let i = 0; i < modelValue.length; i++) {
        let modelItem = modelValue[i];
        const projItem = projectValue.find(
          (projItem) => projItem.key === modelItem.key
        );
        // project中存在的key,model中也存在,则递归调用 projectExtendModel 方法覆盖修改
        result.push(
          projItem ? projectExtendModel(modelItem, projItem) : modelItem
        );
      }
      // 处理新增
      for (let i = 0; i < projectValue.length; i++) {
        let projItem = projectValue[i];
        const modelItem = modelValue.find(
          (modelItem) => modelItem.key === projItem.key
        );
        if (!modelItem) {
          result.push(projItem);
        }
      }
      return result;
    }
  });
};

// 使用 projectExtendModel
  modelList.forEach((item) => {
    const { model, project } = item;
    for (const key in project) {
      project[key] = projectExtendModel(model, project[key]);
    }
  });

经过上述处理,project下各项目的配置已经继承了model.js的公共部分。

四、Dashboard 核心功能模块实现

4.1 多类型页面渲染体系

dashboard页面支持SchemView、SiderView、IframeView、CustomView页面的渲染,重点介绍SchemView的实现。

4.1.1 iframe-view 实现

支持第三方系统集成:

{
  key: 'quality-setting',
  name: '店铺资质',
  menuType: 'module',
  moduleType: 'iframe',
  iframeConfig: {
    path: 'http://www.baidu.com'
  }
}

4.1.2 custom-view 实现

支持自定义组件集成:

{
    key: 'info-setting',
    name: '店铺信息',
    menuType: 'module',
    moduleType: 'custom',
    customConfig: {
        path: '/todo'
    }
}

4.1.3 sider-view 实现

支持多级侧边菜单导航:

{
    key: 'data',
    name: '数据分析',
    menuType: 'module',
    moduleType: 'sider',
    siderConfig: {
      menu: [{
        key: 'analysis',
        name: '电商罗盘',
        menuType: 'module',
        moduleType: 'custom',
        customConfig: {
          path: '/todo'
        }
      }, {
        key: 'sider-search',
        name: '信息查询',
        menuType: 'module',
        moduleType: 'iframe',
        iframeConfig: {
          path: 'https://www.baidu.com/'
        }
      }
      ...
    }

4.2 Schema-View 核心模块设计

管理后台通用的数据操作模式(如“数据查询、表单操作、列表”)本质上是对一张数据库表一份特定数据结构的操作,基于此,schema-view 以数据模型驱动界面渲染的设计思想,将业务板块(搜索栏、表格、表单等)抽象为基于 JSON Schema 的描述文件。一份完整的schemaConfig可同时定义:

  • 数据源:通过 RESTful API 对接数据库表结构;
  • 界面组件:基于字段在不同业务板块的定制配置自动生成搜索框、表格列、表单等控件;
  • 交互逻辑:通过事件配置(如表格行按钮、搜索提交)关联数据操作(增删改查)。

围绕以下示例页面介绍: image.png

4.2.1 schema配置

通过 Schema DSL 描述完整业务板块:

  schemaConfig: {
     api:'', //数据源API(遵循RESTFUL 规范)
     schema: { //板块数据结构
       type: 'object',
       properties: {
         key: {
           ...schema, // 标准schema配置
           type: '', // 字段类型
           label: '', //字段中文名
           // 字段在table中的相关配置
           tableOption:{
             ...elTableColumnConfig, //标准的el-table-column
             visiable: true, // 默认为true(false 或不配置时表示不在表单中显示)
           },
           // 字段在search中的相关配置
           searchOption: {
             ...eleComponentConfig,
             comType:'',// 配置组件类型 input/select/......
             default: '',// 默认值

             // comType === 'select'
             enumList:[], //下拉框可选项

             // comType === 'dynamicSelect'
             api:''
           }
         },
         ...
       }
     },
     // table配置
     tableConfig:{
       headerButtons: [{
         label:'', // 按钮中文名
         eventKey:'', // 按钮事件名
         eventOption: {}, // 按钮事件具体配置
         ...elButtonConfig, // 标准 el-button 配置
       }, ...],
       rowButtons: [{
         label: '', // 按钮中文名
         eventKey: '', // 按钮事件名
         eventOption: {
           // 当eventKey === 'remove'时
           params:{
             // paramKey = 参数键值
             // rowValueKey = 参数值格式为schema::tableKey时,到table中找相应的字段
             paramKey: rowValueKey 
           },
         }, // 按钮事件具体配置
         ...elButtonConfig, // 标准 el-button 配置
       }, ...]
     },
     searchConfig:{}, // search-bar相关配置
     components:{}, // 模块组件
   }

不同项目仅需修改 API 与具体字段配置,即可快速生成符合业务场景的界面。

以table组件为例,在 该schema 的 table 字段配置中,为每个 key 添加 tableOption 用于配置列的相关属性,如宽度、排序、对齐方式等;在 tableConfig 中添加 headerButtons 和 rowButtons 用于配置操作按钮等功能。

如果该 key 需要展示在 search 组件,则在key里继续按照规则添加searchOption,用于配置其在search组件中的相关功能。

4.2.2 配置解析与运行时处理

有了schemaConfig 配置,如何将该配置转换为具体的界面显示呢?——> vue3 Hook 机制

在 hook 中解析配置,拆分出 特定组件的 schema 和 config 内容,提供给组件,实现配置到界面的转换。

// 构造 schemaConfig 相关配置,输送给 schemaView 解释
const buildData = function(){
   const { key, sider_key: siderKey } = route.query;
   const mItem = menuStore.findMenuItem({
     key: 'key',
     value: siderKey ?? key
   });

   if(mItem && mItem.schemaConfig){
     const {schemaConfig: sConfig} = mItem;

     const configSchema = JSON.parse(JSON.stringify(sConfig.schema));
     api.value = sConfig.api ?? '';
     tableSchema.value = {};
     tableConfig.value = undefined;
     searchSchema.value = {};
     searchConfig.value = undefined;

     nextTick(() => {
       // 构造 tableSchema 和 tableCconfig
       tableSchema.value = buildDtoSchema (configSchema, 'table');
       tableConfig.value = sConfig.tableConfig;
       
       // 构造 searchSchema 和 searchConfig
       const dtoSearchSchema = buildDtoSchema (configSchema, 'search');
       for(const key in dtoSearchSchema.properties){
         if(route.query[key] !== undefined){
           dtoSearchSchema.properties[key].option.default = route.query[key];
         }
       }
       searchSchema.value = dtoSearchSchema;
       searchConfig.value = sConfig.searchConfig;
     });
   }
 };
 
 // 通用构建 schema 方法(清除噪音)
 const buildDtoSchema = function(_schema, comName){
   if(!_schema?.properties){
     return;
   }
   const dtoSchema = {
     type: 'object',
     properties: {}
   };
   for(const key in _schema.properties){
     const props = _schema.properties[key];
     if(props[`${comName}Option`]){
       let dtoProps = {};
       // 提取 props中非 option 的部分,存放到dtoProps中
       for(const pKey in props){
         if(pKey.indexOf('Option')<0){
           dtoProps[pKey] = props[pKey];
         }
       }
       // 处理 comName Option
       dtoProps = Object.assign({}, dtoProps, {option: props[`${comName}Option`]});
       dtoSchema.properties[key] = dtoProps;
     }
   }
   // 提取有效 schema 字段信息
   return dtoSchema;
 };
  • buildDtoSchema:从原始 schema 中过滤并重组特定组件(如表格table)所需的字段配置,生成纯净的 DTO(数据传输对象)格式。该DTO只包含目标组件所需信息的新 schema 对象。

  • buildData:通过buildDtoSchema构建表格组件所需的配置对象(tableConfig、tableSchema),触发配置构建,通过provider/inject的方式输送给 schemaView 解释。

可拓展性体现:未来可继续通过添加新组件的schema xxxOption配置、xxxConfig配置,实现其他组件的渲染。

五、框架设计总结

5.1 Elips的总体架构

模版-模型-项目.png

5.1 模板-模型-项目三级架构

模板层(Dashboard1/Dashboard2/Dashboard...)
    |-- 领域模型层(电商/教育/医疗等行业模型)
    |   |-- 项目实例层(京东/淘宝/拼多多等具体项目)
    |-- 解析引擎层(模板解析器/模型解析器)  
  • 模板:系统支持多个模板,每个模板有对应的 DSL、模板描述、模板解析器和页面。
  • 模型与项目:选定模板后可建多个领域模型,每个模型派生出不同项目,项目结合解析引擎生成具体可用系统 。

5.2 框架可拓展性体现

  1. 模板与引擎拓展:框架层面可拓展不同模板(如 dashboard 1、2、3 等)及对应的模板引擎,实现多样化页面需求;

  2. 页面定制化功能拓展: 通过 custom-view 和 iframe-view 可满足用户在框架配置不足时的定制化需求;

  3. DSL中的 schema 拓展:如 schema 可进一步拓展不同的 schemaView 及解析引擎,实现不同类型的业务功能封装;

  4. 组件拓展:schema-view 中的各种组件(搜索框、表格、弹窗等)可动态拓展,通过加 option、config 配置新组件。

六、总结与技术价值

6.1 核心技术创新点

  • 配置化开发模式:通过 DSL 描述替代 80% 以上的前端编码工作,实现 “配置即界面” 的开发范式
  • 领域模型驱动:基于面向对象思想构建行业级可复用模型,通过继承机制实现 “一次建模、多项目复用”
  • 数据驱动:利用数据驱动的概念,实现“一份schema,多组件复用” 的高效开发模式
  • 多模式渲染:支持 iframe/custom/sider/schema 等多种渲染方式,兼容第三方系统与自定义功能

6.2 工程实践价值

  • 效率提升:极大缩短新项目开发周期
  • 质量保障:通过标准化减少重复性开发容易带来的编码错误
  • 技术积累:形成可复用的行业组件库与配置模板
  • 团队赋能:初级工程师可快速构建复杂业务系统

通过这套基于 DSL 与领域模型的框架设计,Elpis 成功实现了企业级系统开发从 "编码为主" 向 "配置为主" 的模式转变,为技术团队提供了高效、标准化的系统构建解决方案。

注:抖音“哲玄前端”,《全栈实践课》