1. 背景
在接触DSL之前,前端开发具有以下痛点:
- 重复性的CRUD体力活
- 纯页面开发
- 多套功能相似的系统需要重新搭建,交付时间长
这些会导致前端开发人员一直在搬砖,自己没有成长,从而丧失竞争力。里程碑三的DSL的设计理念能更好地为前端er们摆脱这种困扰,80%的重复性工作通过配置实现,只需聚焦20%的工作开发,这样可以节约重复性的搬砖时间,在完成20%开发的同时提升自我。
2. DSL设计思想
2.1 What is DSL?
DSL(Domain Specific Language)是用来描述某个领域内通用能力的语言,可以简单地理解为遵循一份配置,根据配置展现不同的页面,从而减少前端页面重复开发的体力活。
在生活中,领域模型广泛存在。比如把电商理解成一个领域,在电商这个领域内有多个项目:淘宝、拼多多、京东等,这些项目都会具有一些通用的功能,如查看商品、下单、支付功能,而每个项目又会有每个项目的特点:淘宝有闪购,京东有自营,拼多多有砍一刀。
如果有三个客户分别找你开发淘宝、拼多多、京东系统,他们希望这些系统具有上述通用能力,但又有各自特色,你会怎么办,难道真的只能一一开发了吗?能不能设计一种通用的结构,这些系统都能复用?有的,DSL。
领域模型就是这些项目共有的特点,而DSL就是描述这些项目共同特点的语言。如果我规定DSL结构为:
{
dashboard: '电商',
schema: [{key: '商品', xxx}, {key: '下单', xxx}]
}
不同的电商系统的DSL是基于模版基础上进行改动,我们只需负责开发通用的商品,下单功能,并规定一份DSL配置,日后要用的时候直接遵循DSL规范,从而生成对应的功能,是不是很好地解决了从零开发三个系统的问题?
2.2 DSL结构
DSL结构规定大致如下:
{
mode: 'dashboard', // 模板类型,不同模板类型对应不一样的模板数据结构
name: '', // 名称
desc: '', // 描述
icon: '', // icon
homePage: '', // 首页(项目配置)
// 头部菜单
menu: [{
key: '', // 菜案唯一描述
name: '', // 菜单名称
menuType: '', // 菜单枚举值: group / module
// 当 menuType == module时,可填
moduleType: '', // 模块枚举值:sider/iframe/custom/schema
// 当moduleType == iframe时,可填
iframeConfig: {
path: '', // iframe路径
},
// 当moduleType == custom时,可填
customConfig: {
path: '', // 自定义路由路径
},
// 当moduleType == schema时,可填
schemaConfig: {
api: '', // 数据源API (遵循 RESTFUL 规范)因为是全栈框架,api都是自己定制的,所以一个api可以表示增删改查
schema: { // 板块数据结构
type: 'object',
properties: {
key: {
...schema, // 标准 schema 配置
type: // 字段类型
label: '', // 字段的中文名
// 字段在 table 中的相关配置
tableOption,
// 字段在 search-bar 中的相关配置
searchOption,
// 后续若要新增功能,先在DSL里定义好
},
...
},
},
// table 相关配置
tableConfig: {},
searchConfig: {}, // search-bar 相关配置
components: {} // 模块组件
},
}, ...],
}
一份DSL代表一个领域模型,这里面从定义菜单开始,逐步延伸到每个模块。后续如果有新的重复性需求,直接新增DSL和schema配置即可
- 菜单:支持子菜单配置
- 模块:支持 sider/iframe/custom/schema。
- sider表示支持配置侧边菜单
- iframe表示支持嵌入其他页面
- custom表示自定义页面配置
- schema表示模版页面配置,这里简化80%的工作量
3. DSL实现
3.1 DSL配置和解析
为了简化用户配置过程,这里采用了“约定大于配置”的思想,规定在elpis/model目录下配置DSL,目录结构大致如下:
model/
│
├── course/
│ ├── model.js
│ └── project/
│ ├── bilibili.js
│ └── douyin.js
├── business/
│ ├── model.js
│ └── project/
│ ├── pdd.js
│ ├── jd.js
│ └── taobao.js
...
上述代码给了一个例子,表示有电商和教育两个领域模型,两个领域模型下分别配置了model,以及不同项目下的个性化配置。如电商领域模型里有一份model公共配置,每个项目又有自己的配置
有了规范的配置后,要做的就是设计一个DSL解析引擎来读取model和project的配置,并合并,这里的合并思路是:
- model有,project没有的配置 => 使用model配置(继承)
- model没有,project有 => 使用project配置(拓展)
- model有,project也有 => 使用project配置(重载)
最后返回一份DSL数据结构
何时调用?
通过配置接口来实现,在之前实现elpis的service层去调用这个方法,最后从接口返回DSL结构
3.2 页面设计
页面目录结构如下:
pages/
│
├── project-list/
│ ├── entry.project-list.js
│ └── project-list.vue
├── dashboard/
│ ├── complex-view
│ ├── header-view
│ ├── iframe-view
│ ├── sider-view
│ └── schema-view
- project-list - elpis应用主入口,里面展示了所有配置了DSL的项目列表
- dashboard - 模板页,根据不同项目的DSL渲染出不同的页面内容
这里同样采用约定大于配置的思想,规定入口文件的命名为"entry.xxx.js",然后webpack打包时就会自动对这类文件进行打包并生成tpl模板文件,最后在node里通过ctx.render来渲染出指定的页面,实现SSR,每个应用同时也是个CSR
通过接口读取DSL配置,并存储在pinia中,并根据配置来渲染出不同的view(sider-view/iframe-view/schema-view)
3.3 schema-view 设计思路
schema-view包括table-panel和search-panel两个组件,这两个组件又分别引用了schema-table和schema-search-bar。
为什么要单独区分?
公共组件不负责处理业务,只渲染视图,网络请求什么的通过table-panel处理
在项目里关于schemaConfig的DSL配置包括:
- api:这里遵循Restful规范,一个api的增删改查通过不同的方法来处理:GET, POST, PUT, DELETE
- schema:字段描述,每个字段可以配置searchOption和tableOption
- tableConfig:table配置,包括按钮
- searchConfig:searchBar配置,包括Input, 日期组件,下拉框,动态搜索框等常见搜索项
- components:模块组件
schema字段里的tableOption和searchOption默认配置都遵循element-plus的标准配置,并在其基础上拓展,如tableOption可以支持visible、toFixed方法
schema-table, schema-search-bar在公共组件widgets中实现,这里不做业务的处理,如发请求,只负责处理数据,变成可渲染的, dto => vo
公共组件widgets里还有其他组件如header-container, sider-container,他们内部会提供插槽给调用方使用。比如header-container会暴露菜单区域
后续若有其他重复代码可以新增schema-view来拓展
这里vue-router使用history模式而不是hash模式,为什么?
在同一个model的不同project中,要根据不同项目可能会共享相同的api,为了做区分,会把proj_key带在query参数里。 问题变成 => 如何获取proj_key?
- window.location.hash获取参数,或vue-router提取 hash后面的参数,但始终有风险(过度依赖url)
- tpl模板注入的时候带上proj_key参数,就能从 window.projKey上获取
- 在hash模式下,#后面的内容不会传给server,因此改为用history模式,同时server端对页面路由做处理
改为history模式后的获取projKey的流程变成了:
- url请求页面 http://localhost:8080/view/dashboard/todo?key=order&proj_key=pdd
- server通过中间件记录proj_key,挂载到ctx上
- Controller对通过ctx.render返回页面,同时将projKey注入到window上
- 后续schema里的api请求的query会自动带上window.projKey,后端根据不同的projKey进行处理
4. 总结
通过DSL配置领域模型以及schema的开发,实现了80%的重复性页面通过配置即可实现,剩下20%通过custom-view来实现。当时学完这一章的一个疑惑点:如果新增一个功能就要配置DSL和schema,开发时间也会提高,因为有的功能可能是一次性的。这个答案在里程碑四会得到解决。