前言
此文接上篇 [study] DSL 方案实现数据化快速搭建不同领域后台 的拓展补充
在上篇文章中提到了强大的拓展性,在此进行 components 部分的拓展补充
前提回顾
上篇文章提到了 dsl 的配置
// 可以看到在 schemaConfig 中的配置是这样的
schemaConfig: {
api: 'xxx',
schema: {
type: 'object',
properties: {
[xxx]: {
...config, // 各种配置
tableConfig, // 列表配置
// 这里可以无限拓展 通过实现不同的解析器实现页面功能
[componentName]Option // 各种组件的配置
apiConfig // api 相关配置
dbConfig // 通过数据库相关配置反推 sql 语句直接生成一张表
}
}
}
}
// 列表通用配置
tableConfig: {}
componentConfig: {
[componentName]: {
...componentConfig
}
} // 各种组件的配置
apiConfig // api 相关配置
dbConfig // 通过数据库相关配置反推 sql 语句直接生成一张表
可以通过配置多个 componentConfig 的内容,快速搭建动态组件并使用,这里就来进行实现
dsl 配置设计
假定要实现一个展示当前表格某个商品的组件 detail-panel 设计如下
// 可以看到在 schemaConfig 中的配置是这样的
schemaConfig: {
api: 'xxx',
schema: {
type: 'object',
properties: {
[xxx]: {
...
// 字段在 detailPanel 的配置
detailPanelOption: {
...elComponentConfig // 标准 el-component-config 配置
}
}
}
}
}
componentConfig: {
// 组件这里都是可以通过需要去配置 而不是只有这两个 如果还需要 subTitle 就添加 subTitle 字段等等
detailPanel: {
mainKey: "" // 表单唯一值
title: "" // 表单标题
}
}
组件的实现
首先就是创建我们定义的组件了
<template>
<el-drawer
v-model="isShow"
direction="rtl"
destroy-on-close
:size="550"
>
<template #header>
<h3>{{ title }}</h3>
</template>
<template #default>
<el-card
v-loading="loading"
shadow="always"
class="detail-panel"
>
<el-row
v-for="(item, key) in components[name]?.schema?.properties"
:key="key"
type="flex"
align="middle"
class="row-item"
>
<el-row class="item-label">{{ item.label }}:</el-row>
<el-row class="item-value">{{ dtoModel[key] }}</el-row>
</el-row>
</el-card>
</template>
</el-drawer>
</template>
<script setup>
import { ref, inject } from 'vue';
import $curl from '$common/curl.js';
// 这是通过获取父组件统一分发的 api
// 在上篇文章讲到的 api 数据源 以及需要获取到的 componentConfig 配置
// 均在父组件 hook 中分发到各个子组件
const { api, components } = inject('schemaViewData');
const name = ref('detailPanel');
const isShow = ref(false);
const loading = ref(false);
const title = ref('');
const mainKey = ref('');
const mainValue = ref(undefined);
const dtoModel = ref({});
const show = (rowData) => {
const { config } = components.value[name.value];
title.value = config.title;
mainKey.value = config.mainKey;
mainValue.value = rowData[config.mainKey];
dtoModel.value = {};
isShow.value = true;
fetchFormData();
};
// 数据源的 API 且该 API 必须遵循 RESTFUL 规范
// 所以我们请求直接用 get
const fetchFormData = async () => {
if (loading.value) return;
loading.value = true;
const res = await $curl({
url: api.value,
method: 'get',
query: {
[mainKey.value]: mainValue.value
}
});
loading.value = false;
if (!res || !res.success || !res.data) return;
dtoModel.value = res.data;
};
defineExpose({
name,
show
});
</script>
接下来多展示一个 hook 吧父组件只展示分发部分的代码
// hook 的实现
import { ref, watch, onMounted, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import { useMenuStore } from '$store/menu';
/** schema hook */
export const useSchema = () => {
const route = useRoute();
const menuStore = useMenuStore();
const api = ref('');
const tableSchema = ref({});
const tableConfig = ref(undefined);
const searchSchema = ref({});
const searchConfig = ref(undefined);
const components = ref({});
/** 构造 schema 相关配置, 输送给 schema-view 进行配置渲染 */
function buildDate(data) {
const { key, sider_key: siderKey } = route.query;
const mItem = menuStore.findMenuItem({
key: 'key',
value: siderKey ?? key
});
if (!mItem || !mItem.schemaConfig) return;
const { schemaConfig: sConfig } = mItem;
// 深 copy 避免后续更改操作污染源数据
const configSchema = Object.assign({}, sConfig);
api.value = configSchema.api ?? '';
tableSchema.value = {};
tableConfig.value = undefined;
searchSchema.value = {};
searchConfig.value = undefined;
components.value = {};
nextTick(() => {
// 构造 tableSchema 和 tableConfig (表格)
tableSchema.value = buildDtoSchema(configSchema.schema, 'table');
tableConfig.value = configSchema.tableConfig;
// 构造 searchSchema 和 searchConfig (搜索)
const dtoSearchSchema = buildDtoSchema(configSchema.schema, '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 = configSchema.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.schema, comName),
config: componentConfig[comName]
};
}
components.value = dtoComponents;
}
});
}
/** 通用构建方法 (清楚噪音) */
function buildDtoSchema(_schema, comName) {
if (!_schema?.properties) return {};
const { required } = _schema;
const dtoSchema = {
type: 'object',
properties: {}
};
// 提取有效值 schema 字段信息
for (const key in _schema.properties) {
const props = _schema.properties[key];
if (props[`${ comName }Option`]) {
let dtoOption = {};
// 提取 props 中非 options 的部分, 存放到 dtoOption 中
for (const pKey in props) {
if (pKey.indexOf('Option') < 0) dtoOption[pKey] = props[pKey];
}
// 处理 comName Option
dtoOption = Object.assign({}, dtoOption, { option: props[`${ comName }Option`] });
// 处理必填字段
if (required && required.includes(key)) {
dtoOption.option.required = true;
}
dtoSchema.properties[key] = dtoOption;
}
}
return dtoSchema;
}
watch([
() => route.query.key,
() => route.query.sider_key,
() => menuStore.menuList
], () => buildDate(),
{
deep: true
}
)
;
onMounted(() => buildDate());
return {
api,
tableSchema,
tableConfig,
searchConfig,
searchSchema,
components
};
};
// 父组件的更新和挂载
<template>
<component
v-for="(_, key) in components"
:key="key"
:is="componentConfig[key]?.component"
ref="comListRef"
@command="onComponentCommand"
></component>
</template>
<script setup>
import { useSchema } from './hook/schema';
import { provide, ref } from 'vue';
import DetailPanel from './detail-panel/detail-panel.vue';
const ComponentConfig = {
detailPanel: {
component: DetailPanel,
}
}
const {
api,
tableConfig,
tableSchema,
searchConfig,
searchSchema,
components
} = useSchema();
provide('schemaViewData', {
api,
tableConfig,
tableSchema,
searchConfig,
searchSchema,
components
});
总结
由此可见了,实现一个动态组件就变得十分简单,哪怕囊括 dsl 配置也就百来行就可以实现了,大大的提高了创建组件的效率,可以达到一个快速搭建的效果(hook 和 父组件只写一次不需要再次书写),以上就是有关于动态组件 (components) 的拓展了,如果有什么高见提出一起交流一下噢~~
学习资源
抖音-哲玄前端
大全栈实践课