Elpis 第三阶段· 领域模型架构建设

1.概述

在Elpis项目领域模型架构主要是以基于 JSON Schema 的 DSL(领域特定语言)数据配置驱动页面渲染的架构。我们以一套标准项目提取出公共的Model配置结合可全自定义化的Project配置继承合并成独立完整项目的JSON Schema配置文件,结合dashboard 模板引擎解析最终生成我们需要的中后台项目。基于这样的DSL配置化领域模型架构下将80%业务逻辑沉淀,提高开发效率。

2.领域模型架构

1.领域模型架构设计

2.领域模型架构流程

1.项目DSL配置文件生成

通过Model基础配置与各个Project独立项目配置合并,形成各个项目完整的项目配置内容,并且在BFF Server层提供Api 获取配置项供给前端使用。

JavaScript 复制代码
//`Model`基础配置与各个`Project`独立项目配置合并继承
const _ = require('lodash');

// project 继承 model
const projectExtendModel = (model, project) => {
    return _.mergeWith({}, model, project, (modelValue, projValue) => {
        //处理数组合并的特殊情况
        if (Array.isArray(modelValue) && Array.isArray(projValue)) {
            let result = []

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

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

2. Dashboard 模板引擎搭建

先了解下DSL中JSON Schema配置文件字段以便于好了解Dashboard 模板引擎是如何进行解析

JavaScript 复制代码
{
    model: 'Dashboard',//模版类型,不同模版类型对应不一样的模版数据结构
    name: "",//模版名称
    desc: "",//模版描述
    icon: "",//模版图标
    homePage: "",//首页路径(项目配置)
    //头部菜单
    menu: [{
        key: '',//菜单key,唯一标识
        name: '',//菜单名称
        menuType: '',//菜单类型,枚举值,group / module

        // 当menuType == group 可填,展示子菜单
        subMenu: [{
            //可递归menuItem,{ key,name,menuType... } 
        }, ...]
        
        // 当 menuType == module 可填
        moduleType: '',//模块类型,枚举值: sider / iframe / custom/ schema
        
        // 当 moduleType == sider 时
        siderConfig={
            menu: [{
                // 可递归menuItem(除去moduleType == sider)
                //也就是说在侧边栏的菜单配置不能再 进行moduleType == sider 配置这样不合理
                //多级菜单 menuType=group 配置子菜单
            }]
        }
        
        // 当 moduleType == iframe 时
        iframeConfig: {
            path: ''//iframe 路径
        },
        
        // 当 moduleType == custom 时
        customConfig: {
            path: ''//自定义路由路径
        },
        
        // 当 moduleType == schema 时
        schemaConfig={...}
    }]
}
1. 导航栏与侧边栏

在DSL中根据menu字段进行导航栏上的菜单的渲染,在通过menuType字段判断是否导航栏上当前菜单需要子菜单展示,moduleType表示当前菜单在页面上的展示方式。只是moduleType=sider时需要侧边栏菜单。

  • header-container(导航栏组件) : 根据menu菜单配置内容生成导航栏
  • sider-container(侧边栏组件) : 根据siderConfig.menu配置内容生成侧边栏
2. 页面内容展示方式

我们将main-content 插槽下作为页面的核心渲染区域,主要有三种方式展示页面,以配置项moduleType(iframe/custom/schema)值进行展示区分,在前端以划分不同路由分别解析对应的页面组件。

JavaScript 复制代码
[
   // moduleType === iframe 时,处理iframeConfig
   {
       path: '/view/dashboard/iframe',
       component: () => import('./complex-view/iframe-view/iframe-view.vue')
   },
   // moduleType === schema 时,处理schemaConfig
   {
       path: '/view/dashboard/schema',
       component: () => import('./complex-view/schema-view/schema-view.vue')
   },
   // moduleType === custom 时,处理customConfig
   {
       path: '/view/dashboard/todo',
       component: () => import('./todo/todo.vue')
   },
   // moduleType === sider 时,处理siderConfig
   {
       path: '/view/dashboard/sider',
       component: () => import('./complex-view/sider-view/sider-view.vue'),
       children: [
           {
               path: 'iframe',
               component: () => import('./complex-view/iframe-view/iframe-view.vue')
           },
           {
               path: 'schema',
               component: () => import('./complex-view/schema-view/schema-view.vue')
           },
           {
               path: 'todo',
               component: () => import('./todo/todo.vue')
           },
       ]
   },
   //进行sider路由兜底
   {
       path: '/view/dashboard/sider/:chapters',
       component: () => import('./complex-view/sider-view/sider-view.vue'),
   },
]
2.1 iframe-view

这种方式主要使用iframe控件加载第三方页面,实现不同页面的集成方案

JavaScript 复制代码
moduleType: 'iframe',
iframeConfig: {
   path: 'https://www.baidu.com/',
}
2.2 custom-view

当然以这种方式进行加载时,没有特定组件进行加载,因为这个属于用户自定义路由方式进行呈现, 这样就给用户在该架构下进行定制化开发。

JavaScript 复制代码
// 因为在该架构下首屏采用SSR方式加载,后续页面采用CSR模式进行路由跳转
// 当 moduleType == custom 时 
moduleType: 'custom',
customConfig: { 
    path: '/todo'  //自定义路由路径
},

//前端routes 路由定义
{
    path: '/view/dashboard/todo',
    component: () => import('./todo/todo.vue')
},
2.3 schema-view 重点

schema-view解析架构图,以商品管理模块进行展示 商品管理Schema配置如下

JavaScript 复制代码
        {
            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',
                                placeholder: '请选择商品名称',
                                api: '/api/proj/product_enum/list',
                            }
                        },
                        price: {
                            type: 'number',
                            label: '价格',
                            tableOption: {
                                width: 200,
                            },
                            searchOption: {
                                comType: 'select',
                                placeholder: '请选择价格',
                                enumList: [{
                                    value: -999,
                                    label: '全部'
                                }, {
                                    value: '39.9',
                                    label: '¥39.9'
                                }, {
                                    value: '199',
                                    label: '¥199'
                                }, {
                                    value: '699',
                                    label: '¥699'
                                }]
                            }
                        },
                        inventory: {
                            type: 'number',
                            label: '库存',
                            tableOption: {
                                width: 200,
                            },
                            searchOption: {
                                comType: 'input',
                                placeholder: '请输入商品名称'
                            }
                        },
                        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',
                        }],
                }
            }
        },

