背景
因为日常开发的项目中有重复性的工作(例如多个页面都存在表格的CRUD),项目与项目之间也是如此(不同后台管理系统的结构高度近似,只是一些模块会有定制化需求)。这就需要一个模板可以根据配置来生成重复部分的代码,减轻开发压力,提高效率,将80%的部分沉淀下来,剩下20%提供客制化需求。这样就能将后台开发时间大大降低,使得开发者专注于定制化需求部分、或者花很少的时间成本完成系统的快速搭建。
鉴于现有的体系,可以在模板页和模板配置之间加层解析器,就好似elpis-core的作用,可以称其为模板解析器,一份模版(基类)配置对应一份模版页和模版解析引擎,在模版(基类)的基础上可以扩展不同的项目(子类)配置
启动文件
在之前的环节中,已经搭建好了基于KOA的后端node服务,通过prod和dev文件启动生产或开发环境的服务(详情文件见上篇文章的《开发环境打包的启动文件配置》)。这里我们先看看支撑DSL模板的启动文件boot。
在该文件中会引入前端项目常用的组件库,状态管理工具等。文件会最终返回一个方法,接收参数依次为要启动的vue文件,页面所需路由及第三方包,主要参数还是前两个
import {createApp} from "vue";
import ElementUI from 'element-plus';
import 'element-plus/theme-chalk/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import './asserts/custom.css'
import pinia from "$store"
import {createRouter,createWebHashHistory} from "vue-router";
/**
* vue页面的主入口,用于启动vue
* @param pageComponent vue入口组件
* @param routes 路由列表
* @param libs 该页面引入的第三方库
*
*/
export default (pageComponent, {routes,libs} = {}) => {
const app = createApp(pageComponent)
app.use(ElementUI)
//引入pinia
app.use(pinia)
// 引入第三方包
if(libs && libs.length){
for(let i = 0; i < libs.length; ++i){
app.use(libs[i])
}
}
//引入页面路由
if(routes && routes.length){
const router = createRouter({
history: createWebHashHistory(),
routes
})
app.use(router)
router.isReady().then(() => {
app.mount("#root")
})
} else {
app.mount("#root")
}
}
模板层级架构
目录结构如下,其中解释了各个层级文件的作用
model
├── business //电商类模板
| ├── model.js //基类模板
| └── project //存放项目模板文件,文件会继承基类模板中的配置
| ├── jd.js
| ├── pdd.js
| └── taobao.js
├── course //课程类模板
| ├── model.js
| └── project
| ├── bilibili.js
| └── douyin.js
└── index.js //模板解析引擎
1.基类模板
该模板就是那80%的沉淀部分了,或者说是同类项目中类似部分的集合。这是什么意思呢?以电商类后台为例,大多数后台都拥有用户管理、商品管理、订单管理。而这就是基类模板所要抽象的内容,以下是模板的规范和数据结构,以及每项配置的作用
{
mode:'dashboard', //模板类型,不同类型对应不一样的模板数据结构
name:"",//模版名称
dec:'',//描述
homePage:''//首页
//头部菜单(header-content)
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:'',//数据源 controller(遵循restful规范)
schema:{ //板块的数据结构
type:"object",
properties:{
key:{
...schema, //标准的schema配置
type:'', //字段类型
label:'', //字段中文名
},...
}
},
//可以通过不同的Config来生成不同组件,实现schema-content部分的内容高度定制化
tableConfig:{ //table 的配置
type:'',
properties:{
//这里配置表格中的表头字段
//同时可以针对每个属性设置对应的搜索字段,实现该字段对应的搜索功能的实现,例如:
age:{
type:'number',
label:'年龄',
searchable:true
}
}
},
searchConfig:{}, //搜索栏的相关配置
components:{},//模块组件
},
},...]
}
以上的模板最终渲染示意图如下,绿色部分即为自定义部分,当然每个content中也可以通过改变模板属性来动态的更改:
schema-content中的内容(以tableConfig为例)
模版的结构如下图所示:
2.项目模板
该类模板就是在基类模板的基础上实现各个同类项目间的差异化,例如A电商系统有数据分析菜单和信息查询菜单,而B电商系统有运营活动、超值秒杀菜单。这时候就可以通过规范的json配置去动态的生成项目页面
module.exports = {
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'
}
}]
}
]
}
3.模板解析引擎
该文件负责将基类模板与项目模板合并,返回符合模板规范的JSON字符串。借助lodash的mergeWith方法实现基类模版与项目模板的合并,并最终返回合并后的项目模板,以供service层
使用 ,以返回以下的格式
modelKey:${model}
project:{
project1Key:${project1},
project2Key:${project2},
}
模板渲染过程
首先看看一个页面的目录结构,这里以dashBoard页面为例
dashBoard
│ dashboard.vue
│ entry.dashboard.js
│
├─complex-view
│ ├─header-view
│ │ │ header-view.vue
│ │ │
│ │ └─complex-view
│ │ └─sub-menu
│ │ sub-menu.vue
│ │
│ ├─iframe-view
│ │ iframe-view.vue
│ │
│ ├─schema-view
│ │ │ schema-view.vue
│ │ │
│ │ ├─complex-view
│ │ │ ├─search-panel
│ │ │ │ search-panel.vue
│ │ │ │
│ │ │ └─table-panel
│ │ │ table-panel.vue
│ │ │
│ │ └─hook
│ │ schema.js
│ │
│ └─sider-view
│ │ sider-view.vue
│ │
│ └─complex-view
│ └─sub-menu
│ sub-menu.vue
│
└─todo
todo.vue
在js文件中,引入boot启动文件与vue文件,然后对于路由再做额外的处理即可
import boot from '$pages/boot.js'
import dashboard from './dashboard.vue'
const routes = []
//对路由的处理
boot(dashboard, {routes})
下图是整体渲染体系图:
源数据中不同的config对应不同的解析器,就和之前elpis-core的各个loader一样,你配置怎样的数据,那么不同的解析器就会解析并渲染到相应的部分,这里要说明的是渲染的不仅仅是常见的页面,还包括对api、数据库的处理。真正做到一份数据——整个项目,实现以数据驱动项目的理念。例如下列对于schema-content的schema源数据配置。这里就是基类模版中table-content的配置,包含了表单,搜索栏。
schema:{
type:'object',
properties:{
product_id:{
type:'string',
label:'商品ID',
tableOption:{} //该字段在表单中的配置
},
product_name:{
type:'string',
label:'商品名称',
tableOption:{},
searchOption:{} //该字段在搜索栏中是否配置
},
}
},
tableConfig:{ //表单配置
headerButtons:[],
rowButtons:[]
},
searchConfig:{}, //搜索配置
apiConfig:{}, //api配置
comConfig:{}, //模块组件配置
dbConfig:{}, //数据库配置
}
心得
看哲哥的课不仅是学习代码能力,更能提升代码思维能力(“哲玄前端”,《大前端全栈实践课》),这里记录下对我有启发的两点
思维方式的转变
当你开发框架这种通用性代码时,要思考的就不仅仅是像业务代码那样能实现功能就可以的,还得考虑到代码的通用性,在这个模块能用,放到那个模块中也照用不误。这就要求在写代码时要考虑到更多的情况,思维要有一定的跳脱性
什么是重构
重构是在项目开发初期发现目前的模式难以支撑接下来项目的健康发展而在事情还可以挽回之前做的代码更改与调整。那种从头到尾修改一遍的不是重构,而是重写。因为重构的目的就在于减缓项目的熵增速度,使其混乱的速度降到尽可能的低以维持项目较为健康的运转。就想人一样,我们终将会老去,但是我们可以通过养生的手段使得我们的状态没那么快的变差。项目也是一样,所以这也是对我的一点启发,因为之前也看到过这种面试题:你认为什么是重构?当时我在想重构不就是把之前的代码优化优化吗,包括逻辑、代码运行效率方面的,但我想这次我有了更好的答案。