引言
这个阶段应该是项目最核心的阶段,主要以领域模型(DSL),结合json-schema规范形成配置文件,通过解析引擎,根据配置生成相应的页面。
自于抖音“哲玄前端”课程(《大前端全栈实践课》)
一、领域模型
领域特定语言(Domain Specific Language)是为特定业务领域定制的语言及配套模型体系,通过声明式配置表达业务规则和界面逻辑,而非通用编程语言。而领域模型则是对业务领域的抽象表示,包含实体、规则和流程的标准化结构。
二、设计及作用
1、核心的设计理念:
(1). 配置驱动:通过一套基于json-schema规范生成的配置,渲染整个站点的页面组件
(2). 模型继承:基础配置可被多个项目继承和扩展,支持沉淀重复开发内容,同时也可以加入一些定制化内容
(3). 组件化:基于 Vue 3 的组件化架构,支持动态组件渲染
(4). 领域分离:按业务领域组织模型,衍生出不同类型的项目
2、作用
解决以下问题:
(1). 重复性 CRUD 开发,即业务模块的增删改查的复写及页面绘制等。
(2). 组件复用困难,不同页面中的相似组件功能差异大,难以复用
(3). 页面状态管理复杂,搜索条件、分页状态、选中状态等需要手动管理
(4). 开发效率低下 ,业务逻辑耦合程度高和 UI 样式重复,开发周期长
(5). 维护成本高,页面逻辑分散,修改功能需要改动多处代码
统一规范的配置驱动加上组件化,让手写页面变成配置生成页面,大幅减少crud的同时提高开发效率及代码质量
三、实现
1、文档(docs)
项目文档,其实也就是描述项目且没有具体配置的json-schema,作为一个基础的模板,后面的具体配置也就在这上面进行拓展
{
mode: "dashboard", //模板类型,不用模板对应不一样的模板数据结构
name: "", // 名称
desc: "", // 描述
icon: "", // 图标
homePage: "", // 首页(项目配置)
menu: [ //头部菜单
{
key: "", //菜单唯一描述
name: "", //菜单名称
menuType: "", //枚举值,group / module
subMenu: [], //当 menuType == group 时,可填
moduleType: "", // 枚举值 sider/iframe/custom/schema
siderConfig: { //当 menuType == sider 时
menu: [] //侧边栏菜单
},
ifameConfig: { //当 menuType == iframe 时
path: "", // iframe 路径
},
customConfig: { //当 menuType == custom 时
path: "", //自定义路由路径
},
schemaConfig: { //当 menuType == schema 时
api: "", //数据源 API (遵循 RESTFUL 规范)
schema: { //板块数据结构
type: "object",
properties: {
key: {
...schema, //标准 schema 配置
type: "", //字段类型
label: "", //字段的中文名
tableOption: { //字段在 table 中相关的配置
...elTableColumnConfig, // 标准 el-table-column 配置
toFixed: 0, //保留小数点后几位
visiable: true, // 默认为 true (false 时,表示不在表单中展示)
},
searchOption: { // search-bar 中的相关配置
...elComponentConfig, //标准 el-component-column 配置
comType: "", // 配置组件类型 input/select/.....
default: "", // 默认值
enumList: [], // comTyppe === 'select'时,下拉框可选项
api: "", //comType === 'dynamicSelect'时,请求的API
},
},...//可扩展
},
},
tableConfig: { // table 相关配置
headerButton: [
{
label: "", // 按钮中文名
eventKey: "", // 按钮事件名
eventOption: {}, //按钮事件具体配置
...elButtonConfig, // 标准 el-button 配置
},
],
rowButton: [
{
label: "", // 按钮中文名
eventKey: "", // 按钮事件名
eventOption: {
params: {//当event === remove
//paramsKey = 参数的键值
// rowVlaueKey = 参数值 (当格式为 schema:: tableKey 的时候,到 table 中找相应的字段)
paramsKey: rowVlaueKey,
},
}, //按钮事件具体配置
...elButtonConfig, // 标准 el-button 配置
},... //可扩展
],
},
searchConfig: {}, // search-bar 相关配置
componentsConfig: {}, // 模块组件
},
},
],
};
2、重载及继承基础模板
在上一部分的基础文档上进行填充,我们就会得到一份配置好的json-schema。当然需要抽取一个model作为初始模板,然后进行系统的差异化配置。有了两份配置那么就需要进行重载和继承,用loadsh融合成一份新的配置,重点注意数组的情况。
_.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
);
result.push(
projItem ? projectExtendModel(modelItem, projItem) : modelItem
);
}
//处理新增
for (let i = 0; i < projValue.length; i++) {
const projItem = projValue[i];
const modeItem = modelValue.find(
(modelItem) => modelItem.key === projItem.key
);
if (!modeItem) {
result.push(projItem);
}
}
return result;
}
});
3、视图组件架构
整体设计
(1) header-view: 头部菜单,支持项目切换和多级菜单
(2) sider-view: 侧边栏菜单,支持嵌套路由和默认选中
(3) schema-view: 配置驱动的CRUD页面核心组件
(4) iframe-view: 外部页面嵌入或微前端服务嵌入
(5) custom-view:自定义页面(拓展区域)
4、schema-view
(1) 、数据清洗(消除噪音)
除却菜单项、拓展及嵌入区域,就剩下CRUD页面核心组件区域,要根据配置项进行渲染,首先得对相应的项进行清洗,只取当前页面所需要的配置,如下:
// 配置分离和转换
const buildDtoSchema = (_schema, comName) => {
const dtoSchema = { type: 'object', properties: {} }
for (const key in _schema.properties) {
const props = _schema.properties[key]
if (props[`${comName}Option`]) {
// 分离基础属性和组件配置
let dtoProps = { option: props[`${comName}Option`] }
dtoSchema.properties[key] = dtoProps
}
}
return dtoSchema
}
(2)、schema-table
动态列渲染:
// schema-table.vue核心渲染逻辑
<template v-for="(schemaItem, key) in schema.properties">
<el-table-column
v-if="schemaItem.option.visible !== false"
:key="key"
:prop="key"
:label="schemaItem.label"
v-bind="schemaItem.option" // 动态绑定所有配置
/>
</template>
(3)、schema-search-bar
上面的schema-table是根据配置进行渲染的,搜索项也得根据配置来,就需要根据配置中对应的字段进行动态组件渲染:
// search-item-config.js - 组件配置映射
const SearchItemConfig = {
input: { component: input },
select: { component: select },
dynamicSelect: { component: dynamicSelect },
dateRange: { component: dateRange }
}
// 动态组件渲染 - schema-search-bar.vue
<component
:is="SearchItemConfig[schemaItem.option?.comType]?.component"
:schema-key="key"
:schema="schemaItem"
@loaded="handleChildLoaded"
/>
五、性能优化
1、按需加载与路由分割:
routes.push({
path: '/view/dashboard/schema',
component: () => import('./complex-view/schema-view/schema-view.vue')
})
2、动态组件及扩展:
// SearchItemConfig组件单例模式
const SearchItemConfig = {
input: { component: input }, // 组件实例复用
select: { component: select },
// 避免重复创建组件实例,提升渲染性能
dateRange: { component: dateRange },
dynamicSelect: { component: dynamicSelect },
// 不需要修改核心代码,只需扩展即可使用
}