概述
作为一名前端开发,我们每天都会和页面打交道。为了产品页面交互和风格的一致性,产品经理会在各个业务功能、页面设计(原型图)之间保持统一风格(理想状态下),但往往不同的业务方案会给我们不同的页面输出,尽管产品经理已经进了最大努力去磨平差异,但页面间还是会有一小部分的不同,这是业务导致的。那么这个问题应该怎么解决呢?大部分同学通常会把相似的页面复制一份,根据当前的业务改一改就OK,随着业务不断壮大,我们的代码也会越来越臃肿,那么恭喜你,屎山已经堆砌好了。那有没有比较好的系统解决方案呢?这里不得不提到Elpis,Elpis为此而生!
什么是Elpis
Elpis是前后端分离的领域模型框架,通过DSL动态生成不同的页面,来满足我们日常差异化开发,这里主要来介绍DSL。
DSL标准配置
module.exports = {
mode: 'dashboard', // 模版类型,不同模版类型对应不一样的模版数据结构
name: '', // 名称
desc: '', // 描述
icon: '', // 图标
homePage: '', // 项目首页( )
// 头部菜单
menu: [{
key: '', // 菜单唯一描述
name: '', // 菜单名称
menuType: '', // 枚举值,group / module
// 当 menuType === group 时,可填
sunMenu: [{
// 可递归 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: {
api: '', // 数据源 API (遵循 RESTFUL 规范)
schema: { // 板块数据结构
type: 'object',
properties: {
key: {
...schema, // 标准 schema 配置
type: '', // 字段类型
label: '', // 字段的中文名
// 字段在 table 中的相关配置
tableOption: {
...elTableColumnConfig, // 标准 el-table-column 配置
toFixed: 0, // 保留小数点后几位
visiable: true, // 默认为 true(false时,表示不在表单中显示)
},
// 字段在 search-bar 中的相关配置
searchOption: {
...eleComponentConfig, // 标准 el-search-bar 配置
comType: '', // 配置组件类型 input/select/...
default: '', // 默认值
// comType === 'select'
enumList: [], // 下拉框选项列表
// comType === 'dynamicSelect'
api: '', // 动态下拉框数据源 API
},
}
}
},
tableConfig: {
headerButtons: [{
label: '', // 按钮中文名
eventKey: '', // 按钮事件名
option: {}, // 按钮具体配置
...elButtonConfig // 标准 el-button 配置
}],
rowButtons: [{
label: '', // 按钮中文名
eventKey: '', // 按钮事件名
eventOption: {
// 当 eventKey === 'remove'
params: {
// paramKey = 参数的键值
// rowValueKey = 参数值,格式为 schema::tableKey,到 table 中找相应的字段
paramKey: rowValueKey
}
}, // 按钮具体配置
...elButtonConfig // 标准 el-button 配置
}],
}, // table 相关配置
searchConfig: {}, // search-bar 相关配置
component: {}, // 模块组件
}
}]
}
一份DSL配置就是一个系统描述文件,这里我们以dashboard为例
homePage 设置我们系统首页
menu 头部菜单集合
menuType 指定菜单类型(group/module),当菜单为下拉多个菜单时,设置为group
sunMenu 当menuType为group时,配置子菜单,配置项和menu一致(共用一份配置)
moduleType 当menuType为module时配置,共有四个配置项(sider/iframe/custom/schema)
siderConfig 有左侧菜单栏时配置,可配置menu,注意左侧菜单栏配置项中除去 moduleType == sider
iframeConfig 引入第三方页面资源配置
customConfigConfig 自定义页面,当有个别特殊页面无法配置时,这时候就需要自己去实现页面。
schemaConfig 标准配置页面配置项
api 这里设置获取业务数据API,需要遵循 RESTFUL 规范
schema 定义板块数据结构,是标准 schema 配置,里面可配置 tableOption(表格),searchOption(表单搜索)
tableConfig 配置headerButtons(表格上方按钮),rowButtons(表格行按钮)
searchConfig search-bar 相关配置
component 模块组件
在项目中文件结构如上图所示 model.js 里面为我们项目的标准页面配置(可理解为面相对象的基类) project 中的每一个js都是一个项目的配置 那项目中是怎么继承基类的配置的呢?
const _ = require('lodash')
const glob = require('glob')
const path = require('path')
const {sep} = path
// 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
}
})
}
/**
* 解析 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 type = file.indexOf(`/project/`) > -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 // 注入 projectKey
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
}
最为核心的 mergeWith 方法
有了这一份DSL配置,页面模版就可以通过DSL来生成具体的页面
想要更系统的学习Elpis,那么 抖音搜索 <哲玄前端>