如何快速搭建积木式搭建平台

2,255 阅读6分钟

一、前言

通过本篇文章你能快速了解如何搭建无代码平台
这里先说一下无代码与低代码平台的区别是:
定义:

  • 无代码开发平台指的是不需要借助任何代码进行开发,只需要托拉拽就能实习系统的开发,适合运营等不熟悉编程的人
  • 低代码开发平台则是运用少量最简单的代码就能进能完成程序的开发 使用场景:
  • 无代码开发平台非常适合构建针对特定场景,如问卷调查、首页内容、双十二活动等
  • 低代码开发平台不仅适用于特定的小型应用程序,可以更灵活地进行定制。

这张图的内容就具体的积木式搭建平台的内容,也是本篇文章讲解的重点

二、平台结构

我们能看到积木式搭建平台分成了4个部分,头部工具栏,左边物料库,中间展示区,右边动态修改内容区。

三、 左边物料库

物料库里统一存储着我们各种活动的组件库。但是因为我们的积木式搭建平台涉及到的活动和搭建场景很多样(比如:各自机构展示的首页内容、官网内容、营销活动页面、问卷调查页面、读书课程分析页面等等),每个场景活动都有自己特制的组件库,所以如何合理设计数据结构是很重要的。

大致数据结构如下:

        {
          id: 'xxx',
          title: '机构简介',
          type: 'OrgIntro',
          icon: 'httpxxxx.png',
          belong: ['home'],
          limit: 1,
          identity:3,
        },
        {
          id: 'xxx',
          title: '轮播图',
          type: 'Banner',
          icon: 'httpxxxx.png',
          belong: ['sitesEditor','questionnaire'...],
          limit: 'infinity',
          identity:1,
        },

我大致说一下整体结构:

  • title对应组件的标题

  • type就是每个物料的类型。就与我们中间展示区域中的component的type对应起来

    <component :is='item.type'></componet>
    
  • icon就是图标

  • belong就是代表了当前所属的活动场景。我们每个场景都有一个对应的type,比如这里的home就代表了首页,questionnaire代表了问卷类型等等。所以通过一个数组就可以去代表当前所属的活动场景,进行动态地决定显不显示该物料

    v-show="item.belong.includes(sitesPath)"
    

    item代表每个物料,sitesPath代表当前的活动场景。

  • limit是用来限制可以添加到中间展示区域的物料数量。因为有一些场景下我们并不希望重复添加某一个物料,所以通过limit可以很好地限制。
    这里原理就是通当把物料拖动到中间展示区域前,通过物料的type去判断中间展示区域里有几个相同的物料,并与limit进行比较就能进行限制了。

  • identity可以理解为用户等级。1~3代表了体验用户、普通会员、高级vip。而用户的等级不同对应的功能也就不同。比如:

    • 等级更高,limit可能就不限制了
    • 等级越高,用户可以拥有更高的定制化操作当前的组件
    • ...

三、 中间展示区

我们来看看如何渲染中间展示区:

      <draggable
        :disabled="isDisable"
        :list="items"
        :group="{ name: 'sites-editor', pull: false }"
        @add="onAdd"
        @sort="onSort"
      >
        <transition-group class="item-list" id='item-list'>
          <div
            :class="{ item: true, active: activeItemId === item.id }"
            v-for="(item, index) in items"
            :key="index"
            @click="activeItem(item)"
            @contextmenu.prevent="onContextmenu"
          >
              <component :is="item.type" :moduleProp="item"></component>
            	<item-action :index="index" :total="total" @doAction="doAction"></item-action>
          </div>
        </transition-group>
      </draggable>

分析如下:

3.1 vuedraggable

我们拖拽所采用vuedraggable库,是个很优秀的开源库。具体使用大家可以看看官网,这里就不多介绍了

3.2 动态组件component

渲染组件是采用vue中的动态组件component,依靠物料库里每个物料都有唯一的type去渲染对应的组件。

此外,推荐这里渲染的组件采用懒加载

  components: {
    OrgIntro: () => import('../modules/home/OrgIntro/Playground'),
    Banner: () => import('../modules/common/Banner/Playground'),
   // ...
  }

3.3 item-action 选中的组件

当我们点击的组件的时候,会出现右边的上移和下移以及删除的小图标(上移下移也可以通过拖拽的方式触发)

