前端低代码平台实现思路-数据格式

2020年做了一个代码生成小工具,虽然用的人不多,不过也是自己从零到一搞出来的,最近也在看低代码平台的一些竞品,所以现在想一想,有必要把自己的一些实现思路捋一捋。其实网上一搜低代码平台什么的,有很多例子,不过我这里仅仅把自己走过的坑列出来,避免其他人再走了。

小玩具

这次主要是讲数据格式,为啥把这个放在前面,因为这个是决定你后面整体的实现方案和后续扩展,如果数据格式没有定义好,那后面升级扩展可都是大问题了。其实这个格式的定义,我前前后后推翻了3次,就是考虑到后面的扩展升级问题。

简单粗暴型

第一次数据格式定义是很简单的扁平的,但是后面发现做到组件的嵌套和插槽的嵌套,那就没办法实现了,所以第一次就这么失败了,这里给一个低代码平台的json格式,和我当时定义的非常类似。

{
  "type": "input",
  "icon": "icon-input",
  "options": {
    "width": "100%",
    "defaultValue": "",
    "required": false,
    "placeholder": "",
    "size": "",
    "disabled": false
  },
  "name": "单行文本",
  "key": "1643163772265",
  "model": "input_1643163772265",
  "rules": []
}

这么定义,虽然简单明了,但是后面嵌套就实现不了,而且他这个数据格式的定义,其实就只能实现表单这种方式(并且是把饿了么组件在平台上面又封装了一遍),没有考虑嵌套问题。如果仅仅实现动态表单,这种的倒也没问题。但是我觉得把组件在平台上面封装一下,也不够优雅,后面平台想要支持业务组件、开源组件那不得挨个封装,频繁修改平台代码啊……

面面俱到型

第二次封装,开始考虑到嵌套问题,然后json结构类似下面的格式:

{
  "id": "",
  "components": "g-button-group",
  "classify": "basic",
  "tag": "el-button-group",
  "title": "按钮组",
  "props": {
    "class": {
      "key": "class",
      "label": "class类名",
      "value": "",
      "el": "input",
      "type": "string"
    }
  },
  "children": [
    {
      "tag": "el-button",
      "props": {
        "type": {
          "key": "type",
          "label": "按钮类型",
          "value": "undefined",
          "el": "select",
          "type": "string",
          "options": [
            {
              "value": "",
              "label": "默认"
            },
            {
              "value": "primary",
              "label": "重要"
            },
            {
              "value": "success",
              "label": "成功"
            },
            {
              "value": "warning",
              "label": "警告"
            },
            {
              "value": "danger",
              "label": "危险"
            },
            {
              "value": "info",
              "label": "提示"
            },
            {
              "value": "text",
              "label": "文本"
            }
          ]
        },
        "plain": {
          "key": "plain",
          "label": "是否朴素",
          "value": "undefined",
          "el": "switch",
          "type": "boolean"
        },
        "round": {
          "key": "round",
          "label": "是否圆角",
          "value": "undefined",
          "el": "switch",
          "type": "boolean"
        }
      },
      "children": [
        {
          "tag": "",
          "props": {},
          "text": "默认按钮"
        }
      ]
    },
    {
      "tag": "el-button",
      "props": {
        "type": {
          "key": "type",
          "label": "按钮类型",
          "value": "undefined",
          "el": "select",
          "type": "string",
          "options": [
            {
              "value": "",
              "label": "默认"
            },
            {
              "value": "primary",
              "label": "重要"
            },
            {
              "value": "success",
              "label": "成功"
            },
            {
              "value": "warning",
              "label": "警告"
            },
            {
              "value": "danger",
              "label": "危险"
            },
            {
              "value": "info",
              "label": "提示"
            },
            {
              "value": "text",
              "label": "文本"
            }
          ]
        },
        "plain": {
          "key": "plain",
          "label": "是否朴素",
          "value": "undefined",
          "el": "switch",
          "type": "boolean"
        },
        "round": {
          "key": "round",
          "label": "是否圆角",
          "value": "undefined",
          "el": "switch",
          "type": "boolean"
        }
      },
      "children": [
        {
          "tag": "",
          "props": {},
          "text": "默认按钮"
        }
      ]
    }
  ]
}

