# elpis--里程碑3-领域模型DSL

35 阅读6分钟

思考:程序员如何把更多机械式的重复的工作进行一个优化,为自己赋能,减少这些没能力提升的体力活,从而把更多的精力,放在能提升自我能力上的工作,为自我提高竞争力。

里程碑3-给出了一个很好的解决方式,通过DSL(Domain Specific Language的缩写,即领域特定语言)

--什么是DSL?

DSL是一种专注于某一特定领域的语言,它能够以简洁、精确的方式表达领域内的概念,避免了通用语言的冗余和复杂性。

--DSL之于elpis

通过声明式配置来替代命令式编程,从而简化开发时间

  • 通过把逻辑抽象出来,以模板类型的形式,构建模板数据结构。

  • 通过继承的方式,以一份基础模板(基类),可以轻易的实现多份项目配置,并根据用户需要,对不同的项目配置进行个性化定制。

  • 通过DSL的配置,把页面的UI结构描述出来。通过引擎的解析渲染,实现页面,减少重新的写页面的时间。

DSL的设计

image.png

我们可以通过对这个dsl-dashboard约定相关的配置,来生成对应的页面

DSL-dashboard的配置

{
    mode: 'dashboard', //模板类型,不同模板类型对应不一样的模板数据结构
    name: '', //名称
    desc: '', //描述
    icon: '', //icon
    homePage: ``, //首页(项目配置)
    // 头部菜单
    menu: [{
            key: '', //菜单唯一描述
            name: '', //菜单名称
            menuType: '', //枚举值 group / module

            // 当menuType == group 时,可填 
            subMenu: [{
                //可递归 menuItem
            }, ...],


            // 当menuType == module 时,可填 
            moduleType: '', //枚举值:sider/iframe/custom/schema

            // 当moduleType == sider 时
            siderConfig: {
                menu: [{
                    //可递归 menuItem(除 moduleType == sider) 
                }, ...]
            },

            // 当moduleType == iframe 时
            iframeConfig: {
                path: '', // iframe 路径
            },

            // 当moduleType == custom 时
            customConfig: {
                path: '',//自定义路由路径
            },

            // 当moduleType == schema 时
            schemaConfig: {
                api: '', //数据源API(遵循 RESTFUL 规范)
                schema: { //板块数据结构
                    type: 'object',
                    properties: {
                        key: {
                            ...schema, //标准schema 配置
                            type: '', //字段类型
                            label: '' // 字段的中文名
                        },
                        // 字段在table 中的相关配置
                        tableOption: {
                            ...elTableColumnConfig, // 标准 el-table-column 配置
                            toFixed: 0,//保留小数点后几位
                            visiable: true,//默认为true (false 或不配置时,标识不在表单中显示)
                        }
                        // 字段在 search-bar 中的相关配置
                        searchOption:{
                            ...eleComponentConfig,//标准 el-component-column 配置
                            comType:'', //配置组件类型 input/select/datePicker/timePicker/timeRangePicker/dateRangePicker

                            default:'',//默认值
                            //comType == select 时
                            enumList:[], //下拉框可选项
                             //comType == dynamicSelect 时
                            api:'', //下拉框可选项
                        }
                    },
                    ...
                },
            },
            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 配置
                }, ...],
            }, //table 相关配置
                searchConfig: {}, //search-bar 相关配置
                components: {}, //模块组件
            }],
};

根据这个作为抽象出来的dashboard模板--我们就可以由此约定,形成各系列项目模板,并通过继承的方式,衍生出该系列下的不同的项目

image.png

通过配置合并的方法,我们就能基于一个model,无需进行冗余的,重复的写重复的代码,来实现配置。

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


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++) {
                const projItem = projValue[i];
                const modelItem = modelValue.find(modelItem => modelItem.key === projItem.key);
                if (!modelItem) {
                    result.push(projItem);
                }
            }
            return result;

        }
    })
}