schema-viewsearch-panel(搜索)与 table-panel(表格)组成,首先我们就要针对schema配置进行拆分找出我们需要的配置内容,给其下的组件进行使用

JavaScript 复制代码
    //通用构建 schema方法
    // _schema配置选项, comName=>table / search
    const buildDtoSchema = (_schema, comName) => {
        if (!_schema?.properties) { return {} }
        const dtoSchema = {
            type: 'object',
            properties: {}
        }
        // 提取有效 schema 字段量
        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
            }
        }
        return dtoSchema
    }
2.3.1 table-panel

该组件主要完成表格Schema配置页面渲染

  1. 通过tableConfig.headerButtons完成表格上面操作新增商品按钮渲染
  2. 通过tableConfig.rowButtons完成表格每列操作栏上完成修改,删除按钮,并针对按钮的事件eventKey进行相关的配置eventOption完成业务操作
  3. 使用schema-table 组件,完成业务接口请求,并通过schema.properties中每一项key完成表格渲染
2.3.2 search-panel

该组件主要完成表单搜索栏,进行对表格数据的筛选搜索功能

  1. 通过schema.properties中每一项的searchOption.comType完成对应的动态表单加载展示,
  2. 并使用tableOption 进行属性透传,使用UI组件已有的功能
  3. 以及针对不同表单类型提供自定义选项或者api请求获取选项数据
JavaScript 复制代码
//不同表单组件的统一导出
import input from "./complex-view/input/input.vue";
import select from "./complex-view/select/select.vue";
import dynamicSelect from "./complex-view/dynamic-select/dynamic-select.vue";
import dateRange from "./complex-view/date-range/date-range.vue";

const SearchItemConfig = {
    input: {
        component: input,
    },
    select: {
        component: select,
    },
    dynamicSelect: {
        component: dynamicSelect,
    },
    dateRange: {
        component: dateRange,
    }
}
export default SearchItemConfig


//表单组件的引用
import SearchItemConfig from './search-item-config.js'

<!-- 动态组件 -->
<el-form-item v-for="(schemaItem, key) in schema.properties" :key="key" :label="schemaItem.label">
    <!-- 子控件展示 -->
    <component :ref="handleSearchComList" :is="SearchItemConfig[schemaItem.option?.comType].component"
    :key="key" :schemaKey="key" :schema="schemaItem" @loaded="handleLoaded" />
</el-form-item>

3.总结

  1. 学习了JsonShema配置化形成领域模型架构思路
  2. 学会提炼关键业务,从以前的开发代码的编写转变为业务需求的配置定义
相关推荐
哆啦A梦15885 小时前
商城后台管理系统 01 Vue-i18n国际化
前端·javascript·vue.js
期待のcode5 小时前
Vue的安装创建与运行
前端·javascript·vue.js
+VX:Fegn08955 小时前
计算机毕业设计|基于springboot + vue旅游信息推荐系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计·旅游
百锦再5 小时前
国产数据库的平替亮点——关系型数据库架构适配
android·java·前端·数据库·sql·算法·数据库架构
旺仔Sec5 小时前
2025年海南省职业院校技能大赛“应用软件系统开发“赛项竞赛样题
前端·应用软件系统开发
码界奇点5 小时前
基于SpringBoot和Vue的Fuint门店会员营销系统设计与实现
vue.js·spring boot·后端·毕业设计·springboot·源代码管理
FakeOccupational6 小时前
【树莓派 002】 RP2040 实现示波器 PIO来驱动 ADC10080 并抓取数据方案+ 内置12-bitADC&DMA&网页前端可视化方案
前端
DJ斯特拉6 小时前
Vue工程化
前端·javascript·vue.js