这次是考虑了嵌套问题,但是后面发现,用这种嵌套格式也是有很大的问题,因为当时考虑所有的嵌套都是用children这个来去定义的,那就没办法区分以下几种嵌套了:

  1. 插槽和作用域插槽嵌套;
  2. 组件默认的有嵌套,例如页签和表格

所以这次定义的数据格式又失败了,关键这次定义的数据格式里面还带上了一些描述属性的信息,这些其实对于组件的渲染是没必要的,数据也有冗余,后面组件属性变化,之前保持的数据是没办法继承。

精简全能格式

结合之前的两次失败经验,然后根据 Vue 深入数据对象 中的定义查看了一下,这次我又重新的定义了一下,看看这次的格式:

{
  "id": "000",
  "title": "html",
  "tag": "div",
  "style": {
    "color": "red",
    "border": "1px dashed red",
    "padding": "10px"
  },
  "domProps": {
    "innerHTML": "任意html文本"
  }
},
{
  "id": "1000",
  "title": "iframe",
  "tag": "iframe",
  "attrs": {
    "src": "https://wangdaodao.com",
    "width": "100%",
    "height": "200px"
  }
},
{
  "id": "100",
  "title": "img",
  "tag": "img",
  "attrs": {
    "src": "https://www.runoob.com/wp-content/uploads/2017/01/vue.png",
    "width": "200px",
    "height": "200px"
  }
},
{
  "id": "001",
  "title": "文字链接",
  "tag": "el-link",
  "props": {
    "href": "https://github.com/",
    "target": "_blank",
    "type": "primary"
  },
  "slots": [
    {
      "name": "default",
      "tag": "template",
      "children": [
        {
          "title": "html",
          "tag": "span",
          "domProps": {
            "innerHTML": "任意html文本"
          }
        }
      ]
    }
  ]
},
{
  "id": "033",
  "key": "input-33",
  "title": "复合输入框",
  "tag": "el-input",
  "ref": "a-input",
  "props": {
    "value": ""
  },
  "slots": [
    {
      "name": "prepend",
      "tag": "template",
      "children": [
        {
          "tag": "el-select",
          "title": "选择器",
          "props": {
            "value": "",
            "size": "medium",
            "clearable": true
          },
          "style": {
            "width": "100px"
          },
          "subTag": [
            {
              "tag": "el-option",
              "props": {
                "label": "选择器a",
                "value": "a"
              }
            },
            {
              "tag": "el-option",
              "props": {
                "label": "选择器b",
                "value": "b"
              }
            },
            {
              "tag": "el-option",
              "props": {
                "label": "选择器c",
                "value": "c"
              }
            }
          ]
        }
      ]
    },
    {
      "name": "append",
      "tag": "template",
      "children": [
        {
          "id": "4",
          "title": "按钮",
          "tag": "el-button",
          "props": {
            "icon": "el-icon-search"
          },
          "slots": [
            {
              "name": "default",
              "tag": "template",
              "children": [
                {
                  "title": "html",
                  "tag": "span",
                  "domProps": {
                    "innerHTML": "搜索"
                  }
                }
              ]
            }
          ]
        }
      ]
    }
  ],
  "events": [
    {
      "name": "blur",
      "function": "console.log('失去焦点时触发')"
    },
    {
      "name": "focus",
      "function": "console.log('获得焦点时触发')"
    },
    {
      "name": "change",
      "function": "console.log('change触发')"
    }
  ]
}

这次定义的数据格式,基本上可以满足各种嵌套和组件的描述。各种的嵌套、常规的具名匿名插槽、表格的作用域插槽、标准的html组件、组件的自定义方法都是可以通过这套json去描述的。

2022-02-26T11:11:39.png

2022-02-26T11:11:58.png

2022-02-26T11:12:22.png

2022-03-17T06:09:51.png

这次定义的格式,其实还考虑了后面组件属性的升级维护,这个我会在后面把整体的架构图画好了,再详细的说一下,为啥要这么设计。这次先把数据格式定义放这里,抛砖引玉的把现在我想到的数据格式的定义整理出来,如果有更好的描述,可以留言讨论哈,我觉得我这里应该还有一些没有考虑到的地方。

最后放下篇文章用到的知识点,可以提前看一看:
动态组件:https://cn.vuejs.org/v2/guide/components-dynamic-async.html
渲染函数:https://cn.vuejs.org/v2/guide/render-function.html

添加新评论