思考:程序员如何把更多机械式的重复的工作进行一个优化,为自己赋能,减少这些没能力提升的体力活,从而把更多的精力,放在能提升自我能力上的工作,为自我提高竞争力。
里程碑3-给出了一个很好的解决方式,通过DSL(Domain Specific Language的缩写,即领域特定语言)
--什么是DSL?
DSL是一种专注于某一特定领域的语言,它能够以简洁、精确的方式表达领域内的概念,避免了通用语言的冗余和复杂性。
--DSL之于elpis
通过声明式配置来替代命令式编程,从而简化开发时间
-
通过把逻辑抽象出来,以模板类型的形式,构建模板数据结构。
-
通过继承的方式,以一份基础模板(基类),可以轻易的实现多份项目配置,并根据用户需要,对不同的项目配置进行个性化定制。
-
通过DSL的配置,把页面的UI结构描述出来。通过引擎的解析渲染,实现页面,减少重新的写页面的时间。
DSL的设计
我们可以通过对这个dsl-dashboard约定相关的配置,来生成对应的页面
DSL-dashboard的配置
{
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: {
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-component-column 配置
comType:'', //配置组件类型 input/select/datePicker/timePicker/timeRangePicker/dateRangePicker
default:'',//默认值
//comType == select 时
enumList:[], //下拉框可选项
//comType == dynamicSelect 时
api:'', //下拉框可选项
}
},
...
},
},
tableConfig: {
headerButtons: [
{
label: '',//按钮中文名
eventKey: '',//按钮事件名
eventOption: {},//按钮具体配置
...elButtonConfig, // 标准 el-button 配置
}, ...
],
rowButtons: [{
label: '',//按钮中文名
eventKey: '',//按钮事件名
eventOption: {
// 当 eventKey == 'remove
params: {
//paramKey = 参数的键值
//rowValueKey = 参数值,格式为 schema::tableKey,到table 中找相应的字段
paramKey: rowValueKey
}
},//按钮具体配置
...elButtonConfig, // 标准 el-button 配置
}, ...],
}, //table 相关配置
searchConfig: {}, //search-bar 相关配置
components: {}, //模块组件
}],
};
根据这个作为抽象出来的dashboard模板--我们就可以由此约定,形成各系列项目模板,并通过继承的方式,衍生出该系列下的不同的项目
通过配置合并的方法,我们就能基于一个model,无需进行冗余的,重复的写重复的代码,来实现配置。
const _ = require('lodash');
const glob = require('glob');
const path = require('path');
const { sep } = path;
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:{
* proj1:${proj1},
* proj2:${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 => {
// 通过path.normailze 将glob.sync 返回路径标准化
const normalizedFile = path.normalize(file);
if (normalizedFile.indexOf('index.js') > -1) { return; }
// 区分配置类型 (model / project)
const type = normalizedFile.indexOf(`${sep}project${sep}`) > -1 ? 'project' : 'model';
if (type === 'project') {
// win 正则
const modelKeyRegexWin = /\\model\\(.*?)\\project/;
const projKeyRegexWin = /\\project\\(.*?)\.js/;
// unix系统正则 如 max、linux
const modelKeyUniSeries = /\/model\/(.*?)\/project/;
const projKeyUniSeries = /\/model\/(.*?)\/project/;
const moodelKetRegex = process.platform === 'win32' ? modelKeyRegexWin : modelKeyUniSeries;
const projKeyRegex = process.platform === 'win32' ? projKeyRegexWin : projKeyUniSeries;
const modelKey = normalizedFile.match(moodelKetRegex)?.[1];
const projKey = normalizedFile.match(projKeyRegex)?.[1];
let modelItem = modelList.find((item) => item.model?.key === modelKey);
// 初始化 model 数据结构
if (!modelItem) {
modelItem = {};
modelList.push(modelItem);
}
// 初始化 project 数据结构
if (!modelItem.project) {
modelItem.project = {};
}
modelItem.project[projKey] = require(path.resolve(normalizedFile));
modelItem.project[projKey].key = projKey; // 注入projKey
modelItem.project[projKey].modelKey = modelKey; // 注入modelKey
}
if (type === 'model') {
// win 正则
const modelKeyRegexWin = /\\model\\(.*?)\\model\.js/;
// unix系统正则 如 max、linux
const modelKeyUniSeries = /\/model\/(.*?)\/model\.js/;
const modelKetRegex = process.platform === 'win32' ? modelKeyRegexWin : modelKeyUniSeries;
const modelKey = normalizedFile.match(modelKetRegex)?.[1];
let modelItem = modelList.find((item) => item.model?.key === modelKey);
if (!modelItem) { //初始化 model 数据结构
modelItem = {};
modelList.push(modelItem);
}
modelItem.model = require(path.resolve(normalizedFile));
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;
}
如:我们定义了一个电商系统的模板
module.exports = {
model: 'dasgboard',
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: 300,
'show-overflow-tooltip': true,
}
},
product_name: {
type: 'string',
label: '商品名称',
tableOption: {
width: 200,
},
searchOption: {
comType: 'dynamicSelect',
api: '/api/proj/product_enum/list'
}
},
price: {
type: 'number',
label: '价格',
tableOption: {
width: 200,
},
searchOption: {
comType: 'select',
enumList: [{
label: '全部',
value: '-999'
},
{
label: '39.9',
value: 39.9
},
{
label: '199',
value: 199
},
{
label: '899',
value: 899
}
]
}
},
inventory: {
type: 'number',
label: '库存',
tableOption: {
width: 200,
},
searchOption: {
comType: 'input'
}
},
create_time: {
type: 'string',
label: '创建时间',
tableOption: {
},
searchOption: {
comType: 'dateRange'
}
}
}
},
tableConfig: {
headerButtons: [
{
label: '新增商品',
eventKey: 'showComponent',
type: 'primary',
plain: true
}
],
rowButtons: [
{
label: '修改',
eventKey: 'showComponent',
type: 'warning',
plain: true
},
{
label: '删除',
eventKey: 'remove',
eventOption: {
params: {
product_id: 'schema::product_id'
}
},
type: 'danger'
}
]
}
}
},
{
key: 'order',
name: '订单管理',
menuType: 'module',
moduleType: 'custom',
customConfig: {
path: '/todo',
}
},
{
key: 'client',
name: '客户管理',
menuType: 'module',
moduleType: 'custom',
customConfig: {
path: '/todo',
}
}
]
}
通过这个电商系统的配置,并且通过合并配置的方法,我们就能以一种类继承的方式,来实现我们的页面,无需重复的写一样的代码,重复公用的代码都会被抽离到model里面。
module.exports = {
name: '拼多多',
desc: '拼多多电商系统',
homePage: '/schema?proj_key=pdd&key=product',
menu: [{
key: 'product',
name: '商品管理(拼多多)'
},
{
key: 'client',
name: '客户管理(拼多多)',
moduleType: 'schema',
schemaConfig: {
api: '/api/client',
schema: {}
}
},
{
key: 'data',
name: '数据分析',
menuType: 'module',
moduleType: 'sider',
siderConfig: {
menu: [{
key: 'analysis',
name: '电商罗盘',
menuType: 'module',
moduleType: 'custom',
customConfig: {
path: '/todo',
}
},
{
key: 'sider-search',
name: '信息查询',
menuType: 'module',
moduleType: 'iframe',
iframeConfig: {
path: 'http://www.baidu.com',
}
},
{
key: 'categories',
name: '分类数据',
menuType: 'group',
subMenu: [{
key: 'category-1',
name: '一级分类',
menuType: 'module',
moduleType: 'custom',
customConfig: {
path: '/todo'
}
},
{
key: 'category-2',
name: '二级分类',
menuType: 'module',
moduleType: 'iframe',
iframeConfig: {
path: 'http://www.baidu.com',
}
},
{
key: 'tags',
name: '标签',
menuType: 'module',
moduleType: 'schema',
schemaConfig: {
api: '/api/client',
schema: {}
}
}
]
}
]
}
},
{
key: 'search',
name: '信息查询',
menuType: 'module',
moduleType: 'iframe',
iframeConfig: {
path: 'http://www.baidu.com',
}
},
]
}
实现出来的页面是这样的
总结:我觉得DSL是以一种数据源,通过一种约定好的配置,来定义页面相关的ui结构,如table,searchbar,components。还有api和数据库db相关的内容。通过一系列的解析器,生成完整的业务页面。从而提高我们的开发效率,解放我们程序员做重复无用的体力活,进而能更好的提升自己。
出处:《哲玄课堂-大前端全栈实践》