里程碑三:基于VUE3完成领域模型架构建设

322 阅读5分钟

引言

这个阶段应该是项目最核心的阶段,主要以领域模型(DSL),结合json-schema规范形成配置文件,通过解析引擎,根据配置生成相应的页面。

自于抖音“哲玄前端”课程(《大前端全栈实践课》)

一、领域模型

领域特定语言(Domain Specific Language)是为特定业务领域定制的语言及配套模型体系,通过声明式配置表达业务规则和界面逻辑,而非通用编程语言。而领域模型则是对业务领域的抽象表示,包含实体、规则和流程的标准化结构。

二、设计及作用

1、核心的设计理念:

(1). 配置驱动:通过一套基于json-schema规范生成的配置,渲染整个站点的页面组件

(2). 模型继承:基础配置可被多个项目继承和扩展,支持沉淀重复开发内容,同时也可以加入一些定制化内容

(3). 组件化:基于 Vue 3 的组件化架构,支持动态组件渲染

(4). 领域分离:按业务领域组织模型,衍生出不同类型的项目

2、作用

解决以下问题:

(1). 重复性 CRUD 开发,即业务模块的增删改查的复写及页面绘制等。

(2). 组件复用困难,不同页面中的相似组件功能差异大,难以复用

(3). 页面状态管理复杂,搜索条件、分页状态、选中状态等需要手动管理

(4). 开发效率低下 ,业务逻辑耦合程度高和 UI 样式重复,开发周期长

(5). 维护成本高,页面逻辑分散,修改功能需要改动多处代码

统一规范的配置驱动加上组件化,让手写页面变成配置生成页面,大幅减少crud的同时提高开发效率及代码质量

三、实现

1、文档(docs)

项目文档,其实也就是描述项目且没有具体配置的json-schema,作为一个基础的模板,后面的具体配置也就在这上面进行拓展

{
  mode: "dashboard", //模板类型,不用模板对应不一样的模板数据结构
  name: "", // 名称
  desc: "", // 描述
  icon: "", // 图标
  homePage: "", // 首页(项目配置)
  menu: [ //头部菜单
    {
      key: "", //菜单唯一描述
      name: "", //菜单名称
      menuType: "", //枚举值,group / module
      subMenu: [],  //当 menuType == group 时,可填
      moduleType: "", // 枚举值 sider/iframe/custom/schema
      siderConfig: { //当 menuType == sider 时
        menu: []  //侧边栏菜单
      },
      ifameConfig: { //当 menuType == iframe 时
        path: "", // iframe 路径
      },
      customConfig: { //当 menuType == custom 时
        path: "", //自定义路由路径
      },
      schemaConfig: {  //当 menuType == schema 时
        api: "", //数据源 API (遵循 RESTFUL 规范)
        schema: {  //板块数据结构
          type: "object",
          properties: {
            key: {
              ...schema, //标准 schema 配置
              type: "", //字段类型
              label: "", //字段的中文名
              tableOption: {  //字段在 table 中相关的配置
                ...elTableColumnConfig, // 标准 el-table-column 配置
                toFixed: 0, //保留小数点后几位
                visiable: true, // 默认为 true (false 时,表示不在表单中展示)
              },
              searchOption: {  // search-bar 中的相关配置
                ...elComponentConfig, //标准 el-component-column 配置
                comType: "", // 配置组件类型 input/select/.....
                default: "", // 默认值
                enumList: [], // comTyppe === 'select'时,下拉框可选项
                api: "",  //comType === 'dynamicSelect'时,请求的API
              },
            },...//可扩展
          },
        },
        tableConfig: {  // table 相关配置
          headerButton: [
            {
              label: "", // 按钮中文名
              eventKey: "", // 按钮事件名
              eventOption: {}, //按钮事件具体配置
              ...elButtonConfig, // 标准 el-button 配置
            },
          ],
          rowButton: [
            {
              label: "", // 按钮中文名
              eventKey: "", // 按钮事件名
              eventOption: {
                params: {//当event === remove
                  //paramsKey = 参数的键值
                  // rowVlaueKey = 参数值 (当格式为 schema:: tableKey 的时候,到 table 中找相应的字段)
                  paramsKey: rowVlaueKey,
                },
              }, //按钮事件具体配置
              ...elButtonConfig, // 标准 el-button 配置
            },... //可扩展
          ],
        },
        searchConfig: {}, // search-bar 相关配置
        componentsConfig: {}, // 模块组件
      },
    },
  ],
};

