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}
}
}]
步骤:
- 读取模型和模板文件
- 处理每个文件路径问题
- 区分配置类型(model/project)
- 模板和模型的数据结构整理成上述结构
- 整理 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设计,我们实现的目标:
- 降低开发成本
- 提升代码质量
- 增加系统的灵活性
- 标准化开发流程
这种基于DSL的开发模式是一种思想,可以实用于不同的项目,也可以是不同的工作,目标就是标准化流程,取其精华,去其糟粕。