[学习] 对于 elpis 全站框架核心设计的总结

514 阅读9分钟

声明

本文所述皆为个人拙见, 大家有意见欢迎在评论区留言, 有更好的设计也可以沟通交流, 欢迎一起讨论

俺还是个菜鸟, 不喜勿喷哈~

本文的设计和原项目不同

现状

当今的开发现状下, 对于后台管理系统, 大部分从事开发的人都在干 crud 的工作, 每天的工作内容就是对接产品开发接口

后端角度来看, 一个页面可能就对应一份数据, 不管这份数据分布在哪些库和表内, 它只需要对这些数据进行增删改查即可

前端角度来看, 一个页面就对应一系列增删改查的接口, 然后通过接口返回的数据进行不同的响应

归纳一下, 其实这些内容都是重复的, 有些甚至可以复制粘贴出来, 那为什么我们不能对这些内容进行一个抽象呢?

本文就从前端的角度来介绍这份抽象的设计

设计

我们先看看哪些部分是可以被复用的

这里我们假设我们有一个对于系统的描述, 这里我们称它为 dsl, 下面我们会一步一步的丰满这个 dsl

项目名
项目描述

页面

可能大家看过很多的后台管理系统, 他们的页面都是大同小异的, 一个菜单, 一块内容展示区域, 复杂一点的, 一个菜单, 一个子菜单, 一块内容展示区域

通过文字描述, 我们可以直接将抽离出 菜单内容展示区域 两个部分, 然后我们的 dsl 就变成下面这样

项目名
项目描述
菜单
内容展示区域

这样我们就完成了第一部分, 对页面内容的归纳, 接下来我们来细化一下菜单部分

菜单

上面的 dsl 只是对页面的一个归纳, 现在我们细化一下对菜单部分的描述

菜单下面会有很多的标签页, 对于标签页我们需要知道他的名称, 跳转地址等等

菜单
  标签页1
    标签名
    跳转地址
  标签页2
    标签名
    跳转地址
  ...

到目前为止, 我们已经暂时完成了对菜单的描述

内容展示区域

那对于内容展示区域呢? 我们其实并不知道内容展示区应该展示什么东西, 所以我们可以把这个能力直接暴露给用户, 让用户自己决定展示什么内容就好了

内容展示区域
  用户自定义内容

如果这么简单的就搞完了我们也就没啥必要去做这个设计了, 所以我们让他再丰满一点

咱们大家都看过很多的后台管理页面吧, 那她们的内容展示区域无非就是下面几种场景

  • 表格
  • 带搜索表单的表格
  • 图表
  • 数据展示

那我们可以直接预设这些场景的模板提供给用户, 让用户选择, 如果实在不满足需求, 再由用户决定是否自定义内容

内容展示区域
  表格页面
  带搜索表单的表格页面
  图表页面
  数据展示页面
  ...
  用户自定义内容

到此我们就完成了对内容展示区域的描述

菜单和内容展示区域的关联关系

上面我们只是单纯的描述了整个页面, 包括菜单和内容展示区域, 但是不同标签页需要对应不同的内容展示区域, 那这个我们怎么描述呢?

其实这里的描述是很开放的, 我们有很多中方式去实现这种数据关联关系的描述, 比如说, 我直接把内容展示区域嵌套在菜单中, 或者我在菜单中存放一个内容展示区域的 id, 这两种方式都可以让他们产生关联, 那我们这里就使用 id 的方式吧

咱们做前端的都知道, 一个 path 对应了一个页面, 他们是一对一的关系, 那其实我们可以直接用 path 来替代 id 作为我们的关联键

菜单
  标签页1
    标签名
    跳转地址 (path1)
  标签页2
    标签名
    跳转地址 (path2)

内容展示区域1 (path1)
内容展示区域2 (path2)

这样我们就可以描述出他们的对应关系了

内容展示区细化

到目前为止我们已经实现了, 对页面/菜单/还有标签页跳转逻辑的描述了, 咱们已经成功了一大部分了, 接下来我们细化一下内容展示区域

这里我们用 带搜索表单的表格页面 来举例子

在这个页面我们可以我们需要请求接口获取数据渲染页面, 然后可以有一些用户交互比如说, 新增/修改/删除表格的某一项, 搜索表格内容等, 我们就需要对这些操作进行一个描述

带搜索表单的表格页面
  接口列表
    查询列表接口
    新增列表项接口
    修改列表项接口
    删除列表项接口
  搜索表单
    搜索
      调用查询列表接口
    展示字段
      支持的搜索字段 1
        搜索参数名
        以什么方式搜索 (文本, 日期, 选择框等)
      支持的搜索字段 1
        搜索参数名
        以什么方式搜索 (文本, 日期, 选择框等)
  操作栏
    新增项
      弹出新增弹窗
      调用新增接口
  表格
    展示的列 1
      列名称
      数据来源
    展示的列 2
      列名称
      数据来源
    ...
    操作列
      修改项
        弹出修改弹窗
        调用修改接口
      删除项
        调用删除接口

上面这一大坨就是对这个带搜索表单的表格页面的一个描述了

大部分情况下, 我们搜索表单的字段信息和表格展示的字段信息是保持一致的, 也就是说我们可以直接把这部分的信息抽离出来

带搜索表单的表格页面
  接口列表
    查询列表接口
    新增列表项接口
    修改列表项接口
    删除列表项接口
  接口字段映射
    字段1
      搜索配置
        以什么方式搜索 (文本, 日期, 选择框等)
      表格配置
        列名
    字段2
      表格配置
        列名
  搜索表单
    搜索
      调用查询列表接口
  操作栏
    新增项
      弹出新增弹窗
      调用新增接口
  表格
    操作列配置
      修改项
        弹出修改弹窗
        调用修改接口
      删除项
        调用删除接口

