手撸低代码平台搭建,揭秘页面设计器如何制作

前端瓶子君

共 5187字,需浏览 11分钟

 · 2021-11-17

点击上方 前端瓶子君,关注公众号

回复算法,加入前端编程面试算法每日一题群

前言

大家好,我是多肉攻城狮。我们在上一篇文章[2]中走进了低代码的世界,这一章节我们要开始干货内容了,来探索一下低代码开发的核心 —— 页面设计器

我们知道,低代码开发平台都是通过拖拉拽可视化的页面设计器进行页面开发的,在这一章节,我们来探索一下页面设计器的实现方式。下图中,我们截取了几款优秀的低代码产品的页面设计器界面。

image.png

可以看到,大多数的页面设计器都包含了如下所示的几个区域:

  • 最上方是操作栏,我们可进行页面的保存、预览、查看json信息、查看代码等操作;
  • 左侧是组件列表,当然也可以添加一些切换,让我们的左侧区域支持查看页面树信息、配置数据源等其他操作;
  • 中间是画布区域,我们可以将左侧的组件拖动到画布中,当然也支持画布中组件的赋值、删除等操作;
  • 右侧是属性配置区域,当我们在画布中选中某个组件时,可以在右侧的属性配置区域罗列出当前组件可支持动态配置的属性,修改了属性后可以在画布中看到对应组件的样式变化。

图片截取自宜搭,仅用于区域展示,与本篇内容无关

下面,我们按上述的区域划分来看一下页面设计器区域都是如何实现的。

组件列表

首先,我们来看一下左侧的组件列表,列表中的每个组件,我们都需要使用一段json来进行描述,这段json我们将它称之为 元数据,元数据中描述了当前组件的中文名称,在列表中显示的图标及描述,和组件可进行配置的一些动态属性。我们以输入框组件为例,它的元数据大致可以定义为如下的样子:

{
 code: "MyInput",
 name: "输入框",
 desc: "输入框的描述",
 icon: "input",
 props: {
  name: "字段名称",
  label: "label名称",
  labelCol: "",
  wrapperCol: "",
  required: false,
 }
}
复制代码

那么左侧的组件列表实际上就是这样的一个元数据对象组成的数组遍历而来的。

拖动

再来看一下将左侧组件列表的组件拖动到画布是如何实现的。拖动又分为顺序排列布局的拖动及自由布局拖动。顺序排列布局的拖动是指拖动到画布中的组件是自上而下顺序排列的,可以通过拖动调整上下顺序,当然我们也可以增加分栏这样的布局类型组件,实现组件的左右排列;自由布局拖动是指拖动到画布中的组件位置是自由的,我们松开鼠标的位置,就是这个组件在画布中的位置。考虑到我们主要服务的是B端项目,需要尽可能的使用户体验保持一致,这里呢我们采用的是顺序排列布局的拖动。这样用户拖动设计出的页面差异性不会太大,页面布局上又相对规整。

vuedraggable

拖动插件由于我们是vue技术栈,选选择了vuedraggable插件。像react技术栈也有类似的插件,大家很容易可以搜索到。对于vuedraggable组件的安装及说明这里我们就不赘述了,直接上demo。

<template>
  <a-row style="padding: 20px">
    <a-col span="10">
      <h3>列表区域h3>
      <draggable
        class="dragArea list-group"
        :list="list1"
        :sort="false"
        :group="{ name: 'people', pull: 'clone', put: false }"
      >

        <div class="list-group-item" v-for="element in list1" :key="element.name" >
          {{ element.name }}
        div>
      draggable>
    a-col>
    <a-col span="10" offset="4">
      <h3>目标区域h3>
      <draggable
        class="dragArea list-group"
        :list="list2"
        group="people"
      >

        <div class="list-group-item" v-for="element in list2" :key="element.name" >
          {{ element.name }}
        div>
      draggable>
    a-col>
  a-row>
template>
<script>
import draggable from "vuedraggable";
export default {
  components: {
    draggable
  },
  data() {
    return {
      list1: [
        { name"组件1"id1 },
        { name"组件2"id2 },
        { name"组件3"id3 },
        { name"组件4"id4 }
      ],
      list2: [
        { name"画布组件1"id5 },
        { name"画布组件2"id6 },
        { name"画布组件3"id7 }
      ]
    };
  }
};
script
>
<style scoped>
.list-group-item {
  height30px;
  border1px solid #888888;
}
style
>
复制代码

