Elpis 框架设计

112 阅读4分钟

      在此前两三年的公司项目接触中,遇到过很多的项目基本都是以之前的一个基础项目演变来的。直接copy一份改改改,包括刚接触到公司老项目的时候,里面有许多重复的组件,例如table组件,每次多一个功能去迭代就封装一个新的table组件,我接手过的table组件就有二十多个,迭代真的是在做无用功,时常会去想这些内容不能体现在一个组件上吗,定制化内容利用插槽等方式去拓展就可以了。这样的系统大大小小七八个,后面提出需用一个管理平台去管理这些系统。相信有些同学是遇到过这些问题的,都在做cv工程师,干一些重复性的活,完全学不到任何知识,对自己也没有任何提升。

       针对于以上问题,我就思考可以通过传入一个配置去渲染出想要的table组件吗?这样就可以避免后续相关同学再次接手这个项目的时候无脑cv去更改。后面偶然时间在抖音上看到“哲玄前端”《大前端全栈实践》这门课,我发现其中设计思路与我之前想到的更完整更具有拓展性。于是有了 Elpis 框架,这个框架就是多页面后台管理平台。其中涉及思路就是通过一份配置去渲染出相应的站点统一管理起来。这份配置满足 json-schema 规范,语义通俗易懂,也方便代码解析做相应处理。

配置文档DSL并解析

       通过去解析配置文档去生成相应的站点内容。其主要渲染核心配置就是 schemaConfig 中 schema 内容,大部分重复工作的增删改查内容都将用一份配置去完成其中 tableOption 配置去配置表格相关内容;同理也有 searchOption 配置去配置哪些字段在表格中可搜索,用什么组件,利用动态组件去渲染,tableConfig 则是处理相关操作事件配置......这样一套配置下来关于基础表格呈现搜索的内容就可以渲染出来,当然后续也会有相关配置去配置 API、数据库等信息**(apiOption、dbOption等)**也是用同样的逻辑去处理。

配置 DSL

{  mode: 'dashboard'; // 模板类型,不同模板类型对应不一样的模板数据结构  name: ''; // 名称  desc: ''; // 描述  icon: ''; // 图标  homepage: ''; // 首页(项目配置)  // 头部菜单  menu: [    {      key: '', // 菜单唯一标识      name: '', // 菜单名称      menuType: '', // 枚举值 group / module      // ---> 当 menuType == ‘group’ , 存在子菜单      subMenu: [        {          // 可递归 menuItem        },      ],      // ---> 当 menuType == ‘module’, 存在模块      moduleType: '', // 枚举值 sider / iframe / custom / schema      // 当 moduleType == ‘sider’      siderConfig: {        menu: [          {            // 可递归 menuItem          },        ],      },      // 当 moduleType == ‘iframe’      iframeConfig: {        path: '', // iframe 路径      },      // 当 moduleType == ‘custom’      customConfig: {        path: '', // 自定义路径      },      // 当 moduleType == ‘schema’      schemaConfig: {        api: '', // 数据源 API(遵循 RESTFUL 规范)        schema: {          // 板块数据结构          type: 'object',          properties: {            key: {              ...schema, // 标准 schema 配置              type: '', // 字段类型              label: '', // 字段名称              // 字段在 table 中的配置              tableOption: {                ...elTableColumnConfig, // 标准 el-table-column 配置                toFixed: 0, // 保留小数位,默认为 0                visiable: true, // 是否显示,默认为 true(false时,表示不在表单中显示)              },              // 字段在 search-bar 中的配置              searchOption: {                ...eleComponentConfig, // 标准 el-coponent-column 配置                comType: '', // 配置组件类型 input / selct ...                default: '', // 默认值                // comType == 'select'                enumList: [], // 下拉框枚举值                // comType =='dynamicSelect'                api: '',              },
              ...             },          },        },        // table 相关配置        tableConfig: {          headerButtons: [            {              label: '', // 按钮名称              eventKey: '', // 按钮事件名              eventOption: {}, // 按钮事件具体配置              ...elButtonConfig, // 标准 el-button 配置            },          ],          rowButtons: [            {              label: '', // 按钮名称              eventKey: '', // 按钮事件名              // 按钮事件具体配置              eventOption: {                // 当 eventKey == ‘remove’                params: {                  // paramKey = 调用 api 时传的参数键值                  // rowValueKey = 被传参的键值格式,格式为 schema::tableKey,到 table 中找相应的字段                  paramKey: rowValueKey,                },              },              ...elButtonConfig, // 标准 el-button 配置            },          ],        },        // search-bar 相关配置        searchConfig: {},        // 模块组件        components: {},
        ...       },    },  ];}

DSL解析处理(例如处理schema-view)

export const useSchema = () => {  const route = useRoute();  const menuStore = useMenuStore();  const api = ref('');  const tableSchema = ref({});  const tableConfig = ref({});  const searchSchema = ref({});  const searchConfig = 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));      api.value = sConfig.api ?? '';      tableSchema.value = {};      tableConfig.value = undefined;      searchSchema.value = {};      searchConfig.value = undefined;      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;      });    }  };  // 通用构建 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 k in props) {          if (k.indexOf('Option') < 0) {            dtoProps[k] = props[k];          }        }        dtoProps = Object.assign({}, dtoProps, {          option: props[`${comName}Option`],        });        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,  };};

       通过 schema.js 去解析配置文档中提取的配置信息,再将数据拿到组件上去进行渲染,这样一来无论是以往重复性的增删改查的工作,还是特定需求的定制都可以通过配置 DSL 去实现。

参考:
抖音“哲玄前端”《大前端全栈实践》