一、动态组件的DSL设计
动态组件的DSL的核心围绕着唯一key(schema.key) 做展开设计
动态组件设计思想:动态组件 DSL 配置 + 动态组件中要展示的字段DSL配置 + 动态组件触发展示
1、属于schema-view模块 则需要配置属于schemaConfig.schema和schemaConfig.componentConfig模块下对应字段和配置 其他组件下的config配置动态组件的触发事件和触发相关的动态组件展示(如:tableConfig)
2、schemaConfig.schema配置相关字段需要不要展示在动态组件中 以何种组件形态展示 组件属性 主要如下:
csharp
product_name:{
createFormOption: {
...elComponentConfig, // 标准el-component 配置
comType: '', // 控件类型 input/ select/ input-number
visible: true, // 是否展示 默认为true
disabled: false, // 是否禁用 默认false element-plus中的配置
default: '', // 默认值 对标element-plus中的placeholder配置
// comType === select 有enumList配置
enumList: [], // 下拉框可选项
},
}
3、schemaConfig.componentConfig配置 动态组件名称 动态组件中的主键 主要如下:
csharp
// 动态组件 相关配置
componentConfig: {
// create-form 表单相关配
createForm: {
title: '', // 表单标题
saveBtnText: '', // 保存按钮文案
},
// edit-form 表单相关配
editForm: {
mainKey: '', // 表单主键 用于唯一标识修改的数据对象
title: '', // 表单标题
saveBtnText: '', // 保存按钮文案
},
// detail-panel 相关配置
detailPanel: {
mainKey: '', // 查询主键 用于唯一标识查询的数据对象
title: '', // 详情标题
}
// 支持用户动态扩展 如下:
// comA: {},
// comB: {},
// ...
}
4、触发事件展示动态组件 是依赖配置的eventKey === showComponent eventOption.comName 组件名称(对应componentConfig下的key 键名称)主要如下:
css
tableConfig: {
headerButtons: [{
label: '新增商品',
eventKey: 'showComponent',
eventOption: {
comName: 'createForm'
},
type: 'primary',
plain: true
},{
label: '展示demo',
eventKey: 'showComponent',
eventOption: {
comName: 'demoComponent'
},
type: 'info',
plain: true
}]
}
二、根据DSL实现动态组件
设计思想: DSL 动态组件配置解析 + 动态组件出口配置(统一管理展示组件) + component组件动态加载 +状态管理(单向数据流)
1、DSL动态组件配置解析
将DSL配置组件的 schema(如配置了tableOption,动态组件名称Option) 和 相关config(如tableConfig,componentConfig)拿出
并组合成如:
yaml
// 组件相关的直接合并成如下:
components = {
comKey: {
schema: componentSchema, // 如下
config: componentConfig // 如下
}
}
// 例如:
// 字段在table中的相关配置
tableOption: {
...elTableColumnConfig, // 标准el-table-column 配置
toFixed: 0, // 保留小数点后几位
visible: true, // 默认为true (false时 表示不在表单中显示)
},
tableSchema = {
properties: {
product_id: {
option: tableOption
},
product_name: {
option: tableOption
},
// ...
}
}
tableConfig = schemaConfig.tableConfig
完整解析DSL代码
ini
import { ref, watch, onMounted, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import { useMenuStore } from '$elpisStore/menu.js';
/**
* 解析DSL中的配置
*/
export const useSchema = () => {
const route = useRoute();
const menuStore = useMenuStore();
const api = ref('');
const tableSchema = ref({});
const tableConfig = ref();
const searchSchema = ref({});
const searchConfig = ref();
const components = ref({});
// 构造 schemaConfig 相关配置 给schema-view解析
const buildData = () => {
const { key, sider_key: siderKey } = route.query;
const mItem = menuStore.findMenuItem({
key: 'key',
value: siderKey ?? key
})
if(mItem && mItem.schemaConfig) {
const { schemaConfig: sConfig } = mItem;
const configSchema = JSON.parse(JSON.stringify(sConfig.schema)); //读取schemaConfig中的配置
api.value = sConfig.api ?? '';
tableSchema.value = {};
tableConfig.value = undefined;
searchSchema.value = {};
searchConfig.value = undefined;
components.value = {};
nextTick(() => {
// 构造tableSchema 和tableConfig
tableSchema.value = buildDtoSchema(configSchema, 'table'); // 获取到相应的数据赋值
tableConfig.value = sConfig.tableConfig;
// 构造 searchSchema 和 searchConfig
const dtoSearchSchema = buildDtoSchema(configSchema, 'search');
// 可能路由跳转的时候需要携带搜索参数 将其赋值给搜索框中的默认值
for(const key in dtoSearchSchema.properties) {
if(route.query[key] !== undefined) {
dtoSearchSchema.properties[key].option.default = route.query[key];
}
};
searchSchema.value = dtoSearchSchema;
searchConfig.value = sConfig.searchConfig;
// 构造components = {comKey: { schema: {}, config: {} }}
const { componentConfig } = sConfig;
if(componentConfig && Object.keys(componentConfig).length > 0) {
const dtoComponents = {};
for(const comName in componentConfig) {
dtoComponents[comName] = {
schema: buildDtoSchema(configSchema, comName),
config: componentConfig[comName]
}
}
components.value = dtoComponents;
}
})
}
};
// 通用构建schema 方法 (清除噪音)
const buildDtoSchema = (_schema, comName) => {
if (!_schema?.properties) {return {}}
const dtoSchema = {
type: 'object',
properties: {}
}
// 提取有效schema字段信息
for (const key in _schema.properties) {
const props = _schema.properties[key];
if(props[`${comName}Option`]) {
let dtoProps = {};
// 提取props 中非option的部分 存放到dtoProps 中
for(const pKey in props) {
if(pKey.indexOf('Option') < 0) {
dtoProps[pKey] = props[pKey];
}
}
// 处理 comName Options
dtoProps = Object.assign({}, dtoProps, { option: props[`${comName}Option`]});
// 处理required字段 例如components中可能会对字段做要求
const { required } = _schema;
if(required && required.find(pk => pk === key)) {
dtoProps.option && (dtoProps.option.required = true);
// dtoProps.option?.required = true;
}
dtoSchema.properties[key] = dtoProps;
}
}
return dtoSchema;
}
watch([
() => route.query.key,
() => route.query.sider_key,
() => menuStore.menuList
], () => {
buildData();
}, {deep: true});
onMounted(() => {
buildData()
});
return {
api,
tableSchema,
tableConfig,
searchSchema,
searchConfig,
components
}
}
2、动态组件出口配置(统一管理展示组件)component-config.js
javascript
import CreateForm from "./create-form/create-form.vue";
import EditForm from './edit-form/edit-form.vue';
import DetailPanel from './detail-panel/detail-panel.vue';
// 业务扩展 component 配置
import BusinessComponentConfig from '$businessComponentConfig';
const ComponentConfig = {
createForm: {
component: CreateForm
},
editForm: {
component: EditForm
},
detailPanel: {
component: DetailPanel
}
};
export default {
...ComponentConfig,
...BusinessComponentConfig
}
3、component动态加载
ini
<component
v-for="(item, key) in components"
:key="key"
:is="ComponentConfig[key]?.component"
ref="comListRef"
@command="onComponentCommand"
></component>
4、状态管理(单向数据流)
容器组件(业务组件分发数据 并监听数据的改变动态加载展示组件,获取展示组件中的改变的数据)
展示组件(获取逻辑组件分发到数据 依靠数据做初始化动作 并做相关的校验 监听逻辑组件数据的改变而发生展示改变 发送展示组件因用户改变后的数据给逻辑组件)
三、动态组件扩展设计
扩展的核心:不改变核心代码的基础 最快接入新组件、新逻辑、新业务
1、定义规范和标准: DSL 规范设计标准 依据JSON-schema规范
2、组件扩展维度的考虑(UI 交互 数据 表单校验规则等)
3、组件扩展加载统一管理: 自带组件和业务自定义组件 统一加载管理到一处 利于组件解析器 处理
4、依据DSL设计组件解析器
5、动态组件的工程化辅助(HMR 热更新)
elpis项目中的扩展维度:
1、系统的扩展(model和name字段)
2、页面组件的扩展(moduleType字段)
3、业务组件的扩展(componentConfig 字段)
4、基础组件的扩展(comType字段)
5、组件交互的扩展(eventKey和eventOption 字段)
6、自主思考(后期可试试加入权限字段控制 生成SaaS系统 还有主题字段 提供不同的主题色)
四、组件模块的拆分
设计思想: 按照逻辑状态分为展示型组件和容器组件
展示组件: 只负责UI渲染 不关心数据来源 数据全靠父组件传入 自身无状态 或仅仅只含UI交互状态 有点 无副作用 易复用
容器组件: 负责业务逻辑 数据获取和状态管理(调用API 连接Pinia) 再将数据分发给子组件 通常包较多的副作用 复用性低
1、容器组件
对于elpis项目中的schema-view.vue模块来说
1、search-panel
2、table-panel
3、component 动态加载展示的组件
schema-view文件分布
scss
schema-view/
├── complex-view/
│ ├── search-panel/ (目录)
│ └── table-panel/ (目录)
├── components/
│ ├── create-form/ (目录)
│ ├── detail-panel/ (目录)
│ ├── edit-form/ (目录)
│ └── component-config.js (文件)
├── hooks/
│ └── schema.js (文件)
└── schema-view.vue (文件)
上述组件基本上都是容器组件 search-panel 和table-panel 主要是用于分发数据 不做数据获取和状态管理
2、展示组件
elpis项目中存放在widgets文件夹中 又称公共组件
和schema-view相关的文件
scss
schema-form/
├── complex-view/ (目录)
│ ├── input/ (目录)
│ ├── input-number/ (目录)
│ └── select/ (目录)
├── schema-form.vue (文件)
└── form-item-config.js (文件)
schema-search-bar/
├── complex-view/ (目录)
│ ├── data-range/ (目录)
│ ├── dynamic-select/ (目录)
│ ├── input/ (目录)
│ └── select/ (目录)
├── schema-search-bar.vue (文件)
└── schema-item-config.js (文件)
schema-table/
└── schema-table.vue (文件)