基于Vue3完成动态组件库建设

上一阶段我们基于 DSL 配置实现了schema-view页面的自动化渲染,核心解决了重复性页面开发的效率问题。但在实际业务中,新增、编辑、删除、查看等交互操作仍需手动开发组件,导致开发流程割裂。本阶段的核心目标是: schema-view 基础上,通过统一配置实现交互组件的动态加载、渲染与通信,让整个页面体系真正实现 "配置即开发"。

本文将围绕三个核心问题展开:

  1. 如何设计合理的配置体系,描述组件的结构、触发方式与字段属性?
  1. 如何解析配置并提取有效信息,为动态渲染提供数据支撑?
  1. 如何实现组件的分层渲染与跨层级通信,保证扩展性与可维护性?

一、配置体系设计:用 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)。

配置设计总结

整个配置体系分为三层,层层递进:

  1. 组件层(componentConfig):描述组件 "是什么";
  1. 触发层(tableConfig):描述组件 "何时触发";
  1. 字段层(properties):描述组件 "包含什么";
  1. 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/injectemit实现全链路通信,避免层层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>

五、方案总结与扩展建议​

  1. 配置驱动开发:通过 JSON 配置实现组件的动态渲染,新增业务页面无需编写组件代码,仅需配置schemaConfig;
  1. 分层解耦:三层组件结构职责清晰(顶层容器、中间层框架、底层控件),便于维护和扩展;
  1. 通信高效:结合provide/inject和emit,实现跨层级通信,避免冗余的 props 传递;
  1. 规范统一:API 交互遵循 REST 规范,组件配置遵循统一格式,降低团队协作成本。
相关推荐
m0_7400437337 分钟前
Vue 组件中获取 Vuex state 数据的三种核心方式
前端·javascript·vue.js
爱吃香菜i39 分钟前
基于Vant的移动端公共选人/选部门组件设计文档
前端
Jingyou42 分钟前
JavaScript 封装无感 token 刷新
前端·javascript
quan26311 小时前
20251204,vue列表实现自定义筛选和列
前端·vue.js·elementui
蜗牛攻城狮1 小时前
JavaScript `Array.prototype.reduce()` 的妙用:不只是求和!
前端·javascript·数组
一入程序无退路1 小时前
若依框架导出显示中文,而不是数字
java·服务器·前端
m0_626535201 小时前
代码分析 关于看图像是否包括损坏
java·前端·javascript
wangbing11251 小时前
layer.open打开的jsf页面刷新问题
前端
Mintopia1 小时前
🌏 父子组件 i18n(国际化)架构设计方案
前端·架构·前端工程化