一、目标
基于设定好的DSL(Domain-Specific Language,领域特定语言) 模板,建立领域模型model,并衍生出各类project配置,通过service层解析到dashboard模板页中展示。
-
为什么要使用DSL,而不是直接写代码?
传统方式写代码,需要新增一个菜单的话:修改router -> 新增页面组件 -> 修改菜单
DLS则是:修改配置 -> 读取DSL配置 -> 解析配置 -> 自动生成菜单
使用DLS,开发人员只要进行修改配置,不用修改代码;后续操作会在系统启动时进行;开发人员从写代码变为写配置。
-
DSL的核心思想是什么?使用DSL的好处是什么?
DSL的核心思想实际上就是 配置驱动,系统架构变成:DSL配置 -> 解析 -> 生成页面
使用DSL的好处:提高开发效率、统一规范、增强系统可扩展性、减少代码量
-
为什么要设计'model + project'这种继承结构,而不是每个项目一份完整配置?
model是基础模板(通用配置),project是具体的项目配置(差异配置);这样设计的原因是为了避免重复配置
如果是每个项目一份完整配置的话,会有大量的重复配置,且后续修改册成本极高
二、DSL配置解析
1. DSL模板代码
系统会根据这个配置自动生成菜单、页面、模块
{
mode: 'dashboard'; // 模板类型,不同模板类型对应不一样的模板数据结构
name: ''; // 名称
desc: ''; // 描述
icon: ''; // 图标
homePage: ''; // 首页(项目配置)
// 头部菜单
menu: [
{
key: '', // 菜单唯一描述
name: '', // 菜单名称
menuType: '', // 枚举值 group(下拉框) / module(页面跳转/侧边栏)
// 当 menuType 为 group 时,可填;可递归配置 menuItem
subMenu: [],
// 当 menuType 为 module 时,可填
// 枚举值: iframe(三方页面) / custom(自定义页面) / schema(共通页面) / sider(侧边栏)
moduleType: '',
// 当 moduleType 为 sider 时,可填
siderConfig: {
menu: [], // 除 moduleType 为 sider以外 可进行递归配置menuItem
},
// 当 moduleType 为 iframe 时,可填
iframeConfig: {},
// 当 moduleType 为 custom 时,可填
customConfig: {},
// 当 moduleType 为 schema 时,可填
schemaConfig: {},
}
];
}
2. DSL整体架构
Dashboard
│
├── 基础信息
│ name
│ desc
│ icon
│ homePage
│
└── menu
│
├── group
│ └── subMenu // 下拉菜单
│
└── module
│
├── iframe // 嵌入的第三方页面
├── custom // 用户自定义页面
├── schema // DSL页面
└── sider // 侧边栏
3. 运行流程
读取DSL、加载DSL配置 -> 解析model、peojct -> 配置继承 merge(model + project) -> 返回组织最终数据结构 -> 解析DSL结构 -> 渲染UI
三、动态配置
1. 动态生成菜单
通过 menuType 来区分菜单类型:
-
group
下拉菜单
-
module
可打开页面的菜单,点击后打开一个模块页面
menu: [
{
key: 'user',
name: '用户管理',
menuType: 'module', // 菜单按钮、点击后跳转到特定菜单页面
moduleType: '', // 菜单页面样式
},
{
key: 'product',
name: '商品管理',
menuType: 'group', // 下拉框选择,选项为subMenu中的配置
subMenu: [
{
key: "productOne",
name: "商品1",
menuType: "module", // 菜单按钮、点击后跳转到特定菜单页面
moduleType: "", // 菜单页面样式
}
]
}
]
2. 动态生成页面
通过 moduleType 来区分菜单类型:
-
ifarame
嵌入第三方页面,需要在 iframeConfig 中配置第三方页面的路径
-
custom
自定义页面,加载一个真实的前端组件(DSL没法配置的复杂业务页面),需要在 customConfig 中配置具体路由
-
schema
DSL页面,解析后生成的页面(比如表单、表格等),具体的信息需要在 schemaConfig 中进行配置
-
sider
侧边栏模块,需要在 siderConfig 中配置侧边栏里面有哪些菜单
menu: [
{
key: 'user',
name: '用户管理',
menuType: 'module', // 菜单按钮、点击后跳转到特定菜单页面
moduleType: "custom",
customConfig: {
path: "" // 自定义页面 路由路径
}
},
{
key: "client",
name: "客户管理",
menuType: "module", // 菜单按钮、点击后跳转到特定菜单页面
moduleType: "iframe",
iframeConfig: {
path: "" // iframe (三方页面)路径
}
},
{
key: 'product',
name: '商品管理',
menuType: 'module', // 菜单按钮、点击后跳转到特定菜单页面
moduleType: 'schema', // DSL页面
schemaConfig: {
api: '',
schema: {},
}
},
{
key: "client",
name: "客户管理",
menuType: "module", // 菜单按钮、点击后跳转到特定菜单页面
moduleType: "sider", // 侧边栏
siderConfig: {
menu: [] // 侧边栏菜单配置
}
}
]
四、schemaConfig配置(⭐⭐⭐⭐⭐)
我们的schemaConfig可以理解为以下几部分:
schemaConfig
│
├── api 数据接口
├── schema 数据结构 + UI规则
│ │
│ ├── type
│ └── properties
│ │
│ ├── type
│ ├── label
│ └── xxxOption
│
└── xxxConfig
-
api
数据源API(遵循 RESTFUL 规范)
-
schema
遵循 JSON Schema 标准,在原有 JSON Schema 基础上扩展了UI配置能力
JSON Schema原本用于 数据结构定义,扩展后变成 数据结构 + UI描述
-
properties
配置的是一个字段,系统会按需把它解析成 表格列、搜索条件
-
xxxOption
它可以是 tableOption,也可以是 searchOption;控制option在某个UI中是如何表现的
tableOption:控制表格列,解析后将option中的配置绑定到 el-table-column上
searchOption:搜索表单配置,通过 comType 来定义配置组件类型
-
-
xxxConfig
控制整个UI模块,与schema中配置的xxxOption相呼应;下面这个例子是tableConfig中的配置:
tableConfig: { // 表格顶部按钮展示 headerButtons: [ { label: "新增商品", eventKey: "showComponent", // 事件标识,点击按钮后触发某个事件 type: "primary", plain: true } ], // 表格每一行按钮 rowButtons: [ { label: "删除", eventKey: "remove", // 触发事件 type: "danger", eventOption: { // 事件参数 params: { product_id: `schema::product_id` } } } ] }
五、扩展
1. Vue中的动态组件(Vue dynamic component)
动态组件代码逻辑:
<component
:is="SearchItemConfig[option?.comType]?.component"
:ref="handleSearchComList"
:schema-key="key"
:schema="schemaItem"
@loaded="handleChildLoaded"
/>
- 为什么要使用动态组件来 加载 searchOption中的表单设置? 为什么不直接使用element-plus中的组件,通过v-if来展示?
使用v-if来展示的话,后续每增加一个组件,就要改一次代码,最终会变为巨型if结构。后续组件数量多起来了,v-if和动态组件的维护成本差距会非常大。
并且我们是由DSL配置来决定展示UI,在DSL中我们的comType已经是确定的了,只渲染某一个组件就行;但是v-if是代码决定UI,每次展示都要走一遍条件判断。
2. 懒加载
-
什么是懒加载?为什么要做懒加载?
懒加载的核心思想是需要的时候才加载资源,而不是页面一打开就加载所有代码;
做懒加载的核心原因只有一个:减少首屏加载体积,缩短首屏加载时间;
如果不做懒加载浏览器在打开首页的时候就要下载全部js,但是用户可能只访问其中的某个或某几个。
-
懒加载的实现方式和实现前提
懒加载不是浏览器自动实现的,而是需要两个前提:
-
动态 import
Vue路由懒加载:
component: () => import('./pages/user') -
打包工具进行代码分包
在编译后webpack会生成 user.chunk.js
-
-
懒加载和代码分包的关系
代码分包是构建阶段的行为,是把代码拆分成多个js文件
懒加载是运行阶段的行为,是决定什么时候加载这些js文件
懒加载必须要依赖代码分包,否则只有一个js文件,没办法实现懒加载
-
浏览器是怎么加载懒加载文件的?
懒加载实际上就是在运行时动态插入 script 标签,具体流程是:
构建阶段,打包工具将代码拆开打包 -> 用户访问页面 -> main.js加载 -> 用户进入某个路由 -> 执行 import() -> webpack runtime执行 -> 创建script标签 -> 浏览器下载chunk -> 执行JS -> 页面渲染
六、总结
通过使用DSL领域模型,达到提高页面开发效率,减少重复代码,支持多业务项目复用(将重复工作进行整合)的目的;了解DSL解析机制和渲染机制。
同时我们的DSL配置采用model + project继承结构,在model中定义通用的页面结构,在project中定义具体业务项目配置;达到减少重复DSL的目的。
七、回顾优化
使用隐藏input注入数据
个人觉得这部分注入不是会在控制台中看到吗?
<body style="margin: 0;">
<div id="root"></div>
<input id="env" value="{{ env }}" style="display: none;">
<input id="options" value="{{ options }}" style="display: none;">
<input id="projKey" value="{{ projKey }}" style="display: none;">
</body>
<script type="text/javascript">
try {
window.env = document.getElementById('env').value;
window.projKey = document.getElementById('projKey').value;
const options = document.getElementById('options').value;
window.options = JSON.parse(options);
} catch (e) {
console.log(e);
}
</script>
可不可以在渲染页面的JS中直接将变量set到 window.__INIT_DATA__ 中 (待验证)