item-action代表当前选中的组件,能进行上移、下移和删除的操作。

  <div class="item-action-wrap">
    <a-icon type="up-circle" @click="$emit('doAction', 'up', index)" v-if="!isFirst" />
    <a-icon type="down-circle" @click="$emit('doAction', 'down', index)" v-if="!isLast" />
    <a-icon class="close" type="close-circle" @click="$emit('doAction', 'delete', index)" />
  </div>
<script>
  
export default {
  props: {
    index: {
      type: Number,
      default: 0,
    },
    total: {
      type: Number,
      default: 0,
    },
  },
  computed: {
    isFirst() {
      if (this.index === 0) {
        return true
      }
      return false
    },
    isLast() {
      if (this.index === this.total - 1) {
        return true
      }
      return false
    },
  },
}
</script>

分析:

  • isFirst 和 isLast是因为第一个组件和最后一个组件是不能进行上移和下移动的
  • 最后他们都会向外部派发一个事件doAction,这个函数作用:
    • 在外部进行删除或者移动的操作,删除和移动也不难,都是靠splice进行操作
    • 记录当前选中的组件对象右半部分的修改数据的内容就是在这里去传递的

3.4 onContextmenu

这里推荐一个vue-contextmenujs库,能很方便的去展示右键弹出菜单。具体写法如下

    onContextmenu(item,index) {
      this.$contextmenu({
        zIndex: 99999999,
        items: [
          {
            label: '置顶',
            onClick: () => this.onTop(index)
          },
          {
            label: '置后',
          	onClick: () => this.onBack(index)
          },
          {
            label: '复制',
            onClick: () => this.onCopy(item,index)
          },
          {
            label: '删除',
            onClick: () => this.onDelete(index)
          },
          {
            label: '粘贴',
            disabled:!(this.copyItem&&this.copyItem.type)
            onClick: () => this.onCopy(item,index)
          },
        ],
        event,
        customClass: 'class-a',
        minWidth: 230,
        zIndex:9999,
      })
      return false
    }

分析:

  • 这里置顶和置后和删除本质也是用splice方法去替换或删除组件

  • 复制需要对当前元素进行深拷贝

    this.copyItem = _.cloneDeep(item)
    

    这里的cloneDeep就是经典的库loadsh里的深拷贝方法

  • 粘贴的前提是已经复制了,所以根据是否有copyItem就能进行判断

四、右边内容修改区

右边部分就很简单了,currentEdit的生成就是靠在中间展示区里有提到过的doAction方法,当点击不同的组件时候currentEdit就是对应的不同组件,然后靠vue的动态组件去渲染对应的内容修改区域。

<component :is="`${currentEdit.type}RightSider`" :moduleProp="currentEdit"></component>

五、顶部工具栏

工具栏可以大大方便提高我们的效率,这里有下载模版、上传模版、撤销、前进、复制、删除、预览海报+保存海报的功能。我们来一个个看如何实现

5.1 撤销前进

首先要了解撤销前进的原理:

  • 撤销前进实际就是快照的原理。
  • 我们用一个数组去存储当前各种操作(这里涉及到排序,添加,删除,粘贴、置顶、置后)后的组件列表的数据
  • 然后依靠指针 index 去不断地+1 或者-1,不断地进行前进后退,拿到各种状态的数据进行重新赋值
  • 这里要注意一点: 就是当进行撤销后,又进行了新的操作后,这时候需要把原本撤销前的数据清空,改为新加入的操作后的列表数据

看看图解:

各种操作:

撤销前进:

当撤销后又进行各种操作后:


然后看看代码是如何实现的:

    const snapshot = []  // 快照数组
    let currentSnapshotIndex = -1 //快照索引
    
    //撤销
    undo() {
      if (this.currentSnapshotIndex >= 0) {
        this.currentSnapshotIndex--
        this.items = _.cloneDeep(this.snapshot[this.currentSnapshotIndex])
      }
    },
      
    //前进
    forward() {
      if (this.currentSnapshotIndex < this.snapshot.length - 1) {
        this.currentSnapshotIndex++
        this.items = _.cloneDeep(this.snapshot[this.currentSnapshotIndex])
      }
    },
      
    // 添加新的快照
    addSnapshot() {
      this.snapshot[++this.currentSnapshotIndex] = _.cloneDeep(this.items)
      if (this.currentSnapshotIndex < this.snapshot.length - 1) {
        this.snapshot = this.snapshot.slice(0, this.currentSnapshotIndex + 1)
      }
    }

