零基础上手低代码,揭秘如何写一个页面设计器

352次阅读  |  发布于3年以前

前言

本文将带大家一起探索低代码开发的核心 —— 页面设计器

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

image.png

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

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

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

组件列表

首先,我们来看一下左侧的组件列表,列表中的每个组件,我们都需要使用一段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", id: 1 },
        { name: "组件2", id: 2 },
        { name: "组件3", id: 3 },
        { name: "组件4", id: 4 }
      ],
      list2: [
        { name: "画布组件1", id: 5 },
        { name: "画布组件2", id: 6 },
        { name: "画布组件3", id: 7 }
      ]
    };
  }
};
</script>
<style scoped>
.list-group-item {
  height: 30px;
  border: 1px solid #888888;
}
</style>
复制代码

呈现的样式如下图所示:

image.png

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

支持拖动的区域需要使用<draggable>组件进行包裹,<draggable>组可以添加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>
复制代码

2 . 画布中组件支持删除、复制、拖动操作

image.png

拖动

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

复制

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

删除

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

嵌套组件

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

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

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

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

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

<template>
    <div>
        <draggable class="dragArea list-group" :list="data.children" handle=".drag-icon"
            @add="handleAdd" @start="handleStart" @end="handleEnd">
            <slot>
            </slot>
        </draggable>
    </div>
</template>
复制代码

2 . 画布能够按照嵌套组件进行显示

对于无嵌套组件的页面,渲染时我们只需要对list2数组进行遍历渲染就可以了。但是具有嵌套组件的页面这种简单的for循环就无法满足了,我们需要对组件进行深层循环遍历。例如下面的实例代码。

export default {
  props: {
    data: Array,
  },
  methods: {
    renderTree (list, id) {
      return list.map((item) => {
        return (
          <content-item data={item} id={id} >
            {item.children && item.children.length ? this.renderTree(item.children, item.id) : null}
          </content-item>
        );
      });
    }
  },
  render: function (h) {
    return (
      <div>
        {this.renderTree(this.data)}
      </div>
    );
  }
};
复制代码

这样我们就扫清了拖动的一切障碍。下面,让我们把目光聚焦到页面设计器右侧的属性配置区域。

属性配置区域

其实到这里,很多小伙伴应该已经大致能够推理出属性配置后画布中组件根据配置进行显示的联动是如何进行的了。原理同样非常简单。我们可以对画布中的组件添加点击事件,当点击某个组件时,我们能够获取到当前点击组件的组件类型,例如输入框、下拉选择等等,针对每一种组件,我们已经提前在元数据中的props属性定义了这个组件能够进行动态控制的参数,我们只需要将这些参数以合适的表单形式展示在右侧的属性配置区域就可以了,例如,按钮组件的props中有一个text属性,用来控制按钮的显示文案,那么我们就在右侧属性控制区域用一个输入框来做为控制这个属性的表单形式,当修改数据时,我们找到list2中该组件所在的元数据对象,然后将该对象中props属性中text属性值修改为输入框中的内容。每个组件都会接受这个组件对应的元数据props参数,然后根据参数值进行渲染。例如按钮组件,现在按钮的文案时,我们可以使用props.text进行显示。

操作栏区域

从上面的文章中可以看出,一个页面实际就是用一段带有层级结构的json来进行描述的。

保存

保存时实际就是将这段json进行保存操作,我们可以将json存储到数据库中。

预览

在上面讲解画布区域时,我们已讲到组件如何通过json进行渲染。预览以及真实的页面渲染实际和画布中组件的展示实现原理完全一致。其中的区别有两点:(1)画布中的组件不支持交互操作,这里,我们需要屏蔽画布中组件的交互操作。我们可以通过css中的after伪类,设置content""来实现。(2)画布中的组件需要包裹一个div,这个div包含了复制、删除等功能。

其它功能

其实整个页面设计器的核心就是json,其它各种功能也都是围绕json进行。我相信大家仔细读完这篇文章,再看其它功能时也可以推断出其实现的原理。

后记

好啦,至此页面设计器的组件列表、画布和属性配置三个区域的联动我们就都实现了。目前,我们的页面设计器设计出的界面还是静态的,画布中的组件也都是独立而毫无关联的。在真实的业务场景,组件间的通讯是非常常见的。在后面的章节中,我们讲会重点介绍低代码中如何进行 组件间通讯 的配置。我们一起手拉手,搭建自己的低代码平台~!

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8