里程碑3 - 基于vue3完成领域模型架构建设

10 阅读7分钟

一、目标

基于设定好的DSL(Domain-Specific Language,领域特定语言) 模板,建立领域模型model,并衍生出各类project配置,通过service层解析到dashboard模板页中展示。

  • 为什么要使用DSL,而不是直接写代码?

    传统方式写代码,需要新增一个菜单的话:修改router -> 新增页面组件 -> 修改菜单

    DLS则是:修改配置 -> 读取DSL配置 -> 解析配置 -> 自动生成菜单

    使用DLS,开发人员只要进行修改配置,不用修改代码;后续操作会在系统启动时进行;开发人员从写代码变为写配置。

  • DSL的核心思想是什么?使用DSL的好处是什么?

    DSL的核心思想实际上就是 配置驱动,系统架构变成:DSL配置 -> 解析 -> 生成页面

    使用DSL的好处:提高开发效率、统一规范、增强系统可扩展性、减少代码量

  • 为什么要设计'model + project'这种继承结构,而不是每个项目一份完整配置?

    model是基础模板(通用配置),project是具体的项目配置(差异配置);这样设计的原因是为了避免重复配置

    如果是每个项目一份完整配置的话,会有大量的重复配置,且后续修改册成本极高

二、DSL配置解析

1. DSL模板代码

系统会根据这个配置自动生成菜单、页面、模块

{
  mode: 'dashboard';  // 模板类型,不同模板类型对应不一样的模板数据结构
  name: ''; // 名称
  desc: ''; // 描述
  icon: ''; // 图标
  homePage: ''; // 首页(项目配置)
  // 头部菜单
  menu: [
    {
      key: '', // 菜单唯一描述
      name: '', // 菜单名称
      menuType: '', // 枚举值 group(下拉框) / module(页面跳转/侧边栏)
      // 当 menuType 为 group 时,可填;可递归配置 menuItem
      subMenu: [],
      // 当 menuType 为 module 时,可填
      // 枚举值: iframe(三方页面) / custom(自定义页面) / schema(共通页面) / sider(侧边栏)
      moduleType: '',
      // 当 moduleType 为 sider 时,可填
      siderConfig: {
        menu: [],  // 除 moduleType 为 sider以外 可进行递归配置menuItem
      },
      // 当 moduleType 为 iframe 时,可填
      iframeConfig: {},
      // 当 moduleType 为 custom 时,可填
      customConfig: {},
      // 当 moduleType 为 schema 时,可填
      schemaConfig: {},
    }
  ];
}

2. DSL整体架构

   Dashboard
       │
       ├── 基础信息
       │      namedesciconhomePage
       │
       └── menu
               │
               ├── group
               │       └── subMenu // 下拉菜单
               │
               └── module
                       │
                       ├── iframe // 嵌入的第三方页面
                       ├── custom // 用户自定义页面
                       ├── schema // DSL页面
                       └── sider  // 侧边栏
                       

3. 运行流程

读取DSL、加载DSL配置 -> 解析model、peojct -> 配置继承 merge(model + project) -> 返回组织最终数据结构 -> 解析DSL结构 -> 渲染UI

三、动态配置

1. 动态生成菜单

通过 menuType 来区分菜单类型:

  • group

    下拉菜单

  • module

    可打开页面的菜单,点击后打开一个模块页面

    menu: [
        {
            key: 'user',
            name: '用户管理',
            menuType: 'module', // 菜单按钮、点击后跳转到特定菜单页面
            moduleType: '', // 菜单页面样式
        },
        {
            key: 'product',
            name: '商品管理',
            menuType: 'group', // 下拉框选择,选项为subMenu中的配置
            subMenu: [
                {
                  key: "productOne",
                  name: "商品1",
                  menuType: "module", // 菜单按钮、点击后跳转到特定菜单页面
                  moduleType: "", // 菜单页面样式
                }
            ]
        }
    ]

2. 动态生成页面

通过 moduleType 来区分菜单类型:

  • ifarame

    嵌入第三方页面,需要在 iframeConfig 中配置第三方页面的路径

  • custom

    自定义页面,加载一个真实的前端组件(DSL没法配置的复杂业务页面),需要在 customConfig 中配置具体路由

  • schema

    DSL页面,解析后生成的页面(比如表单、表格等),具体的信息需要在 schemaConfig 中进行配置

  • sider

    侧边栏模块,需要在 siderConfig 中配置侧边栏里面有哪些菜单

    menu: [
        {
            key: 'user',
            name: '用户管理',
            menuType: 'module', // 菜单按钮、点击后跳转到特定菜单页面
            moduleType: "custom",
            customConfig: {
              path: "" // 自定义页面 路由路径
            }
        },
        {
            key: "client",
            name: "客户管理",
            menuType: "module", // 菜单按钮、点击后跳转到特定菜单页面
            moduleType: "iframe",
            iframeConfig: {
              path: "" // iframe (三方页面)路径
            }
        },
        {
            key: 'product',
            name: '商品管理',
            menuType: 'module', // 菜单按钮、点击后跳转到特定菜单页面
            moduleType: 'schema', // DSL页面
            schemaConfig: {
              api: '',
              schema: {},
            }
        },
        {
            key: "client",
            name: "客户管理",
            menuType: "module", // 菜单按钮、点击后跳转到特定菜单页面
            moduleType: "sider", // 侧边栏
            siderConfig: {
              menu: [] // 侧边栏菜单配置
            }
        }
    ]

