动态组件设计(elpis)

一、动态组件的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             (文件)
相关推荐
得物技术6 小时前
从表单到 Agent:得物社区活动搭建的 AI 实践之路
人工智能·架构·agent
Ausra无忧6 小时前
记录在公司把单服务器升级成多服务器架构流程
前端·后端·架构
不好听6137 小时前
拆解 LLM Tool Use 的完整机制:从缸中大脑到 Agent 觉醒
架构·llm·agent
starsstreaming7 小时前
200K 的窗口,跑完 400K 的任务:Claude Code 上下文压缩机制全拆解
架构
禅思院8 小时前
前端部署“三层漏斗”完全指南:从CI/CD到自动回滚的工程化实战【基石】
前端·架构·前端框架
玉宇夕落1 天前
自注意力机制(Self-Attention Mechanism)简单学习一
架构