关于DSL(domain-specific language)设计实践与思考

456 阅读5分钟

DSL设计实践

背景:在管理系统开发中,大量时间被消耗在重复性工作上,如页面表单开发、增删改查功能实现和字段建表等。这些机械性任务不仅降低了开发效率,还限制了自身的成长和对开发的理解。

目标:通过配置化方案减少重复性工作,例如 dashboard 页面,实现 70%-80% 的开发工作配置化。

思路

由数据驱动视图,采用声明式配置方式。不同的业务领域模型通过DSL配置转换为对应的视图模型,再由模板引擎和解析器处理,最终渲染出完整的 dashboard 站点。这种方式将业务逻辑与展示逻辑分离,使开发者只需关注数据模型和业务规则的定义,而无需重复编写代码。

步骤

根据 dashboard 页面,设计 DSL 模型,通过 DSL 模型派生出不同的模板类型,xxx电商管理系统,xxx课程系统,...

目录结构

├── docs/
│   └── dashboard-module.js         # DSL文档,描述模板结构
├── model/
│   └── buiness/
│       └── model.js                # 模板配置,根据不同模型
│   └── project/
│       └── model.js                # 模板配置,构建具体业务模型
│   └── index.js                    # 重要!解析引擎,用于读取,整理,继承模板和模型
├── app/
│   └── pages/
│       └── dashboard/
│           ├── entry.dashboard.js  # Dashboard入口文件
│           ├── dashboard.vue       # Dashboard主组件
│           └── complex-view/       # 复杂视图组件目录
│               ├── header-view/    # 头部视图组件
│               ├── sider-view/     # 侧边栏视图组件
│               ├── schema-view/    # Schema视图组件
│               │   ├── complex-view/
│               │   │   ├── search-panel/  # 搜索面板组件
│               │   │   └── table-panel/   # 表格面板组件
│               │   └── hook/
│               │       └── schema.js      # Schema处理钩子
│               └── iframe-view/    # Iframe视图组件

页面组成

  • 头部菜单(header-container):菜单,个人信息,退出登录
  • 侧边菜单(sider-container):头部菜单的延伸配置
  • schemaView:主要视图部分
    • schema-view-bar:搜索栏,input,select,date-range,...其他控件
    • schema-table:展示表格,表头信息,表格内容,表格控件(多页,新增,编辑,删除,...其他控件)
  • iframeView:第三方页面
  • customView:提供定制化页面

DSL文档设计

根据 json-schema 文档结构,设计领域模型

领域模型
{
  module: 'dashboard', // 模板类型, 不同模板类型对应不同的模板
  name: '', // 模块名称
  desc: '', // 模块描述
  icon: '', // 模块图标
  homepage: '', // 模块首页
}
菜单设计
// 头部菜单 header-container
{
  menu: [
    {
      key: '', //菜单唯一描述
      name: '', //菜单名称
      menuType: '', // 枚举值 group 分组, module 模块

      // menuType 为 group 时, 配置为子菜单
      subMenu: [
        {
          // 可递归 menuItem 结构和 menu 一样
        }
      ],

      // menuType 为 module 时, 配置为该菜单跳转的类型
      moduleType: '', // 枚举值: schema/custom/iframe/sider

      // moduleType 为 custom 时, 自定义属性
      customConfig: {},

      // moduleType 为 iframe 时, 第三方页面
      iframeConfig: {},

      // moduleType 为 schema 时, 主要功能页面
      schemaConfig: {},

      // moduleType 为 sider 时, 侧边栏菜单配置,菜单结构模式和 menu 一样可递归
      siderConfig: {
        menu: [
          {
            // 可递归 menuItem
          }
        ]
      }
    }
    //...
  ]
}
主要内容 schema-view 数据源 设计
// ...existing code
// schemaConfig 中配置
{
    api: '', // 数据源 (遵循 RESTFUL)
    schema: {
        // 数据源结构
        type: 'object',
        properties: {
        key: {
            ...schema, // 标准的schema字段
            type: '', // 字段类型
            label: '', // 字段中文名
            // 字段在table中的配置
            tableOption: {
            ...elTableColumnConfig, // 标准的 el-table-column 配置
            visiable: true // 是否显示
            },
            // 字段在search-bar中的配置
            searchOption: {
            ...eleComponentConfig, // 标准的 el-component 配置
            comType: '', // 配置组件类型
            default: '', // 默认值

            // 如果 comType 为 select, 需要配置以下属性
            enumList: [
                {
                label: value
                }
            ]
            },
            // 字段在form中的配置
            formOption: {}
        },
        ...other
        }
    },
    // table 相关配置
    tableConfig: {
        // 头部按钮
        headerButtons: [
        {
            label: '', //按钮中文名
            eventKey: '', // 事件名称
            eventOption: {}, // 按钮配置
            ...elButtonConfig // 标准的 el-button 配置
        },
        ...otherButtons
        ], 
        // 数据项按钮
        rowButtons: [
        {
            label: '', // 按钮中文名
            eventKey: '', // 按钮事件名
            eventOption: {
            // 当 eventKey 为 'edit' 时, 需要配置以下属性
            params: {
                // 以下为请求参数
                /**
                 * eg.  request({
                 *  url: 'xxx',
                 *  method: 'edit',
                 *  data: {
                 *    paramKey: rowValueKey
                 *  }
                 * })
                 */
                // paramKey = 参数的键值
                // rowValueKey = 参数值, 格式为 schema::tableKey 到 table 中找到相应的字段
                paramKey: rowValueKey
            }
            }, // 按钮配置
            ...elButtonConfig // 标准的 el-button 配置
        },
        ...otherButtons
        ] // 行按钮
    },
    searchConfig: {}, // search-bar 相关配置
    components: {} //模块组件
},
// ...existing code

