基于 Elpis下的DSL设计与实现

115 阅读5分钟

DSL 设计理解与总结

前言

在工作中我曾开发过类似的功能,因此在学习哲哥课程时,对其表达的思想能够较为容易地理解。例如,工作中的菜单栏是通过后端配置来渲染的,接口返回的信息包含了图标、路由跳转信息、描述等;弹窗模块中的权限渲染,也是依据后端配置的表结构实现权限联动,这种基于配置的开发模式大幅减少了后续的维护成本,基本实现了“一次开发,终身使用”。

不过在课程的学习过程中,由于对 DSL 结构不够清晰,虽然可以理解其意图,却难以记住各个配置的作用与处理方式。随着学习的深入,我才逐渐理解了这些配置结构的价值和意义。

核心思想:学会偷懒

通过一部分配置,减少重复的工作,把时间放到更有意义的地方。

一、什么是 DSL

在学习哲哥课程的过程中,最初不清楚“DSL”到底是什么,只知道它是一种配置规范。直到学习到里程碑3,才去深入了解。

DSL(Domain Specific Language) ,即领域特定语言,是一种专为特定问题领域设计的编程语言或语言规范。不同于通用编程语言(如 Python、Java),DSL 更专注于特定业务场景,提供更贴合该领域的语法和语义,使得配置和表达更加直观清晰。

在 DSL 配置中,字段结构严谨、类型明确,使人一眼就能明白每个配置的意图及其影响。

类比理解模型与模板

在学习完里程碑3时,我觉得可以将 DSL 中的“模型”理解为装不同类型玩具的盒子:有的盒子用来装汽车,有的用来装玩偶,还有的用来装积木。每个盒子内部会包含某类玩具的通用特征,比如所有汽车都有四个轮子、方向盘。这些通用特征可以作为模型的基础配置,这些“盒子”就相当于系统中的“模型(Model)”。

而对于某些特殊类型的玩具(如消防车、救护车),虽然它们都属于“汽车”模型,但还会有各自的特殊属性(比如消防车上有洒水装置、救护车有急救装置等等)。这时,我们就可以在模型的基础上,通过模板的方式扩展或重载配置,实现灵活定制,对应类型的玩具就相当于模型下的模板。

目录结构如下图所示:

image.png

我们基于不同的模型来延伸出各式各样的模板。

二、DSL 配置示例

以下是一个基于Elpis的 DSL 配置文件示例,用于定义电商系统的菜单与模块结构:

module.exports = {
    model: 'business',
    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
                            },
                            searchOption: {
                                comType: 'select',
                                enumList: [
                                    { label: '全部', value: 'all' },
                                    { label: '1', value: 1 },
                                    { label: '2', value: 2 },
                                    { label: '3', value: 3 }
                                ]
                            }
                        },
                        product_name: {
                            type: 'string',
                            label: '商品名称',
                            tableOption: {
                                width: 200,
                            },
                            searchOption: {
                                comType: 'input',
                                default: '',
                            }
                        },
                        price: {
                            type: 'number',
                            label: '价格',
                            tableOption: {
                                width: 200,
                            },
                            searchOption: {
                                comType: 'dynamicSelect',
                                api: '/api/proj/product_enum/list',
                            }
                        },
                        inventory: {
                            type: 'number',
                            label: '库存',
                            tableOption: {
                                width: 200,
                            }
                        },
                        create_time: {
                            type: 'string',
                            label: '创建时间',
                            tableOption: {
                                width: 400,
                            },
                            searchOption: {
                                comType: 'dateRange',
                                dateType: 'daterange',
                            }
                        }
                    }
                },
                tableConfig: {
                    headerButtons: [
                        { label: '新增商品', eventKey: 'showComponent', type: 'primary', plain: true }
                    ],
                    rowButtons: [
                        { label: '修改信息', eventKey: 'showComponent', type: 'warning' },
                        {
                            label: '删除',
                            eventKey: 'remove',
                            type: 'danger',
                            eventOption: {
                                params: {
                                    product_id: 'schema::product_id',
                                }
                            }
                        }
                    ]
                },
                searchConfig: {}
            }
        },
        {
            key: 'order',
            name: '订单管理',
            menuType: 'module',
            moduleType: 'custom',
            customConfig: {
                path: '/todo'
            }
        },
        {
            key: 'client',
            name: '客户管理',
            menuType: 'module',
            moduleType: 'custom',
            customConfig: {
                path: '/todo'
            }
        }
    ]
}

字段说明

  • menuType:描述菜单类型(模块 module 或 多级菜单 group)
  • moduleType:描述模块关联的模板类型,如 schema标准配置、自定义、有侧边栏、iframe第三方等

这里不做字段的过多说明,主要是思想!!!

三、DSL 在系统设计中的角色

在 Elpis 中,创建“模型”是确定系统分类的第一步。模型就像是各种业务系统的容器(例如:电商系统、人事系统等)。每个模型下可以拓展出多个模板(比如:商品管理、订单管理),模板之间可以共享基础配置,同时支持局部覆盖和个性化拓展。

结构示例:

  • 模型配置(如 model.js):定义基础配置
  • 模板目录:基于模型的配置进行扩展或重载
  • index.js:对结构进一步封装处理,供系统消费

四、Schema 模块的核心逻辑

moduleType 设置为 schema,系统会引入标准表单模板,在 schemaConfig 中读取配置,并封装成组件。

例如:

  • tableConfig:配置操作栏按钮
  • tableOption:配置表格列字段

通过组件的二次封装,这些配置项被透传至底层 UI 组件,从而实现强大的复用性与可扩展性。

此外,由于每个配置名称不同,在组件用调用会使耦合性变高,可以将多个配置项抽离为 option,以提升模块化程度。

五、实践思考

例如在二次封装 date-range 组件时,dateType 是一个关键配置项。这时可以考虑将其纳入 DSL 规范中,增强配置的一致性和标准化。当然,设计 DSL 时也要考虑是否具备通用性,不能胡乱配置。

总结

DSL 的核心价值在于将开发中的共性提取为结构化的配置,进而通过模板机制实现高度复用和快速迭代。学习与实践 DSL,不仅可以优化开发效率,也能增强系统的可维护性和扩展能力。

通过这次学习与总结,我更加理解了 DSL 的设计思想和实际落地方式,在今后的工作中也将更多尝试将其应用于实际业务系统的建设中。