呈现的样式如下图所示:

image.png

上面的demo定义了两个区域,列表区域和目标区域,并定义了两个数组,list1list2。列表区域和目标区域分别使用list1数组和list2数组进行遍历渲染。当我们将列表区域的组件3拖动到目标区域时,我们打印list2变量的数据,就会 发现组件3被复制到了list2中,即复制到了目标区域。细心的小伙伴已经发现,唉!这不就是我们页面设计器组件拖动到画布中的实现方式嘛!是的,设计器中的拖动原理就是这样简单。

支持拖动的区域需要使用组件进行包裹,组可以添加onAddonStartonEndmove事件回调函数,我们可以在这些事件中添加一些我们需要的逻辑。例如,我们可以在onAdd函数中对我们添加到list2数组列表中的对象动态的添加一个唯一值id,用于我们区分同一个页面拖入两个相同组件的情况。

下面让我们对上面的简单demo稍加改动:

列表区域

  1. 我们将list1数组的中的每一项修改为我们之前定义好的组件元数据。
  2. 可以将我们定义在元数据中的图标显示在组件列表中,方便我们快速识别出想要拖动到画布中的组件。

画布区域

  1. 将画布中的组件列表渲染为真实的组件

我们现在知道,画布中的列表实际也是通过组件元数据数组进行渲染的,而每个原数据项都对应了一个真实的组件,这样我们只需要将元数据项替换成UI组件进行渲染就可以了。在代码中大致是如下的样子:

<template>
    <div v-for="item in list2" :key="item.id">
        <my-input v-if="item.code === 'MyInput'" :data="item"/>
        <my-select v-if="item.code === 'MySelect'" :data="item"/>
        ...
    div>
template>
复制代码

哈哈,有的小伙伴已经看出来了,这样写不太优雅,我们用动态组件优化一下。

<template>
    <div v-for="item in list2" :key="item.id">
        <component
            :data="item"
            :is="item.code"
        />

    div>
template>
复制代码
  1. 画布中组件支持删除、复制、拖动操作
image.png

拖动

我们的demo示例中,目标区域list2是支持可拖动排序的

复制

我们已经知道,画布中的组件是通过list2遍历渲染出的。当点击复制操作时,只需要将当前被点击复制按钮的组件所对应的元数据添加到list2中就可以了。这里需要注意,在复制元数据的时候,我们需要将id属性值进行累加计算,这样才能区分被复制的组件和复制生成的组件。

删除

同理,删除操作,我们只需要将list2中的组件通过被复制组件id进行过滤就可以了。

嵌套组件

到这里,有些小伙伴可能有些疑问,目前。组件拖动到画布进行显示已经问题不大了。那么组件中是否可以再拖入组件,就像我们在vue编程中进行组件的嵌套一样呢?通过一些优秀低代码产品,我们可以发现,他们组件列表都是进行分类显示的,布局类组件就是这样一类可以在组件内部再进行拖动的组件了。这类组件包括栅格组件、容器组件、多页签组件、卡片组件等。我们知道,list2就是最终页面渲染的组件列表,它是一个对象数组的数据结构,为了让它支持嵌套组件,我们需要在组件的元数据对象上增加一个属性,这个属性用来描述该组件下又嵌套了哪些组件,我们就命名这个属性为children。那么,包含嵌套组件的页面数据大致就是下面所示的样子。

[{
 code: "MyCard",
 name: "卡片",
 props: {
  ...卡片组件相关配置属性
 },
 children: [{
  code: "MyContainer",
  name: "容器",
  props: {
   ...容器组件相关配置属性
  },
  children: [{
   ....
  }]
 }]
}]
复制代码

为了满足嵌套组件的需求,我们需要做两个方面的调整。

  1. 布局类的组件能够继续拖入组件

我们在容器类组件内部再次引用组件,组件的list参数值为容器组件元数据的children数组,然后在draggable组件内部使用插槽将children进行渲染。容器组件的模板大致是下面的样子