[study] 接上篇 DSL 动态组件拓展应用

74 阅读3分钟

前言

此文接上篇 [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) 的拓展了,如果有什么高见提出一起交流一下噢~~

学习资源

抖音-哲玄前端
大全栈实践课