2、重载及继承基础模板

在上一部分的基础文档上进行填充,我们就会得到一份配置好的json-schema。当然需要抽取一个model作为初始模板,然后进行系统的差异化配置。有了两份配置那么就需要进行重载和继承,用loadsh融合成一份新的配置,重点注意数组的情况。

_.mergeWith({}, model, project, (modelValue, projValue) => {
    //处理数组合并的特殊情况
    if (Array.isArray(modelValue) && Array.isArray(projValue)) {
      let result = [];
      // 因为 project 继承 model,所以需要处理修改和新增内容的情况
      // project有的键值,model也有 => 修改(重载)
      // project有的键值, model没有 => 新增(拓展)
      // model有的键值,project没有 => 保留(继承)
      //处理修改
      for (let i = 0; i < modelValue.length; i++) {
        let modelItem = modelValue[i];
        const projItem = projValue.find(
          (projItem) => projItem.key === modelItem.key
        );
        result.push(
          projItem ? projectExtendModel(modelItem, projItem) : modelItem
        );
      }
      //处理新增
      for (let i = 0; i < projValue.length; i++) {
        const projItem = projValue[i];
        const modeItem = modelValue.find(
          (modelItem) => modelItem.key === projItem.key
        );
        if (!modeItem) {
          result.push(projItem);
        }
      }
      return result;
    }
  });

3、视图组件架构

领域模型设计.png

整体设计

(1) header-view: 头部菜单,支持项目切换和多级菜单

(2) sider-view: 侧边栏菜单,支持嵌套路由和默认选中

(3) schema-view: 配置驱动的CRUD页面核心组件

(4) iframe-view: 外部页面嵌入或微前端服务嵌入

(5) custom-view:自定义页面(拓展区域)

4、schema-view

(1) 、数据清洗(消除噪音)

除却菜单项、拓展及嵌入区域,就剩下CRUD页面核心组件区域,要根据配置项进行渲染,首先得对相应的项进行清洗,只取当前页面所需要的配置,如下:

// 配置分离和转换
const buildDtoSchema = (_schema, comName) => {
  const dtoSchema = { type: 'object', properties: {} }
  for (const key in _schema.properties) {
    const props = _schema.properties[key]
    if (props[`${comName}Option`]) {
      // 分离基础属性和组件配置
      let dtoProps = { option: props[`${comName}Option`] }
      dtoSchema.properties[key] = dtoProps
    }
  }
  return dtoSchema
}

(2)、schema-table

动态列渲染:

// schema-table.vue核心渲染逻辑
<template v-for="(schemaItem, key) in schema.properties">
  <el-table-column
    v-if="schemaItem.option.visible !== false"
    :key="key"
    :prop="key"
    :label="schemaItem.label"
    v-bind="schemaItem.option"  // 动态绑定所有配置
  />
</template>

(3)、schema-search-bar

上面的schema-table是根据配置进行渲染的,搜索项也得根据配置来,就需要根据配置中对应的字段进行动态组件渲染:

// search-item-config.js - 组件配置映射
const SearchItemConfig = {
  input: { component: input },
  select: { component: select },
  dynamicSelect: { component: dynamicSelect },
  dateRange: { component: dateRange }
}

// 动态组件渲染 - schema-search-bar.vue
<component
  :is="SearchItemConfig[schemaItem.option?.comType]?.component"
  :schema-key="key"
  :schema="schemaItem"
  @loaded="handleChildLoaded"
/>

五、性能优化

1、按需加载与路由分割:

routes.push({
  path: '/view/dashboard/schema',
  component: () => import('./complex-view/schema-view/schema-view.vue')
})

2、动态组件及扩展:

// SearchItemConfig组件单例模式
const SearchItemConfig = {
  input: { component: input },      // 组件实例复用
  select: { component: select },
  // 避免重复创建组件实例,提升渲染性能
  
  dateRange: { component: dateRange }, 
  dynamicSelect: { component: dynamicSelect },
  // 不需要修改核心代码,只需扩展即可使用
}