前端-CRUD战神? 工程化思想?
一、架构工程化思想
- 对于一个后台管理系统来说,大部分是crud一小部分是定制化开发
- 那么我们将大部分的curd开发抽象化为一份JSON配置通过解析器,再用我们封装好的组件渲染出我们想要的效果就不用再重复的去干那些体力活。
- 像我一样的新手总会去想vue好还是react好,webpack好还是vite好,koa好还是express好
- 这个elpis是使用vue3 + webpack5 + element-plus + koa(解析器) + express(实现热更新)
- 学完elpis争辩那些发现是没有意义的只要我们有工程化思想其实 使用vue还是react,webpack还是vite、koa还是express其实都一样,他们只是工具,只要我们会一个另一个就大差不差看看文档就能上手使用这才是核心。
- 比如webpack 热更新配置 vite热更新配置 都只是为了实现热更新而已。
- DSL(领域模型):假如有多个购物系统且模块大同小异,那么可以配置一份基类,各个项目再自己单独配置,再通过解析器去覆盖、新增得到各自系统的JSON配置,然后对于这份JSON配置落地为真实项目。
- 先设计一个糙版本配置
export default {
mode: 'dashboard',
name: '',
desc: '',
icon: '',
homePage: '',
menu: [{
key: '',
name: '',
menuType: '',
subMenu: [{
},],
moduleType: '',
siderConfig: {
menu: [{
}]
},
iframeConfig: {
path: '',
},
customConfig: {
path: '',
},
schemaConfig: {
api: '',
schema: {
type: 'object',
properties: {
key: {
...schema,
type: '',
label: '',
tableOption: {
...elTableColumnConfig,
toFixed: 0,
visiable: true,
},
searchOption: {
...eleComponentConfig,
comType: '',
default: '',
enumList: [],
api: '',
}
},
}
},
tableConfig: {
headerButtons: [{
label: '',
eventKey: '',
eventOption: {},
...elButtonConfig,
}],
rowButtons: [{
label: '',
eventKey: '',
eventOption: {
params: {
paramKey: rowValueKey,
}
},
...elButtonConfig,
}]
},
searchConfig: {
},
components: {
}
},
},]
}
- 实际模板
module.exports = {
model: '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: 300,
'show-overflow-tooltip': true
}
},
product_name: {
type: 'string',
label: '商品名称',
tableOption: {
width: 200,
'show-overflow-tooltip': true
},
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",
}, {
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'
}
}]
}
- 再根据单独的项目配置
name: '京东',
desc: '京东电商系统',
homePage: '/schema?proj_key=jd&&key=product',
menu: [{
key: 'shop-setting',
name: '店铺设置',
menuType: 'group',
subMenu: [{
key: 'info-setting',
name: '店铺信息',
menuType: 'module',
moduleType: 'custom',
customConfig: {
path: '/todo'
}
}, {
key: 'quality-setting',
name: '店铺资质',
menuType: 'module',
moduleType: 'iframe',
iframeConfig: {
path: 'http://www.baidu.com'
}
}]
}]
}
- 构造不同领域下的不同项目配置
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 => {
// 提取文件名称
let name = path.resolve(file)
if (name.indexOf('index.js') > -1) { return; }
// 区分配置类型(model / project)
const type = name.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(name);
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(name);
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
}
- 再根据 Restful API规范实现一个接口不同请求方式实现不同功能
- schema就是我们解决重复性crud的配置我们去封装一些表格组件 和搜索组件去解析我们这份配置生成页面即可
- 额外的定制化需求可以是 iframe 还是custom去自定义一些组件开发大大减少了体力
- 学习地址--> 抖音搜索 <哲玄前端>