在此前两三年的公司项目接触中,遇到过很多的项目基本都是以之前的一个基础项目演变来的。直接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 去实现。
参考:
抖音“哲玄前端”《大前端全栈实践》