前言
在日常针对于管理后台的开发中,每次新业务需求过来的时候,经常是拿之前写好的页面复制过来进行修改。大部分情况下,两个页面多半有些相似,但是又不太好进行复用,所以基本上都是在做重复的工作。
对于大多数后台管理系统而言,往往是由layout,header,sider,main组成,而main绝大多数都是表格,表格有search-bar、点击事件。既然80%都是重复的,我们可以把这些重复的功能整合起来变成一套dsl配置,通过配置解析引擎转换成页面,同时针对剩下20%的定制需求提供扩展能力,减少重复的工作。
系统设计
什么是DSL
DSL 是 Domain-Specific Language(领域特定语言)的缩写,领域特定语言指的就是专注于某个应用程序领域的计算机语言。
在我们这里指的是 针对某一种领域的重复型工作,我们将重复型的工作完成沉淀,让其可以通过配置来完成。我们只需要专注于不能通过配置完成的工作。并将其完成沉淀。
系统拥有的特性
声明式编程: 对于重复型的工作通过配置的方式来完成开发。减少编码时间,提高工作效率
可继承: 通过一个模板标准。可以配置出多个不同领域的基础模版。然在不同领域的基础模版上可以派生出不同项目来扩充自己的需求。
可扩充性: 对于模版中不支持的类型或者业务可以通过自定义组件或者页面来完成
持续性: 组件的开发可以提升DSL功能,来提高DSL的能力
最终实现: 通过配置实现页面的效果。程序员通过对配置文件的修改来实现页面的UI,页面的交互,API的增删改查。路由的跳转。
DSL的设计与实现
DSL的核心设计思路
DSL的设计核心在于领域模型的抽象。通过将业务逻辑抽象为领域模型,开发者可以通过简单的配置描述复杂的业务逻辑。具体来说,DSL的设计可以分为以下几个步骤:
• 领域模型设计:针对不同的业务领域,设计出该领域特有的功能模块。例如,电商领域的商品管理、订单管理等。
• 项目模型设计:继承领域模型中的内容,根据具体项目的需求进行扩展和定制。
• 解析器设计:通过解析器将DSL配置解析为具体的业务逻辑,生成页面、API接口等。
根据DSL生成站点的流程
DSL配置示例
以下为一个dsl配置示例
{
module: 'dashboard',
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: 200,
visible: true
}
}
}
},
tableConfig: {
headerButtons: [
{
label: '新增商品',
eventKey: 'add',
type: 'primary'
}
],
rowButtons: [
{
label: '编辑',
eventKey: 'edit',
type: 'warning'
},
{
label: '删除',
eventKey: 'delete',
type: 'danger'
}
]
}
}
}
]
}
根据上述DSL配置,编写对应的领域模型配置,这里以电商系统为例,领域模型如下所示
module.exports = {
model: 'dashboard',
name: '电商系统',
menu: [
{
key: 'product',
name: '商品管理',
menuType: 'module',
moduleType: 'schema',
schemaConfig: {
api: 'xxxxxx',
schema: {
type: 'object',
properties: {
product_id: {
type: 'string',
label: '商品ID',
tableOption: {
....
}
}
}
}
},
{
key: 'order',
name: '订单管理',
menuType: 'module',
moduleType: 'custom',
customConfig: {
path: '/todo'
}
},
...
]
}
DSL配置的解析与渲染
解析领域模型
上述我们已经得到了dsl模板配置以及根据模板配置所衍生出的领域模型,我们需要根据这份配置通过继承、重载、新增的方式来得到我们的站点所需的项目配置数据。这就需要一个模板解析引擎来得到配置数据,模板引擎代码如下
const glob = require('glob')
const path = require('path')
const { sep } = path
const _ = require('lodash')
/**
* project 继承 model 得到新数据
* @param {*} project
* @param {*} model
*/
function projectExtendModel(project, model) {
// 合并 project ---> model
return _.mergeWith({}, model, project, (modelValue, projectValue) => {
// 处理数组合并的特殊情况
if (Array.isArray(modelValue) && Array.isArray(projectValue)) {
let result = []
// 因为 project 继承 model,所以需要处理修改和新增内容的情况
// project 存在的键值,如果 model 也存在,==> 修改(重载
// project 存在的键值,如果 model 不存在,==> 新增
// model 存在的键值,如果 project 不存在,==> 保留(继承
// 处理修改
for (let i = 0; i < modelValue.length; i++) {
let modelItem = modelValue[i]
const proejctItem = projectValue.find(item => item.key === modelItem.key)
// project 有的键值,model 也有,则递归调用 projectExtendModel 覆盖修改
result.push(proejctItem ? projectExtendModel(proejctItem, modelItem) : modelItem)
}
// 处理新增
for (let i = 0; i < projectValue.length; i++) {
// project 有的键值,model 没有,则新增
if (!modelValue.find(item => item.key === projectValue[i].key)) {
result.push(projectValue[i])
}
}
return result
}
})
}
/**
* 解析 model 配置,并返回组织且继承后的数据结构
* [{
* model:${model},
* project: {
* project1Key: {},
* project2Key: {}
* }
* } ...]
*/
module.exports = app => {
const modelList = []
// 遍历当前文件夹,构造模型数据结构,挂载到 modelList 上
const modelPath = path.resolve(process.cwd(), `.${sep}model`)
const fileList = glob.sync(path.resolve(modelPath, `.${sep}**${sep}**.js`))
fileList.forEach(file => {
if (file.indexOf('index.js') > -1) {
return
}
// 区分配置类型 (model / project)
const type = file.indexOf(`project`) > -1 ? 'project' : 'model'
if (type === 'project') {
const modelKey = file.match(/\/model\/(.*?)\/project/)?.[1]
const projectKey = file.match(/\/project\/(.*?)\.js/)?.[1]
let modelItem = modelList.find(item => item.model?.key === modelKey)
if (!modelItem) {
// 初始化 model 数据结构
modelItem = {}
modelList.push(modelItem)
}
if (!modelItem.project) {
modelItem.project = {}
}
modelItem.project[projectKey] = require(path.resolve(file))
modelItem.project[projectKey].key = projectKey // 注入 projectKey
modelItem.project[projectKey].modelKey = modelKey // 注入 modelKey
}
if (type === 'model') {
// 取的是 file 目录下 model 和 model.js 之间的字符串
const modelKey = file.match(/\/model\/(.*?)\/model\.js/)?.[1]
let modelItem = modelList.find(item => item.model?.key === modelKey)
if (!modelItem) {
modelItem = {}
modelList.push(modelItem)
}
modelItem.model = require(path.resolve(file))
modelItem.model.key = modelKey // 注入 modelKey
}
})
// 数据进一步整理: project ==> 继承 model
modelList.forEach(item => {
const { model, project } = item
for (const key in project) {
project[key] = projectExtendModel(project[key], model)
}
})
return modelList
}
解析配置数据
经过上述流程我们可以得到对应的项目的配置数据,这时候我们就需要书写对应的解析器来转换可渲染数据进而渲染页面,这里以一个通用页面为例,解析流程和所需解析器如图所示