分析:

  • 撤销功能是进行 currentSnapshotIndex 指针后退(-1)的操作,所以要考虑 currentSnapshotIndex 小于 0 的情况,当小于 0 说明目前是最早的操作了,不能在进行撤销了。

  • 而前进则是进行 currentSnapshotIndex 指针前进(+1)的操作,所以要考虑当前 currentSnapshotIndex 大于快照数组的长度,当大于的时候代表是最新的操作了。

  • addSnapshot 是当进行操作的时候(排序,添加,删除,粘贴、置顶、置后)为快照增加当前操作的记录,是撤销和前进的基础

  • 注意一下addSnapshot 中,当进行撤销后,又进行了新的操作后,这时候需要把撤销后的数据清空,所以当currentSnapshotIndex 的值小于当前的快照数量说明是经历过撤销操作了,要把之后的状态置空。所以只要记住addSnapshot添加快照永远是最新的操作就可以了。

5.2 导入导出

因为我们的平台是面向各种教育机构的 b 端用户,为了方便各种机构快速搭建页面,我们提供了各种搭建好的模版。如下图:

而这些模版的生成是按以下步骤

  1. 点击导出模版会导出json文件
  2. 点击保存海报会保存图片
  3. 到模版管理,把拿到的图片和json文件进行表单提交到数据库
  4. 模版库里就有对应模版海报和对应的json数据了

这些过程是不需要我们前端开发去配合的,靠运营人员就能按活动安排自行生成,大大解放了前端人员。


来看看导出的功能

    downTemplateJson() {
      const blob = new Blob([JSON.stringify(this.items)], { type: '' })
      saveAs(blob, 'template.json')
    }

分析:

  • 这里推荐一个很好用的库:file-saver。能在没有原生支持 saveAs() 的浏览器上实现了 saveAs() 接口,保存文件
  • 我们把当前展示的组件列表序列化,然后进行保存下载

接着看看导入的实现(有没有好用的gif软件呢?可以根据时间加马赛克的那种)

导入我是使用 ant-design 的 upload

    customRequest(date) {
      const reader = new FileReader()
      reader.onload = (e) => {
        const data = e.target.result
        this.items = JSON.parse(data)
      }
      reader.readAsText(date.file)
    }

代码也很简单就是读取文件内容

5.3 预览海报+保存海报

先看预览海报·:

预览海报和保存海报都是把中间的展示区域的内容生成图片,这需要用到canvas来实现。这里推荐采用html2canvas 库,它可以把 dom 元素生成对应的canvas。

我们先看看代码

    <a-modal
      :visible="imgPreviewSrc !== ''"
      @ok="savePoster"
      @cancel="() => (imgPreviewSrc = '')"
      width="500"
      title="海报展示"
      okText="保存海报"
    >
      <img :src="imgPreviewSrc" class="postImage" />
    </a-modal>
<script>
//...
  methods:{
    showPoster() {
      const itemListElement = document.getElementById('item-list')
      html2canvas(itemListElement, {
        dpi: window.devicePixelRatio,
        useCORS: true, // 开启跨域配置
        scale: 1,
      }).then((canvas) => {
        const url = canvas.toDataURL('image/png')
        this.imgPreviewSrc = url
      })
    },
  }

</script>

分析:

  • 通过html2canvas生成canvas

  • 再通过toDataURL转为base64进行展示

  • imgPreviewSrc有值的时候就会弹出显示框

  • 注意html2canvas有个bug,当设置display: -webkit-box;时候是不能显示元素内容的

    场景:我给一段文字设置了溢出显示省略号,这时候会不能显示元素内的内容

              h4 {
                color: #333333;
                width: 200px;
                display: -webkit-box;
                -webkit-box-orient: vertical;
                -webkit-line-clamp: 1;
                overflow: hidden;
              }
    

保存海报

可以看到当打开弹出框展示海报的时候,会有个保存海报的按钮savePoster

    savePoster() {
      saveAs(this.dataURLtoBlob(this.imgPreviewSrc), 'poster.png')
    }
    dataURLtoBlob(dataurl) {
      const arr = dataurl.split(',')
      const mime = arr[0].match(/:(.*?);/)[1]
      const bstr = atob(arr[1])
      let n = bstr.length
      const u8arr = new Uint8Array(n)
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n)
      }
      return new Blob([u8arr], { type: mime })
    }

分析:

  • dataURLtoBlob可以把base6转为blob
  • 然后进行拿到blob就是用file-saver进行导出图片了