根据模型派生出业务模板

举个🌰:

model:课程管理模型

module.exports = {
  module: 'course',
  name: '课程管理',
  menu: [
    {
      key: 'video',
      name: '视频管理',
      menuType: 'module',
      moduleType: 'custom',
      customConfig: {
        path: ''
      }
    },
    {
      key: 'user',
      name: '用户管理',
      menuType: 'module',
      moduleType: 'custom',
      customConfig: {
        path: ''
      }
    }
  ]
}

project:xxx课程管理系统

module.exports = {
  name: 'xxx课堂',
  desc: 'xxx课堂管理系统',
  homePage: '/todo?proj_key=bilibili&key=video',
  menu: [
    {
        key:"video",
        name:"视频管理(xxx)"
    },
    {
        key:"user",
        name:"用户管理(xxx)"
    },
    {
      key: 'course-file',
      name: '课程资料',
      menuType: 'module',
      moduleType: 'sider',
      siderConfig: {
        menu: [
          {
            key: 'pdf',
            name: 'PDF',
            menutType: 'module',
            moduleType: 'custom',
            customConfig: {
              path: '/todo'
            }
          },
          {
            key: 'excel',
            name: 'Excel',
            menutType: 'module',
            moduleType: 'custom',
            customConfig: {
              path: '/todo'
            }
          },
          {
            key: 'ppt',
            name: 'PPT',
            menutType: 'module',
            moduleType: 'custom',
            customConfig: {
              path: '/todo'
            }
          }
        ]
      }
    }
  ]
}

重要:解析模板引擎

通过解析引擎,将上述模型和模板通过新增,重载,继承,整理出一份完整的模板配置。

  • project 有key , model 没有 key => 添加 (新增)
  • project 有key , model 也有 key => 修改 (重载)
  • project 没有key , model 有 key => 合并 (继承)

目标:解析 model 配置, 并返回组织且继承后的数据结构如下

 [{
     model:${model}, // 模型
     project:{
      proj1:${proj1} , // 模板
      proj2:${proj2}
     }
 }]

步骤:

  1. 读取模型和模板文件
  2. 处理每个文件路径问题
  3. 区分配置类型(model/project)
  4. 模板和模型的数据结构整理成上述结构
  5. 整理 project 要继承的 model 配置

重要:解析数据源引擎

根据数据源中的配置延伸出配置解析引擎:

举个🌰:product_id

{
  // 数据源配置
  properties:{
    product_id:{
      type: 'string',
      label: '商品ID',
      // 表格中的配置
      tableOption: {
        width: 300,
      },
      // 搜索配置
      searchOption: {
        comType: 'input' // 可以是 select date-range,dynamic-select,等等组件
      },
      // 组件配置
      comOption: { }
    }
  },
  tableConfig:{ // table 相关配置
    headerButtons: [], // table 头部按钮
    rowButtons: [] // 列表按钮
  },
  searchConfig: {}, // search-bar 相关配置
  comConfig: {}, // 组件相关配置
}
  • searchBar解析器(searchOption + searchConfig): 根据 product_id 生成一个 input 的搜索条件
  • table解析器(tableOption + tableConfig):根据 product_id 生成一列表单名字为 商品ID
  • component解析器(componentOption + componentConfig): 根据 product_id 生成一个组件,弹窗、表单、抽屉,等等

思考

通过DSL设计,我们实现的目标:

  1. 降低开发成本
  2. 提升代码质量
  3. 增加系统的灵活性
  4. 标准化开发流程

这种基于DSL的开发模式是一种思想,可以实用于不同的项目,也可以是不同的工作,目标就是标准化流程,取其精华,去其糟粕。