这样我们就可以统一的去配置哪些接口字段可以用来搜索和展示了, 我们把这些字段相关的内容从不同板块的配置中抽离出来放到对接口字段的映射中来管理, 而原来的搜索表单和表格就配置一些与接口字段无关的内容 (是不是有一种高内聚的感觉了?)

到这里基本上已经完成了, 但是有些会思考的同学会想到, 这个接口列表好复杂, 我还得在每个用到接口的地方去配置调用哪个接口

诶, 有基础的同学就能想到了, 增删改查嘛, 那不是正好对应 RESTful API 规范嘛, 所以我们可以直接用这个规范去重新描述一下

带搜索表单的表格页面
  接口 (遵循 RESTful API 规范)
  接口字段映射
    字段1
      搜索配置
        以什么方式搜索 (文本, 日期, 选择框等)
      表格配置
        列名
    字段2
      表格配置
        列名
  搜索表单
    搜索
      // 无需手动配置 (GET) 接口
  操作栏
    新增项
      弹出新增弹窗
      // 无需手动配置 (POST) 接口
  表格
    操作列配置
      修改项
        弹出修改弹窗
        // 无需手动配置 (PATCH) 接口
      删除项
        调用删除接口
        // 无需手动配置 (DEL) 接口

这下好了, 我们只需要配置一个接口, 然后调用的时候就根据 RESTful API 规范去以不同方式调用接口即可

到目前为止我们已经实现了对于带搜索表单的表格页面的一个描述

数据结构

那上面的描述我们能看懂, 但是程序就不一定了, 所以这里我们就来把它转成程序能理解的数据结构吧 (一般是 json, 但是我懒得打双引号, 所以用 js 替代一下)

假如说我们现在需要一个后台管理系统, 名字叫 我不卖课

export default {
  name: '我不卖课',
  description: '我不卖课',
  menu: [
    { name: '商品管理', path: '/items' },
    { name: '课程管理', path: '/courses' },
    { name: '订单管理', path: '/orders' },
  ],
  pageContentMapping: {
    '/items': {
      template: 'tableWithSearchBar',
      api: '/api/item',
      apiFieldMapping: {
        id: {
          searchOption: { fieldType: 'text' },
        },
        name: {
          searchOption: { fieldType: 'text' },
          tableOption: { columnName: '商品名称' },
        },
        price: {
          tableOption: { columnName: '商品价格' },
        },
        createTime: {
          searchOption: { fieldType: 'dateRange' },
          tableOption: { columnName: '上架时间' },
        },
        inventory: {
          tableOption: { columnName: '库存' },
        },
        status: {
          searchOption: { fieldType: 'select' },
          tableOption: { columnName: '状态' },
        },
      },
      tableConfig: {
        itemOperation: {
          edit: { command: 'showEditModal' },
          delete: { command: 'deleteItem' },
        },
      },
    },
    '/courses': {
      template: 'iframe',
      url: 'https://example.com/courses/',
    },
    '/orders': {
      template: 'custom',
      path: '/pdd-orders',
    },
  },
};

上面这一坨呢就是很简单的描述了 我不卖课 这个后台管理系统的 dsl, 然后我们就可以通过解析这个 dsl 来生成对应的站点了

诶? 接口字段映射? 只能前端用吗?

正像标题描述的这样, 接口字段映射只能前端用吗? 好像, 大概, 可能不止吧, 后端也可以根据这个映射直接创建一张数据表吧 (当然已经有了那就当我没说咯~)

可是从现在这个数据结构来说, 好像没发生成数据表啊, 他还缺少好些东西, 比如数据类型? 那我们调整一下好了

export default {
  // 省略其他内容 ...
  pageContentMapping: {
    '/items': {
      // 省略其他内容 ...
      apiFieldMapping: {
        type: 'object',
        id: {
          type: 'string',
          searchOption: { fieldType: 'text' },
          // 数据库相关配置
          dbOption: { primaryKey: true },
        },
        name: {
          type: 'string',
          searchOption: { fieldType: 'text' },
          tableOption: { columnName: '商品名称' },
        },
        price: {
          type: 'number',
          tableOption: { columnName: '商品价格' },
        },
        createTime: {
          type: 'number',
          searchOption: { fieldType: 'dateRange' },
          tableOption: { columnName: '上架时间' },
        },
        inventory: {
          type: 'number',
          tableOption: { columnName: '库存' },
        },
        status: {
          type: 'string',
          // 字段枚举值
          enum: ['online', 'offline'],
          searchOption: { fieldType: 'select' },
          tableOption: { columnName: '状态' },
        },
        updateTime: {
          type: 'number',
          // 数据库相关配置
          dbOption: { autoUpdate: true },
        },
        required: ['id', 'name', 'price', 'createTime', 'inventory', 'status'],
      },
      dbConfig: {
        tableName: 'items',
      },
      // 省略其他内容 ...
    },
    // 省略其他内容 ...
  },
};

上面这坨呢就是我们调整过后的接口字段映射配置了, 它是基于 json schema 规范进行扩展的, 常用的基础 json schema 配置我们知道有 type, enum, required 等, 那我们在他之上扩展一些我们自己业务需要用到的 searchOption, tableOption, dbOption 之类的, 让他的功能更完善

这样我们就可以通过这个 dsl 来创建一张数据库表了, 什么表名啊, 字段类型啊, 是否必填啊, 自动更新啊, 主键啊, 外键啊之类的都可以在这个 dsl 中进行描述