elpis 一个企业级应用 —— DSL 框架设计理念

189 阅读4分钟

DSL 介绍

DSL 指的是通过针对特定领域,设计出一套更加简洁,且直观的语言语法和功能,通过DSL能够将高重复性的模块进行抽象,并且设计出不同领域中特定的语法,从而减少重复性代码的编写,提高开发的效率。

DSL 设计思路

DSL框架的总体思路

  • 领域模型
    • 通过针对各个领域设计出该领域系统中特有的功能模块,此类模块内的文档描述会被各个项目模块继承。
  • 项目模型
    • 继承领域模型中的内容,然后根据项目的需求设置各个项目中的内容
  • 解析器
    • 解析器将对各个项目的DSL文件进行继承与解析,构造出属于当前项目模型的数据结构
image.png

Elpis 模型DSL规则设计

通过设置一份DSL设计标准,此份标准决定了后续模型的编写标准,程序员只需要配置相应DSL文档与特定的功能模块,既可以实现相应的页面搭建。

image.png

首先是项目的基本配置,此类配置用于描述项目所使用的DSL模板类型,也就是模板标准。并且描述项目的名称、项目描述等基本信息。

image.png

其次是对项目内容的描述,该内容块会以顶部导航栏出现在页面之中,将功能模块类型(menuType)分为两类:

  • ① 更多模块类,此类型表示其下有更多的单一模块或更多层级,具体页面展示可以理解为下拉菜单。
  • ② 单一模块类,此类型为最终模块展示,具体将其分成 sider | iframe | custom | schema 四大类,不同的类别都将呈现不同的内容展示方式。
单一模块类型介绍
siderConfig
        siderConfig: {
            menu : [{
                moduleType: iframe | custom | schema
        }, ...menu]
        },

siderConfig 会在页面中以侧边栏的方式显示,其中侧边栏内的内容会在侧边栏的右侧进行展示。

image.png

iframeConfig
    iframeConfig: {
      path: '' // 相应的 iframe 路径  
    },

iframeConfig 在页面中的展示是将外部的页面的链接进行放置,从使用第三方页面的功能

image.png

schemaConfig
    schemaConfig: {
        api: '', // 数据源 API (遵循 RESTFUL 规范)
        schema: {
            type: 'object', 
            properties: {
                key: {
                    ...schema, // 基础配置
                    type: '', // 字段的类型
                    label: '', // schema 的中文名称

                    // 字段在 table 中的相关配置
                    tableOption: {
                        ...elTableColumnConfig, // 标准 el-table-column 配置
                        toFixed: 0, // 保留小数点后几位
                        visiable: true, // 默认为 true 该属性表示内容在不在表单中展示
                    },

                    // 字段在 search-bar 中的相关配置
                    searchOption: {
                        ...elComponentConfig, // 标准 el-conponent-column 配置
                        comType: '', // 配置控件类型 AS: inpiut | select | button |.....
                        default: '', // 默认值
                    },

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

                    // 当 comType === dynamicSelect
                    api: ''
                },
            },
        },  
    }

schemaConfig 用于配置基本页面内的配置,其中 schema 中的内容为配置不同功能模块(例如按钮、搜索栏)的基本配置。然后会在下方配置相应的模块配置,例如 tableConfig(表格的相关配置),具体里面的配置可以为 headerButtons 和 rowButtons 两个配置。

    tableConfig: { // 关于 table 内容的配置
        headerButtons: [{
            label:'', //按钮中文名
            eventKey: '', // 按钮触发事件的名称
            eventOption: {}, // 按钮具体配置
            ...elButtonConfig // 标准 el-button 配置
        }, ...headerButtons],
        rowButtons: [{
            label:'', //按钮中文名
            eventKey: '', // 按钮触发事件的名称
            eventOption: {
                // 当 eventKey === 'remove'
                params: {
                    // paramKey = 参数的键值
                    // rowValueKey = 参数值格式为 schema::tableKey 时,到 table 中找到相应的字段
                    paramKey: rowValueKey 
                }
            }, // 按钮具体配置
            ...elButtonConfig // 标准 el-button 配置
        }, ...rowButtons]
    },
customConfig
customConfig: {
    path: '' // 相应的 custom(自定义)路径
},

customConfig 用于配置自定义页面的路径。

DSL 解析引擎

// 返回的数据结构
modelList = [{
     model: ${model},
     project: {
         proj1key: ${project1},
         proj2key: ${project2}
}]

引擎实现步骤:

  • 遍历文件,获取文件绝对路径,然后将路径组合为对象
  • 对对象进行遍历,将类型划分为 model | project 两种处理思路
  • 将类型为 project 的数据继承 model 中的结构信息
  • 将处理完的数据集合到一个列表之中返回。
// 实现代码
{
    const modelList = [];
    //  遍历当前的文件夹,构造模型数据结构,然后挂载到 modelList 上
    //  获取文件夹的绝对路径
    const modelPath = path.resolve(app.baseDir, `.${sep}model`);
    //  将路径组合为对象
    const fileList = glob.sync(path.resolve(modelPath, `.${sep}**${sep}**.js`));
    //  进行遍历
    fileList.forEach(file =>{
        // 判断是否为 index ,是则返回
        if(file.indexOf('index.js') > -1){ return };
        let fileName = path.resolve(file)
        // 区分配置类型 (model / project)
        const type =fileName.indexOf(`${sep}project${sep}`) > -1 ? 'project' : 'model';
        
            if(type === 'project'){
            const modelkey = file.match(/\/model\/(.*?)\/project/)?.[1];
            const projkey = file.match(/\/project\/(.*?)\.js/)?.[1];
            let modelItem = modelList.find(item => item.model?.key === modelkey);
            if(!modelItem){ //初始化 model 结构
                modelItem ={};
                modelList.push(modelItem);
            }
            if(!modelItem.project){ // 初始化 project 结构
                modelItem.project = {};
            }
            // 将相应的 key 挂载到 modelItem 上
            modelItem.project[projkey] = require(path.resolve(file));
            modelItem.project[projkey].key = projkey; //注入 projkey
            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);
            }
            // 将相应的 model 挂载到 modelItem 上
            modelItem.model = require(path.resolve(file));
            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
}

DSL的设计,减少了在编写代码时,那些重复性较高的复用代码,通过这套框架,使得系统的构建只需要根据 schema 规则,编写相应的配置,然后增加特殊的功能模块,大大提升了编写的效率。