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. 学会提炼关键业务,从以前的开发代码的编写转变为业务需求的配置定义
相关推荐
涡能增压发动积18 小时前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
Wenweno0o18 小时前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
于慨18 小时前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz18 小时前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
swg32132118 小时前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
从前慢丶18 小时前
前端交互规范(Web 端)
前端
tyung18 小时前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald18 小时前
SpringBoot - 自动配置原理
java·spring boot·后端
CHU72903518 小时前
便捷约玩,沉浸推理:线上剧本杀APP功能版块设计详解
前端·小程序
GISer_Jing19 小时前
Page-agent MCP结构
前端·人工智能