四、schemaConfig配置(⭐⭐⭐⭐⭐)

我们的schemaConfig可以理解为以下几部分:

schemaConfig
     │
     ├── api           数据接口
     ├── schema        数据结构 + UI规则
     │      │
     │      ├── type
     │      └── properties
     │              │
     │              ├── type
     │              ├── label
     │              └── xxxOption
     │
     └── xxxConfig
     
  • api

    数据源API(遵循 RESTFUL 规范)

  • schema

    遵循 JSON Schema 标准,在原有 JSON Schema 基础上扩展了UI配置能力

    JSON Schema原本用于 数据结构定义,扩展后变成 数据结构 + UI描述

    1. properties

      配置的是一个字段,系统会按需把它解析成 表格列、搜索条件

    2. xxxOption

      它可以是 tableOption,也可以是 searchOption;控制option在某个UI中是如何表现的

      tableOption:控制表格列,解析后将option中的配置绑定到 el-table-column上

      searchOption:搜索表单配置,通过 comType 来定义配置组件类型

  • xxxConfig

    控制整个UI模块,与schema中配置的xxxOption相呼应;下面这个例子是tableConfig中的配置:

        tableConfig: {
            // 表格顶部按钮展示
            headerButtons: [
                {
                  label: "新增商品",
                  eventKey: "showComponent", // 事件标识,点击按钮后触发某个事件
                  type: "primary",
                  plain: true
                }
            ],
            // 表格每一行按钮
            rowButtons: [
                {
                  label: "删除",
                  eventKey: "remove", // 触发事件
                  type: "danger",
                  eventOption: { // 事件参数
                    params: {
                      product_id: `schema::product_id`
                    }
                  }
                }
            ]
        }
    

五、扩展

1. Vue中的动态组件(Vue dynamic component)

动态组件代码逻辑:

  <component
    :is="SearchItemConfig[option?.comType]?.component"
    :ref="handleSearchComList"
    :schema-key="key"
    :schema="schemaItem"
    @loaded="handleChildLoaded"
  />
  • 为什么要使用动态组件来 加载 searchOption中的表单设置? 为什么不直接使用element-plus中的组件,通过v-if来展示?

使用v-if来展示的话,后续每增加一个组件,就要改一次代码,最终会变为巨型if结构。后续组件数量多起来了,v-if和动态组件的维护成本差距会非常大。

并且我们是由DSL配置来决定展示UI,在DSL中我们的comType已经是确定的了,只渲染某一个组件就行;但是v-if是代码决定UI,每次展示都要走一遍条件判断。

2. 懒加载

  • 什么是懒加载?为什么要做懒加载?

    懒加载的核心思想是需要的时候才加载资源,而不是页面一打开就加载所有代码;

    做懒加载的核心原因只有一个:减少首屏加载体积,缩短首屏加载时间;

    如果不做懒加载浏览器在打开首页的时候就要下载全部js,但是用户可能只访问其中的某个或某几个。

  • 懒加载的实现方式和实现前提

    懒加载不是浏览器自动实现的,而是需要两个前提:

    1. 动态 import

      Vue路由懒加载:component: () => import('./pages/user')

    2. 打包工具进行代码分包

      在编译后webpack会生成 user.chunk.js

  • 懒加载和代码分包的关系

    代码分包是构建阶段的行为,是把代码拆分成多个js文件

    懒加载是运行阶段的行为,是决定什么时候加载这些js文件

    懒加载必须要依赖代码分包,否则只有一个js文件,没办法实现懒加载

  • 浏览器是怎么加载懒加载文件的?

    懒加载实际上就是在运行时动态插入 script 标签,具体流程是:

    构建阶段,打包工具将代码拆开打包 -> 用户访问页面 -> main.js加载 -> 用户进入某个路由 -> 执行 import() -> webpack runtime执行 -> 创建script标签 -> 浏览器下载chunk -> 执行JS -> 页面渲染

六、总结

通过使用DSL领域模型,达到提高页面开发效率,减少重复代码,支持多业务项目复用(将重复工作进行整合)的目的;了解DSL解析机制和渲染机制。

同时我们的DSL配置采用model + project继承结构,在model中定义通用的页面结构,在project中定义具体业务项目配置;达到减少重复DSL的目的。

七、回顾优化

使用隐藏input注入数据

个人觉得这部分注入不是会在控制台中看到吗?

  <body style="margin: 0;">
    <div id="root"></div>
    <input id="env" value="{{ env }}" style="display: none;">
    <input id="options" value="{{ options }}" style="display: none;">
    <input id="projKey" value="{{ projKey }}" style="display: none;">
  </body>
  <script type="text/javascript">
    try {
      window.env = document.getElementById('env').value;
      window.projKey = document.getElementById('projKey').value;

      const options = document.getElementById('options').value;
      window.options = JSON.parse(options);
    } catch (e) {
      console.log(e);
    }
  </script>

可不可以在渲染页面的JS中直接将变量set到 window.__INIT_DATA__ 中 (待验证