基于领域模型的架构建设
前言
前端发展至今,已经相当成熟,但是如何简化开发流程,减少业务上的CRUD工作,一直都是一个热门的话题,目前主流的是采用低代码的方案,即通过封装大量的组件给使用者,使用者再拼接组合成一个页面。低代码平台的初衷是降低开发门槛,但这也意味着它们在处理复杂性和高度定制性方面可能存在局限,并且其在性能和扩展性上也是一大挑战。
方案设计
如果只是为了解决代码复用,我们其实可以,提前封装好一份通用代码,每次新建菜单时将整个文件夹复制粘贴进去:
这种方案确实可以解决代码重复编写的问题,同时也不会像低代码平台那样存在定制化和性能等问题,但是其仍然存在许多重复性的工作,比如建路由,改参数名等。不可否认,提前建立一些文件并规划好文件目录也是一种可以减少代码量的方案。
那么,我们或许可以学习低代码,设计一份 jsonSchema 配置,通过配置生成页面,以后的需求我们就转变为维护这份配置数据,减少代码的编写,我们要做的,仅是编写一个对这份文件的解析器。这种方案,其实就是基于领域模型思想,描述具体的某一个业务领域的抽象和简化的表示,它是针对特定领域里的关键事物以及其关联的表现,它是为了解决特定问题的抽象的模型。
领域建模 - 有趣的张大敏的文章 - 知乎
代码设计
模型设计:
{
mode: 'dashboard', // 模板类型,不同模板类型对应不一样的模板数据结构
name: '', // 名称
desc: '', // 描述
icon: '', // icon
homePage: '', // 首页(项目配置)
// 头部菜单
menu: [{
key: '', // 菜单唯一描述
name: '', // 菜单名称
menuType: '', // 枚举值:group / module
// 当 menuType == group 时,可填
subMenu: [{
// 可递归 menuItem
}, ...],
// 当 menuType == module 时,可填
moduleType: '', // 枚举值:sider/iframe/custom/schema
// 当 moduleType == sider 时
siderConfig: {
menu: [{
// 可递归 menuItem(除 moduleType == sider)
}, ...]
},
// 当 moduleType == iframe 时
iframeConfig: {
path: '', // iframe 路径
},
// 当 moduleType == custom 时
customConfig: {
path: '', // 自定义路由路径
},
// 当 moduleType == schema 时
schemaConfig: {
// RESTFUL 规范:
// GET 'api/user' 查
// POST 'api/user' 增
// PUT 'api/user' 改
// DELETE 'api/user' 删
api: '', // 数据源API(遵循 RESTFUL 规范)
schema: { // 板块数据结构的描述
type: 'object',
properties: {
key: {
...schema, // 标准 schema 配置
type: '', // 字段类型
lable: '', // 字段的中文名
// 字段在 table 中的相关配置
tableOption: {
...elTableColumnConfig, // 标准 el-table-column 配置
toFixed: 0, // 保留小数点后几位
visiable: true, // 默认为 true (false 或 不配置时,标识不在表单中显示)
},
// 字段在 search-bar 中的相关配置
searchOption: {
...elTableColumnConfig, // 标准 el-table-column 配置
comType: '', // 配置组件类型 input/select/...
default: '', // 默认值
}
},
...
}
},
// table 相关配置
tableConfig: {
headerButtons: [{
label: '', // 按钮中文名
eventKey: '', // 按钮事件名
eventOption: {}, // 按钮事件具体配置
...elButtonConfig // 标准 el-button 配置
}, ...],
rowButtons: [{
label: '', // 按钮中文名
eventKey: '', // 按钮事件名
eventOption: {
// 当 eventKey === 'remove'
params: {
// paramKey: 参数的键值,属性名
// rowValueKey: 参数值,属性的规则,固定或者取数据中的哪个属性
// user_id: schema::user_id
paramKey: rowValueKey
}
}, // 按钮事件具体配置
...elButtonConfig // 标准 el-button 配置
}, ...]
},
searchConfig: {}, // search-bar 相关配置
components: {} // 模块组件
}
}, ...]
}
即:
这份数据模型可以大概描述出一个站点应该长什么样,并且我们可以拓展这份配置,描述出各种各样的 schema 页面模板,不仅仅局限于固定布局的搜索栏,表格和表单,同时,我们在 moduleType 属性中,定义了 iframe/custom 枚举值,可以使我们的代码更加灵活。
但是这份数据太长了,并且我们为了能够生成大量站点,我们就需要对它进行解耦,拆分成各个模型:
所以我们还需要一个解析器,把数据解析成我们最终想要的样子:
const _ = require('lodash')
const glob = require('glob');
const path = require('path');
const { sep } = path;
/**
* 解析 model 配置,并返回组织且继承后的数据结构
[{
model: ${model}
project: {
proj1Key: ${proj1},
proj2Key: ${proj2}
}
}, ...]
*/
module.exports = (app) => {
const modelList = [];
// 遍历当前文件夹,构建模型数据结构,挂载到 modelList 上
const modelPath = path.resolve(app.baseDir, `.${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 normalizedFile = path.normalize(file);
const type = normalizedFile.indexOf(`${sep}project${sep}`) > -1 ? 'project' : 'model';
if(type === 'project') {
const modelKey = file.match(/\/model\/(.*?)\/project/)?.[1];
const projKey = file.match(/\/project\/(.*?)\.js/)?.[1];
let modelItem = modelList.find(item => item.model?.key === modelKey);
if(!modelItem) { // 初始化 model 数据结构
modelItem = {}
modelList.push(modelItem)
}
if(!modelItem.project){ // 初始化 project 数据结构
modelItem.project = {}
}
modelItem.project[projKey] = require(path.resolve(file))
modelItem.project[projKey].key = projKey // 注入 projKey
modelItem.project[projKey].modelKey = modelKey // 注入 modelKey
}
if(type === 'model') {
const modelKey = file.match(/\/model\/(.*?)\/model\.js/)?.[1];
let modelItem = modelList.find(item => item.model?.key === modelKey);
if(!modelItem) { // 初始化 model 数据结构
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(model, project[key])
}
})
return modelList;
}
// project 继承 model 方法
const projectExtendModel = (model, project) => {
return _.mergeWith({}, model, project, (modelValue, projValue) => {
// 处理数组合并的特殊情况
if(Array.isArray(modelValue) && Array.isArray(projValue)){
let result = []
// 因为 project 继承 model,所以需要处理修改和新增内容的情况
// project 有的键值,model 也有 => 修改(重载)
// project 有的键值,model 没有 => 新增(拓展)
// model 有的键值,project 没有 => 保留(继承)
// 处理修改和保留
for(let i = 0; i < modelValue.length; ++i){
let modelItem = modelValue[i];
const projItem = projValue.find(projItem => projItem.key === modelItem.key)
// project 有的键值,model 也有,则递归调用 projectExtendModel 方法覆盖修改
result.push(projItem ? projectExtendModel(modelItem, projItem) : modelItem)
}
// 处理新增
for(let i = 0; i < projValue.length; ++i){
const projItem = projValue[i];
const modelItem = modelValue.find(modelItem => modelItem.key === projItem.key);
if(!modelItem){
result.push(projItem);
}
}
return result
}
})
}
最后就是利用这份数据去封装我们的组件,然后生成页面:
这种基于领域模型设计的架构方案使得我们能够更加灵活的解决日常80%的工作内容,把工作时间花在剩余20%的复杂定制化开发中和沉淀我们这一套可复用的方案上。
或许,我们会觉得,随着时间的推移,jsonSchema 的配置可能会变得非常长,变得不好管理或者在解析时会有性能上的问题,我们可以优化一下配置规则,使其更简洁更便于阅读。
以上引用,如有侵权,请联系作者删除。