/**
 * 解析 model 配置,并返回组织且继承后的数据结构
 * [{
 *   model:${model},
 *   project:{
 *      proj1:${proj1},
 *      proj2:${proj2},
 *   }
 * },...]
 */
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 => {
        // 通过path.normailze 将glob.sync 返回路径标准化
        const normalizedFile = path.normalize(file);
        if (normalizedFile.indexOf('index.js') > -1) { return; }

        // 区分配置类型 (model / project)
        const type = normalizedFile.indexOf(`${sep}project${sep}`) > -1 ? 'project' : 'model';

        if (type === 'project') {
            // win 正则
            const modelKeyRegexWin = /\\model\\(.*?)\\project/;
            const projKeyRegexWin = /\\project\\(.*?)\.js/;

            // unix系统正则 如 max、linux
            const modelKeyUniSeries = /\/model\/(.*?)\/project/;
            const projKeyUniSeries = /\/model\/(.*?)\/project/;

            const moodelKetRegex = process.platform === 'win32' ? modelKeyRegexWin : modelKeyUniSeries;
            const projKeyRegex = process.platform === 'win32' ? projKeyRegexWin : projKeyUniSeries;

            const modelKey = normalizedFile.match(moodelKetRegex)?.[1];
            const projKey = normalizedFile.match(projKeyRegex)?.[1];

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

        }

        if (type === 'model') {
            // win 正则
            const modelKeyRegexWin = /\\model\\(.*?)\\model\.js/;
            // unix系统正则 如 max、linux
            const modelKeyUniSeries = /\/model\/(.*?)\/model\.js/;

            const modelKetRegex = process.platform === 'win32' ? modelKeyRegexWin : modelKeyUniSeries;

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


    });


    // 数据进一步整理:project => 继承 model 
    modelList.forEach(item => {
        const { model, project } = item;
        for (const key in project) {
            project[key] = projectExtendModel(model, project[key]);
        }
    });


    return modelList;
}

如:我们定义了一个电商系统的模板

module.exports = {
    model: 'dasgboard',
    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: 39.9
                                },
                                {
                                    label: '199',
                                    value: 199
                                },
                                {
                                    label: '899',
                                    value: 899
                                }
                                ]
                            }
                        },
                        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',
                            plain: true
                        },
                        {
                            label: '删除',
                            eventKey: 'remove',
                            eventOption: {
                                params: {
                                    product_id: 'schema::product_id'
                                }
                            },
                            type: 'danger'
                        }
                    ]
                }

            }
        },
        {
            key: 'order',
            name: '订单管理',
            menuType: 'module',
            moduleType: 'custom',
            customConfig: {
                path: '/todo',
            }
        },
        {
            key: 'client',
            name: '客户管理',
            menuType: 'module',
            moduleType: 'custom',
            customConfig: {
                path: '/todo',
            }
        }
    ]
}

通过这个电商系统的配置,并且通过合并配置的方法,我们就能以一种类继承的方式,来实现我们的页面,无需重复的写一样的代码,重复公用的代码都会被抽离到model里面。

module.exports = {
    name: '拼多多',
    desc: '拼多多电商系统',
    homePage: '/schema?proj_key=pdd&key=product',
    menu: [{
        key: 'product',
        name: '商品管理(拼多多)'
    },
    {
        key: 'client',
        name: '客户管理(拼多多)',
        moduleType: 'schema',
        schemaConfig: {
            api: '/api/client',
            schema: {}
        }
    },
    {
        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: 'http://www.baidu.com',
                }
            },
            {
                key: 'categories',
                name: '分类数据',
                menuType: 'group',
                subMenu: [{
                    key: 'category-1',
                    name: '一级分类',
                    menuType: 'module',
                    moduleType: 'custom',
                    customConfig: {
                        path: '/todo'
                    }

                },
                {
                    key: 'category-2',
                    name: '二级分类',
                    menuType: 'module',
                    moduleType: 'iframe',
                    iframeConfig: {
                        path: 'http://www.baidu.com',
                    }

                },
                {
                    key: 'tags',
                    name: '标签',
                    menuType: 'module',
                    moduleType: 'schema',
                    schemaConfig: {
                        api: '/api/client',
                        schema: {}
                    }

                }
                ]
            }
            ]
        }
    },
    {
        key: 'search',
        name: '信息查询',
        menuType: 'module',
        moduleType: 'iframe',
        iframeConfig: {
            path: 'http://www.baidu.com',
        }
    },
    ]
}

实现出来的页面是这样的

image.png

总结:我觉得DSL是以一种数据源,通过一种约定好的配置,来定义页面相关的ui结构,如table,searchbar,components。还有api和数据库db相关的内容。通过一系列的解析器,生成完整的业务页面。从而提高我们的开发效率,解放我们程序员做重复无用的体力活,进而能更好的提升自己。

出处:《哲玄课堂-大前端全栈实践》