上一阶段我们基于 DSL 配置实现了schema-view页面的自动化渲染,核心解决了重复性页面开发的效率问题。但在实际业务中,新增、编辑、删除、查看等交互操作仍需手动开发组件,导致开发流程割裂。本阶段的核心目标是:在 schema-view 基础上,通过统一配置实现交互组件的动态加载、渲染与通信,让整个页面体系真正实现 "配置即开发"。
本文将围绕三个核心问题展开:
- 如何设计合理的配置体系,描述组件的结构、触发方式与字段属性?
- 如何解析配置并提取有效信息,为动态渲染提供数据支撑?
- 如何实现组件的分层渲染与跨层级通信,保证扩展性与可维护性?
一、配置体系设计:用 JSON 描述组件全生命周期
配置设计的核心思想是 "单一数据源":通过一个schemaConfig对象,统一描述组件元信息、触发规则、字段属性与 API 交互,让框架能够自动识别 "需要什么组件"、"什么时候触发"、"组件里有什么"、"如何与后端交互"。
1.1 组件元信息配置(componentConfig)
定义动态组件的基础属性(标题、按钮文本、主键等),作为组件的 "身份标识":
css
schemaConfig: {
componentConfig: {
createForm: {
title: '添加商品',
saveBtnText: '保存', // 按钮文本自定义
},
editForm: {
mainKey: 'product_id', // 主键字段,用于数据回显
title: '编辑商品',
saveBtnText: '保存',
},
detailPanel: {
mainKey: 'product_id',
title: '商品详情',
},
}
}
1.2 触发规则配置(tableConfig)
在表格的表头 / 行尾按钮中配置事件,指定 "点击哪个按钮触发哪个组件"。通过eventKey: 'showComponent'统一触发逻辑,eventOption.comName关联组件元信息:
css
schemaConfig: {
tableConfig: {
// 表头按钮(新增操作)
headerButtons: [{
label: '添加商品',
eventKey: 'showComponent', // 统一触发函数
eventOption: { comName: 'createForm' }, // 关联新增组件
type: 'primary',
plain: true
}],
// 行尾按钮(详情/编辑/删除)
rowButtons: [
{
label: '详情',
eventKey: 'showComponent',
eventOption: { comName: 'detailPanel' },
type: 'primary'
},
{
label: '修改',
eventKey: 'showComponent',
eventOption: { comName: 'editForm' },
type: 'warning'
},
{
label: '删除',
eventKey: 'remove', // 特殊事件(无需渲染组件)
eventOption: {
params: { product_id: 'schema::product_id' } // 动态取当前行product_id
},
type: 'danger'
}
]
}
}
1.3 字段属性配置(properties)
为每个数据字段配置 "在不同组件中的表现形式",通过xxxOption(如createFormOption)指定字段在对应组件中的控件类型、校验规则、默认值等:
css
schemaConfig: {
api: '/api/proj/product', // 基础API(遵循REST规范)
properties: {
product_id: {
type: 'string',
label: '商品ID',
// 表格中显示配置
tableOption: { width: 300, 'show-overflow-tooltip': true },
// 编辑表单中配置(禁用状态)
editFormOption: { comType: 'input', disabled: true },
// 详情面板中配置(仅展示)
detailPanelOption: {}
},
product_name: {
type: 'string',
label: '商品名称',
maxLength: 20, // 全局校验规则
minLength: 3,
tableOption: { width: 200 },
// 搜索框配置(动态下拉框)
searchOption: { comType: 'dynamicSelect', api: '/api/proj/product_enum/list' },
// 新增表单配置(默认值)
createFormOption: { comType: 'input', default: '哲玄新课程' },
editFormOption: { comType: 'input' },
detailPanelOption: {}
},
price: {
type: 'number',
label: '价格',
maximum: 1000, // 数值校验
minimum: 30,
tableOption: { width: 200, toFixed: 2 }, // 保留2位小数
searchOption: {
comType: 'select',
enumList: [/* 静态下拉选项 */]
},
createFormOption: { comType: 'inputNumber' }, // 数字输入框
editFormOption: { comType: 'inputNumber' },
detailPanelOption: {}
}
}
}
1.4 API 交互配置:REST 规范 + 动态参数
- API 复用:通过schemaConfig.api配置基础接口,框架根据操作类型自动映射 HTTP 方法(新增→POST、编辑→PUT、删除→DELETE、详情→GET)。
- 动态参数:通过eventOption.params配置参数映射,schema::xxx语法表示从当前行数据中取xxx字段值(如删除操作取product_id)。
配置设计总结
整个配置体系分为三层,层层递进:
- 组件层(componentConfig):描述组件 "是什么";
- 触发层(tableConfig):描述组件 "何时触发";
- 字段层(properties):描述组件 "包含什么";
- API 层:描述组件 "如何与后端交互"。
二、配置解析:用 HOOK 统一处理配置数据
配置解析的核心是 "去噪提取":从复杂的schemaConfig中过滤无效信息,提取出每个动态组件所需的 "字段 schema" 和 "组件配置",并封装为可复用的 HOOK(useSchema),供schema-view使用。
2.1 HOOK 核心功能
- 监听路由和菜单配置变化,自动重建配置数据;
- 分离表格、搜索、动态组件的配置,避免相互干扰;
- 为每个动态组件构建独立的 schema(仅包含该组件所需字段)。
2.2 关键代码实现
ini
// src/hooks/schema.js
import { ref, watch, onMounted, nextTick } from "vue";
import { useRoute } from "vue-router";
import { useMenuStore } from "@/store/menu.js";
export const useSchema = () => {
const route = useRoute();
const menuStore = useMenuStore();
// 暴露给外部的数据
const api = ref('');
const components = ref({}); // 动态组件配置:{ comName: { schema, config } }
const [tableSchema, tableConfig, searchSchema, searchConfig] = [ref({}), ref({}), ref({}), ref({})];
// 构建配置数据(核心函数)
const buildData = () => {
const { key, sider_key: siderKey } = route.query;
const menuItem = menuStore.findMenuItem({ key: 'key', value: siderKey ?? key });
if (!menuItem?.schemaConfig) return;
const { schemaConfig } = menuItem;
const configSchema = JSON.parse(JSON.stringify(schemaConfig.schema)); // 深拷贝避免污染原配置
// 初始化数据
api.value = schemaConfig.api ?? '';
[tableSchema.value, tableConfig.value, searchSchema.value, searchConfig.value, components.value] = [{}, {}, {}, {}, {}];
nextTick(() => {
// 1. 构建表格配置
tableSchema.value = buildDtoSchema(configSchema, 'table');
tableConfig.value = schemaConfig.tableConfig;
// 2. 构建搜索配置(支持路由参数回显)
const searchDto = buildDtoSchema(configSchema, 'search');
Object.keys(searchDto.properties).forEach(key => {
if (route.query[key] !== undefined) {
searchDto.properties[key].option.default = route.query[key];
}
});
searchSchema.value = searchDto;
searchConfig.value = schemaConfig.searchConfig;
// 3. 构建动态组件配置(核心)
const { componentConfig } = schemaConfig;
if (componentConfig && Object.keys(componentConfig).length) {
const coms = {};
Object.keys(componentConfig).forEach(comName => {
coms[comName] = {
schema: buildDtoSchema(configSchema, comName), // 仅提取该组件所需字段
config: componentConfig[comName]
};
});
components.value = coms;
}
});
};
// 通用schema构建:过滤指定组件的字段(去噪)
const buildDtoSchema = (_schema, comName) => {
if (!_schema?.properties) return { type: 'object', properties: {} };
const dtoSchema = { type: 'object', properties: {} };
Object.keys(_schema.properties).forEach(key => {
const props = _schema.properties[key];
// 只保留包含当前组件Option的字段(如createFormOption)
if (props[`${comName}Option`]) {
// 提取非Option属性(type、label、maxLength等)
const baseProps = Object.fromEntries(
Object.entries(props).filter(([k]) => !k.includes('Option'))
);
// 合并组件专属配置(xxxOption)
const dtoProps = {
...baseProps,
option: props[`${comName}Option`],
required: _schema.required?.includes(key) // 标记必填字段
};
dtoSchema.properties[key] = dtoProps;
}
});
return dtoSchema;
};
// 监听依赖变化,自动重建配置
watch([() => route.query, () => menuStore.menuList], buildData, { deep: true });
onMounted(buildData);
return { api, tableSchema, tableConfig, searchSchema, searchConfig, components };
};
三、分层渲染:从顶层到底层的动态组件实现
渲染设计遵循 "分层解耦" 原则,将组件分为三层:顶层容器(schema-view)、中间层组件(新增 / 编辑 / 详情)、底层字段控件(输入框 / 下拉框等),每层负责不同职责,通过 Vue 的动态组件()实现灵活渲染。
3.1 目录结构设计
python
src/
├── pages/
│ └── dashboard/
│ └── complex-view/
│ └── schema-view/
│ ├── schema-view.vue # 顶层容器
│ └── components/ # 中间层组件
│ ├── create-form/
│ ├── edit-form/
│ ├── detail-panel/
│ └── component-config.js # 中间层组件映射
└── widgets/
└── schema-form/ # 底层字段控件
├── schema-form.vue # 字段容器
├── form-item-config.js # 控件映射
└── components/ # 具体控件(input/select等)
3.2 顶层渲染:schema-view.vue
作为容器,负责加载中间层组件,管理组件状态与事件分发:
xml
<template>
<!-- 动态渲染中间层组件 -->
<component
v-for="(item, comName) in components"
:key="comName"
:is="ComponentConfig[comName]?.component"
ref="comListRef"
@command="handleComponentCommand"
></component>
</template>
<script setup>
import { ref, provide } from "vue";
import { useSchema } from "@/hooks/schema.js";
import ComponentConfig from "./components/component-config.js";
// 获取解析后的配置
const { api, components } = useSchema();
// 提供全局数据(供子组件注入)
provide("schemaViewData", { api, components });
// 组件引用集合
const comListRef = ref([]);
// 处理子组件事件(如保存后刷新表格)
const handleComponentCommand = ({ event, params }) => {
if (event === "loadTableData") {
// 触发表格刷新逻辑
emit("refreshTable");
}
};
</script>
3.3 中间层渲染:新增 / 编辑 / 详情组件
中间层组件是 "组件框架",负责布局(抽屉 / 弹窗)、按钮事件(保存 / 关闭)、表单渲染,通过schema-form组件加载底层字段控件:
ini
<!-- create-form/create-form.vue -->
<template>
<el-drawer
v-model="isShow"
direction="rtl"
:destroy-on-close="true"
:size="550"
>
<template #header>
<h2 class="title">{{ title }}</h2>
</template>
<schema-form
ref="schemaFormRef"
v-loading="loading"
:schema="components[name]?.schema"
/>
<template #footer>
<el-button type="primary" @click="save">{{ saveBtnText }}</el-button>
</template>
</el-drawer>
</template>
<script setup>
import { ref, inject } from "vue";
import { ElNotification } from "element-plus";
import $curl from "@/common/curl.js";
import SchemaForm from "@/widgets/schema-form/schema-form.vue";
// 注入顶层数据
const { api, components } = inject("schemaViewData");
const emit = defineEmits(["command"]);
const name = ref("createForm");
const schemaFormRef = ref(null);
const [isShow, loading, title, saveBtnText] = [ref(false), ref(false), ref(""), ref("")];
// 显示组件(供顶层调用)
const show = () => {
const { config } = components.value[name.value];
title.value = config.title;
saveBtnText.value = config.saveBtnText;
isShow.value = true;
};
// 保存逻辑(遵循REST规范)
const save = async () => {
if (loading.value || !schemaFormRef.value.validate()) return;
loading.value = true;
try {
const res = await $curl({
method: "post",
url: api.value,
data: schemaFormRef.value.getValue()
});
if (res.success) {
ElNotification({ title: "创建成功", type: "success" });
isShow.value = false;
emit("command", { event: "loadTableData" }); // 通知顶层刷新表格
}
} finally {
loading.value = false;
}
};
// 暴露show方法(供顶层调用)
defineExpose({ name, show });
</script>
3.4 底层渲染:schema-form字段控件
底层组件负责渲染具体的表单字段(输入框、下拉框等),通过form-item-config.js统一管理控件映射,支持自定义控件扩展:
css
// widgets/schema-form/form-item-config.js
import Input from "./components/input.vue";
import InputNumber from "./components/input-number.vue";
import Select from "./components/select.vue";
import DynamicSelect from "./components/dynamic-select.vue";
export default {
input: { component: Input },
inputNumber: { component: InputNumber },
select: { component: Select },
dynamicSelect: { component: DynamicSelect }
};
xml
<!-- widgets/schema-form/schema-form.vue -->
<template>
<el-row class="schema-form">
<template v-for="(fieldSchema, key) in schema.properties" :key="key">
<!-- 动态渲染字段控件 -->
<component
v-show="fieldSchema.option.visible !== false"
:is="FormItemConfig[fieldSchema.option.comType].component"
:schema-key="key"
:schema="fieldSchema"
v-model="formModel[key]"
/>
</template>
</el-row>
</template>
四、组件通信机制:跨层级的数据传递与事件联动
动态组件库中存在三层组件结构,通信需求主要分为两类:数据向下传递 (配置、API地址等)和事件向上传递 (保存成功、关闭组件等)。我们通过Vue的provide/inject和emit实现全链路通信,避免层层props传递的冗余。
4.1 数据向下传递:provide/inject穿透传递
顶层容器(schema-view)通过provide提供全局数据(API地址、组件配置等),中间层组件(如create-form)和底层组件(schema-form)通过inject直接获取,无需逐层传递:
js
// 顶层:schema-view.vue 提供数据
import { provide } from "vue";
import { useSchema } from "@/hooks/schema.js";
const { api, components } = useSchema();
provide("schemaGlobalData", {
api, // 基础API地址
components, // 动态组件配置
tableRefresh: () => { /* 表格刷新函数 */ } // 全局方法
});
// 中间层:create-form.vue 注入数据
import { inject } from "vue";
const { api, components } = inject("schemaGlobalData");
// 底层:schema-form.vue 注入数据(如需)
const { api } = inject("schemaGlobalData");
4.2 事件向上传递:emit + 暴露接口双机制
4.2.1 事件冒泡(emit)
中间层组件通过emit触发事件,顶层容器监听并处理(如保存成功后刷新表格):
js
// 中间层:create-form.vue 触发事件
const emit = defineEmits(["command"]);
const save = async () => {
// 保存逻辑...
emit("command", {
event: "tableRefresh", // 事件类型
params: { success: true } // 附加参数
});
};
// 顶层:schema-view.vue 监听事件
<component
v-for="(item, comName) in components"
:key="comName"
:is="ComponentConfig[comName]?.component"
@command="handleComponentCommand"
/>
const handleComponentCommand = ({ event, params }) => {
if (event === "tableRefresh") {
// 执行表格刷新逻辑
tableRefresh();
}
};
4.2.2 组件接口暴露(defineExpose)
顶层容器通过ref获取中间层组件的实例,直接调用组件的方法(如主动显示编辑表单并回显数据):
js
<component
const comRefs = ref({}); // 组件实例集合
// 编辑按钮点击:主动调用edit-form的show方法并传参
const handleEdit = (row) => {
const editCom = comRefs.value.editForm;
if (editCom) {
editCom.show({
data: row, // 回显数据(如product_id、product_name)
callback: () => { /* 编辑完成回调 */ }
});
}
};
</script>
<!-- 中间层:edit-form.vue 暴露show方法 -->
<script setup>
const show = (options) => {
const { data } = options;
// 数据回显到表单
Object.keys(data).forEach(key => {
formModel.value[key] = data[key];
});
isShow.value = true;
};
defineExpose({ show }); // 暴露方法给顶层调用
</script>
五、方案总结与扩展建议
- 配置驱动开发:通过 JSON 配置实现组件的动态渲染,新增业务页面无需编写组件代码,仅需配置schemaConfig;
- 分层解耦:三层组件结构职责清晰(顶层容器、中间层框架、底层控件),便于维护和扩展;
- 通信高效:结合provide/inject和emit,实现跨层级通信,避免冗余的 props 传递;
- 规范统一:API 交互遵循 REST 规范,组件配置遵循统一格式,降低团队协作成本。