前言
本文是个人学习实践过程中的记录及理解,如有错漏欢迎指出。
相关知识来源于
哲玄前端(抖音ID:44622831736)大前端全栈实践课程
首先,得先了解什么是 DSL。DSL 全称是 Domain-Specific Language,翻译成中文是“领域特定语言”。
简单来说,它是专门为了解决某个特定领域的问题而设计的一种编程语言或规范,比如像我们所熟知的 HTML、CSS,与通用编程语言不通,DSL 牺牲了通用性,换取了在特定场景下极高的表达效率和可读性,只为解决特定问题而生。
解决痛点
既然为了解决特定问题,那么该 DSL 是为了解决什么问题而被设计出来的呢?
相信大家在做业务开发时,经常会遇到的很多 重复性工作,比如:
- 页面样式大差不差,部分组件被多次重复引用
- 接口代码逻辑、数据结构高度相似
- 表结构相似
在遇到以上问题时,如果不能复用代码,就需要频繁的新建一份结构相似的文件,然后为了其中 10% - 20% 不同的内容做着重复性的工作,后续维护还特别麻烦,尤其消耗心神。
这时常用的做法是:把重复部分组件化进行复用,通过一份配置传参动态生成组件使用。业务接口各功能原子化,业务信息需要相关功能处理时再引入调用。这样做确实能一定程度的减少重复性工作,但面临新的相似页面、功能时,还是得复制文件,重新再引入一遍。
有没有办法只通过一份配置便能生成系统,其中差异性只需更改配置项便能轻易解决的呢?
这便是该 DSL 解决的痛点。能够描述整个系统,通过解析器解析生成完整的系统。
DSL 设计
笼统点看,系统其实由多个页面组成,每个页面对应着路由,路由背后引用的是组件,组件中可能嵌套着其他组件。无论如何,这些组件所做的事情,本质是通过请求接口的方式通知后端维护数据。
由此可以看出数据与组件间的关联性,那么可以实现 由页面数据驱动生成页面组件。
可以得出一份这样的数据结构:
const menu = [
{
name: "", // 页面名称
key: "", // 页面 key 值,保持页面唯一性,
path: "", // 页面路由
component: "", // 承载页面的组件
schema: { // 数据结构描述
type: "object",
properties: {
[keyName]: {
type: "", // 数据类型
label: "", // 对应的中文名
searchOption: ..., // 搜索组件中该字段的相关配置
tableOption: ..., // 表格组件中该字段的相关配置
formOption: ..., // 表单组件中该字段的相关配置
...
}
}
}
},
...
]
以后在其他组件中添加该字段相关输入或展示,只需继续添加相关配置即可。
这时候,如果某些组件操作的并非某个字段,而是某条数据,可以添加新的配置完成。
比如:表格中的删除、修改等按钮
const menu = [
{
name: "",
...,
tableConfig: {
rowButtons: [ // 表格行操作按钮
{
eventKey: "", // 按钮触发的事件名
label: "", // 按钮名称
...
}
]
}
}
]
这样通过解析器解析在 tableConfig 添加的新配置,便能为表格添加行按钮,相关事件也能由统一 emit 回调到父组件中借助配置的事件名分发出去。
而这份 DSL 具有很强的 可拓展性,往后表格添加其他组件,仅在 tableConfig 中添加新的配置,配合相关的解析器便能实现。
不仅如此,某个字段背后关联着的接口、数据库,也能通过配置项方式添加:
const menu = [ { ..., schema: { type: "object", properties: { [keyName]: {
...,
apiOptions: {}, // api 配置
dbOptions: {}, // 数据库配置
}
}
},
apiConfig: {} // api 拓展
dbConfig: {} // 数据库拓展
}
]
按照设定好的数据结构,配合解析器解析生成相关联的接口、数据库,这便是该 DSL 最强大的能力。一份配置便能 完成从页面到接口、再到数据库的全部过程。
领域模型设计
而最初的目的是为了通过一份 DSL 直接生成系统,目前为止所展现的是远远不够的,还需要考虑更多自定义的特殊情况。
得做出一些改变:
{
name: "", // 系统名称
desc: "", // 系统描述
homePage: "", // 系统主页
menu: [
{
key: "",
name: "",
...,
// 新增以下内容
menuType: "", // 枚举值 group / module
// menuType === group 实现下拉菜单
subMenu: []
// menuType === module
moduleType: "", // 枚举值:sider/iframe/custom/schema/.... (可拓展)
// 实现侧栏菜单页面
siderConfig: {
menu: []
},
iframeConfig: {}, // iframe 嵌套页面拓展
customConfig: {}, // 用户自定义页面拓展
// 系统解析生成页面
schemaConfig: {
// schema 属性位置改变
schema: {
...
}
},
}
]
}
这样既保留了足够的系统生成能力,也为用户留下了自定义的空间。
需要添加新模块时,用新的 menuType 枚举值配合解析相关 config 即可轻松实现。
同样,为了省略每次编写的时间,我们可以将某类型系统归为一类,将相似部分抽离成 领域模型。新系统通过 继承领域模型 ,合并个性化内容便能得到一份完整的 DSL 文件。这样能大大缩减重复编写的时间。
结语
与以往二次封装组件配合配置项生成的方式不同,其由页面数据依赖驱动组件生成,减少了维护多份数据源时的脑力消耗,更方便数据集中管理,具备更多可拓展性。
当然,我们可以看出当前实现还不能适用于各端页面,更多满足于 B 端系统页面,其意解决 “减少编写管理系统时的重复性工作” 的问题。
但其最大优点在于跳出以往的开发习惯,设计出更优美、拓展性更强的数据结构,提供了新的编程方向。我们可以借助其设计出满足所有系统的新 DSL,在不断的迭代完善的过程中,得到 解决重复性工作 的最佳实现。