Skip to content

elpis之领域模型

作为一个前端开发,通常我们都会遇到这么一个场景,当我们在页面的某个模块中开发好一个弹窗之后我们又接到了一个需求,这个需求也需要开发一个弹窗,这个弹窗与之前那个有些一样又有些不一样,于是我们就会通过组件的方式抽取相同的,不同的使用插槽自定义。在这个项目完结之后,当我们又遇到重复的需求时,我们又继续之前的动作,日复一日。那么,该怎么解决呢?之前说过,elpis的出现就是为了解决日常工作中**80%的重复需求,满足20%**的定制化需求。那么,elpis是怎么做的呢?就是通过领域模型。

什么是领域模型

领域模型又称dsl,是一份通用的基础配置,通过领域模型,我们可以以其为基础,派生出很多份配置,以面向对象的角度来谈,领域模型就是一个基类,通过这个基类,我们可以利用继承的思想派生出无数个子类。比如在电商领域,我们可以定义一个基类,然后通过继承便可以派生出诸如淘宝拼多多抖音电商等站点。

文档的定义

在开始写领域模型之前,我们应该搞清楚elpis作为一个通用的全栈中后台管理系统解决方案,应该具备哪些东西:

  • 通用页面配置

  • 一级菜单

  • 下拉菜单

  • 侧边栏菜单

  • 页面

    • iframe渲染的页面
    • 自定义页面
    • 通用页面

了解了需要的东西之后,我们结合json-schema有了以下通用的文档定义

js
export default {
    // 模板类型,不同模板类型对应不一样的模板数据结构
    mode: 'dashboard',
    name: '', // 名称
    desc: '', // 描述
    icon: '', // 图标
    homePage: '', //首页(项目配置)
    // 头部菜单
    menu: [
        {
            key: '', // 菜单唯一描述
            name: '', // 菜单名称
            menuType: '', // 枚举值, group / module
            // 当menuType = group 时, 可填
            subMenu: [
                {
                    // 可以递归menuItem
                }
            ],
            // 当menuType = module 时, 可填
            moduleType: '', // 枚举值 schema/custom/iframe/sider
            // 当moduleType = schema 时, 可填
            schemaConfig: {
                api: '', // 数据源api 遵循RESTful规范
                // 板块数据结构
                schema: {
                    type: 'object',
                    properties: {
                        key: {
                            ...schema, // 标准schema 配置
                            type: '', // 字段类型
                            label: '', // 字段中文名
                            // 字段在table中的相关配置
                            tableOption: {
                                ...elTableColumnConfig, //标准el-table-column 配置
                                visible: true, // 默认为true(false 表示不在表单显示)
                                toFixed: 2 // 保留小数点后几位
                            },
                            // 字段在search-bar中的相关配置
                            searchOption: {
                                ...elComponentConfig, //标准el-component(form 表单组件) 配置
                                comType: '', // 配置控件类型(input/select)
                                default: '', // 默认值
                                // 当comType = select 时, 可填
                                enumList: [],
                                // 当comType = dynamicSelect 时, 可填
                                api: '' // 枚举列表数据源api 遵循RESTful规范
                            }
                        }
                    }
                },
                // table相关配置
                tableConfig: {
                    headerButtons: [
                        {
                            label: '', // 按钮中文名
                            eventKey: '', // 按钮事件名
                            eventOption: {}, // 按钮具体配置
                            ...elButtonConfig // 标准el-button配置
                        }
                    ],
                    rowButtons: [
                        {
                            label: '', // 按钮中文名
                            eventKey: '', // 按钮事件名
                            eventOption: {
                                // 当eventKey = remove 时, 可填
                                params: {
                                    // paramKey = 参数的键值
                                    // rowValueKey = 参数值 ,格式为 schema::xxx时,到table中找响应的字段
                                    paramKey: rowValueKey
                                }
                            }, // 按钮事件具体配置
                            ...elButtonConfig // 标准el-button配置
                        }
                    ]
                },
                searchConfig: {}, // search-bar相关配置
                components: {} // 模块组件相关配置
            },
            // 当moduleType = custom 时, 可填
            customConfig: {
                path: '' // 自定义路径
            },
            // 当moduleType = iframe 时, 可填
            iframeConfig: {
                path: '' // iframe路径
            },
            // 当moduleType = sider 时, 可填
            siderConfig: {
                menu: [
                    {
                        // 可递归 menuItem(除moduleType = sider)
                    }
                ]
            }
        }
    ]
}

除却以上配置以外,我们还能与数据库挂钩,派生出如dbConfig的配置对象,需要什么都可以往上加。

解析配置并实现继承

js
const glob = require('glob')
const path = require('path')
const _ = require('lodash')
const { sep } = path

/**
 * 实现project 继承 model
 * @param {*} model
 * @param {*} project
 */
const projectExtendModel = (model, project) => {
    return _.mergeWith({}, model, project, (modelValue, projectValue) => {
        // 处理数组合并的特殊情况
        if (Array.isArray(modelValue) && Array.isArray(projectValue)) {
            let result = []
            // 因为project 继承 model 所以需要处理修改和新增内容的情况
            // 1. project有的键值, model也有 => 修改
            // 2. project有的键值, model没有 => 新增
            // 3. 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]
                const modelItem = modelValue.find(item => item.key === projectItem.key)
                if (!modelItem) {
                    // model没有的键值 => 新增
                    result.push(projectItem)
                }
            }
            return result
        }
    })
}

/**
 * 解析module配置,并返回组织且继承后的数据结构
 * @param {*} app
 * @returns {Array} 解析后的module配置
 */

module.exports = app => {
    const modelList = []
    // 遍历当前文件夹,构造模型数据结构,挂载到modelList上
    const modulePath = path.resolve(app.baseDir, `.${sep}model`)
    const fileList = glob.sync(path.resolve(modulePath, `.${sep}**${sep}**.js`))
    fileList.forEach(file => {
        // file = path.resolve(file)
        // 过滤掉index.js文件
        if (file.indexOf('index.js') > -1) return
        // 区分配置类型(model/project)
        const type = path.resolve(file).indexOf(`${sep}project${sep}`) > -1 ? 'project' : 'model'
        if (type === 'project') {
            const modelKey = file.match(/\/model\/(.*?)\/project/)?.[1]
            const projectKey = file.match(/\/project\/(.*?)\.js/)?.[1]
            const modelItem = modelList.find(item => item.model?.key === modelKey)
            if (!modelItem) {
                // 初始化model数据结构
                modelItem = {}
                modelList.push(modelItem)
            }
            if (!modelItem.project) {
                // 初始化project数据结构
                modelItem.project = {}
            }
            modelItem.project[projectKey] = require(path.resolve(file))
            modelItem.project[projectKey].key = projectKey // 注入projectKey
            modelItem.project[projectKey].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
        }
    })
    // 整理数据,实现继承
    modelList.forEach(item => {
        const { model, project } = item
        for (const key in project) {
            project[key] = projectExtendModel(model, project[key])
        }
    })
    return modelList
}

总结

一句话总结,数据驱动视图的变化。利用elpis,当我们接到一个新的需求时,如果是重复的,我们只需要生成一份dsl配置即可。

最近更新