H5页面可视化平台构建 - childlabor/blog GitHub Wiki

前言

页面可视化搭建,是用可视化交互的方式直观的生成页面。使用者只需简单的几个点击、拖拽操作就能定制出需要的页面。 很好的解决页面生成的效率问题,同时也降低了非开发人员的使用门槛。

目前,主流的页面可视化搭建主要有两种,一种是pc端搭建用于展示数据的图表可视化dashboard,一种是定制H5页面。相比传统的直接输出html代码的方式,以数据驱动,动态加载组件的方式更加优雅,易于解耦和维护。

篇幅有限,本文只分模块的列举实现平台功能的一些点,拓展内容,有兴趣的同学请自行 百度 Goooooogle。以下所有例子都基于vue开发(其他框架实现也大同小异),所贴代码仅为示例代码块,完整代码请到demo仓库

组件化思想

可视化搭建平台一大特点就是能提高资源、组件及代码的复用率,提升整体研发效率。而这一切的基础就是组件。我们在页面上看到的所有元素都可以分离为一个组件,万物皆组件。组件就是一块砖,哪里需要哪里搬,组件的细化程度直接影响了页面的可定制化程度。因此,实现功能的基础就是在平台外必须有个组件库,供平台调用。

我们可以把组件划分为容器组件、基础组件、功能组件、业务组件等等。其实,在平时开发项目时,我们也会不同程度的将各种功能组件化,但是因为没有一个统一的管理,组件过度偏重于特定业务,在多人协作的项目或者是历史项目中,我们可能因为不知道有这个可以使用的组件,也可能因为接入旧组件成本过大(不够灵活),从而未复用组件又去重复开发了一遍既有功能。组件的复用率没有达到理想的状态,也就失去了编写组件的一大意义。

因此,为了方便管理和接入的灵活性,组件库的信息需要有个完整的数据做统一管理,每次的增删改都要做数据同步。

// 示例 组件信息 数据结构根据实际需求定义
const componentsBase = [{
   type: 1,
   id: 1,
   name: "banner",
   show:"轮播组件",
   desc: "描述文档",
   props: "...", // 可配置项
   msg: "..."
   ...
},
...
]

组件编写需要满足:

  • 风格统一
  • 灵活性,开放接口,方便接入(props、slot)
  • 独立性
  • 组合性(容器组件 + n个基础组件 = 新业务组件)

拖拽交互

为了更好的交互效果,需要拖拽功能。目前社区有很多成熟的拖拽相关的库,我们选用了vuedraggable。

vuedraggable是个强大且易用的基于Sortable.js封装的vue拖拽组件库。

具体实现效果可以查看官方的demo

中后台系统

接下来介绍核心内容,这部分也就是我们直观的操作页面。按操作习惯一般分为三个区块。

  • 左侧:组件列表区(组件名称列表 点选、拖拽...)
    • 根据组件库信息数据渲染标签,每个标签(代表一个组件)附带组件基本信息数据。
  • 中间:视窗区(实时预览、删除、移动...)
    • 组件树渲染可以使用递归算法,组件用slot插槽嵌套
  • 右侧:配置区(组件样式、文本、布局设置...)
    • 细化功能(每一个样式修改按钮为一个组件),选中视窗区的组件后加载显示目标组件能配置的项(样式)

功能迭代多了后,三大区块及组件间的数据交互会增多,特别是一些递归调用。因此,建议引入vuex集中管理状态。

各区域的数据传递

平台每个区域都是一个组件,每个区域又是由多个组件组成,因此,需要有一套统一的数据来管理所有的状态。

// 示例

// 组件列表区 
// 通过组件名`name`来渲染组件标签按钮,数组的一个对象就对应了一种组件。拖拽渲染时,将对象数据传递到视窗区  
componentsList: [
    {
        name: "HelloWorld",
        id: 1,
        property: {
            msg: '123',
            styleObject: {
                background: '#fff',
            }
        },
        // 有此字段的内部才能嵌套子组件
        childs: []
    }, {
        name: "Title",
        id: 2,
        property: {
            msg: '标题',
            styleObject: {
              color: 'red',
              fontSize: '14px'
            }
        }
    },
]

// 视窗区 
// 接收组件列表区传递的数据合并入数组渲染出预览组件。因为一种组件可能多次使用,因此传递时需要添加个唯一的标识,此处为`renderId`
componentsRender: [
    {
      renderId: '0', // 渲染id
      name: "TopTitle",
      id: 100,
      property: {
        msg: '',
        styleObject: {
          color: 'black',
          fontSize: '18px',
          background: '#f2f2f2',
          padding: '10px'
        }
      },
      childs: [
        {
          renderId: '1',
          name: "Title",
          id: 2,
          property: {
            msg: '标题改',
            styleObject: {
              color: 'red',
              fontSize: '14px'
            }
          },
          childs: []
        },
      ]
    },
]
 
// 配置区
// 根据当前目标组件(抽象为componentsRender的某一项数据)的`property`来动态加载配置项组件
requireOptionsComps() {
  this.activeComponentsOptions.forEach(item => {
    this.$set(item, 'comps',
      function(resolve) {
        require([`../components/options/${item.name}.vue`], resolve)
      }
    )
  })
},

平台的所有操作基本上都是在对上述数据的增删改查,最终,我们只要输出视窗区的componentsRender数据,作为页面组件渲染的原始数据。

实时预览(可编辑)

有了数据我们就可以渲染组件了。下面我们来对比下视窗区实时预览的两种实现方式:

  1. 页面挂载:项目需要需引入组件库,提供组件库组件的渲染环境(安装相同的依赖包), 需要时刻关注着组件库的变化同步更新。但是,此方式实时渲染速度快,能很好的实现编辑功能。
// 用拖拽框将组件包裹,尽量减少无关标签
<div class="drop-box">
    <!--此处添加组件-->
    <component />
<div/>

页面直接挂载方式,需要将两个项目整合在一起,因此,优化程度就取决于对多个项目的整合方式(复制副本、git submodule、npm包引入。。。)

这里推荐使用git的submodule子模块引入组件库。

能双向同步;项目使用子模块后,不必负责子模块的维护,只需要在必要的时候同步更新子模块即可; 如果修改到子模块,父子模块的提交时独立的,所以只需切换到子模块目录下正常提交。

注意: 更新子模块后,子模块引入的所有依赖在主项目下也需要安装,否则运行会报错。

// 引入
$ cd [存放目录]
$ git submodule add [仓库地址]

// 更新(根目录下)
$ git submodule update --remote

// 提交(切换到子模块目录下)
$ git add .
$ git commit ...
$ git push origin ...
  1. 后台渲染:平台页面通过iframe 嵌套 H5 的页面,使用 postmessage 来做数据交互。分离解耦合理,平台项目不需要涉及任何组件库的代码,只需要获取到上述的组件库信息数据,但是渲染速度没发和直接页面挂载相比且编辑操作实现困难,需要H5自带编辑功能。

H5带编辑功能可以让项目分两个路由入口(index/preview)

  • index: 对外入口,直接渲染组件生成页面。
  • preview: 平台预览入口,需要渲染的组件外层嵌套拖拽组件以实现编辑功能,组件配置的数据通过postmessage传递到父页面。
// index
<div
  v-for="(element, index) in componentsRender"
  :key="index"
>
  <component :is="element.comps" :options="element.property" :isEdit="true" />
</div>

// preview
<draggable>
    <div
      v-for="(element, index) in componentsRender"
      :key="index"
    >
      <component :is="element.comps" :options="element.property" :isEdit="true" />
    </div>
</draggable>
...
  // 添加编辑功能代码
...

这样做,实际上==相当于将编辑功能切割到了两个项目中== ,增加耦合,难以维护。

总结就是,页面挂载组件的方式成本要更小,使用git submodule 后管理组件库也足够方便,因此,页面挂载方式为较优的实现实时预览的技术方案,推荐使用。

保存预览(不可编辑)

视窗区的预览存在一些编辑的辅助线等干扰项,可能无法直观的表现页面实际效果,因此需要点击预览来生成页面。

因为不存在编辑的问题,所以这一步可以直接选择上述实时预览讨论的第二种方式。通过iframe 嵌套 H5 的页面,展示的页面能更好的还原真实效果。

此处的预览数据依旧使用postmessage传递(可在url后带参数?mode=preview加以区分)。为了避免页面刷新后数据初始,可以将临时数据保存在localstorage。

// 点击预览跳转前将数据存入localstorage
....
<span @click="saveRenderData">预览</span>
...
methods: {
    saveRenderData() {
        window.localStorage.setItem('previewRenderData', JSON.stringify(this.renderData));
    }
}

注意:如果传递数据变化了,在H5的子组件需要设置监听来触发视图的更新

H5

负责生成最终页面的项目,是上述iframe所要引入的页面。渲染页面只要根据数据库(预览时为postmessage)返回数据,动态加载组件即可。

vue实现动态注册并渲染组件

// index.vue 传递参数
<render-components :componentsRenderData="renderData" />

// RenderComponents.vue 渲染组件
<div
  class="group"
  v-for="(element, index) in componentsRenderData"
  :key="index"
>
  <component :is="element.comps" :options="element.property">
    <render-components :componentsRenderData="element.childs" />
  </component>
</div>
...
// 动态加载组件
requireComps(renderObj) {
  if(renderObj instanceof Array) {
    renderObj.forEach(item => {
      // 存在组件名则动态加载组件
      if(item.name) {
        this.$set(item, 'comps',
          function(resolve) {
            require([`./libs/${item.name}.vue`], resolve)
          }
        )
      }
      // 存在嵌套 递归
      if(item.childs) {
        this.requireComps(item.childs);
      }
    })
  }
},

总结

总结一下就是:平台端通过制定一套数据规则,所有操作均为对数据的增删改查,最终输出一份页面渲染的数据;H5则是通过数据映射对应的组件数组来实现展示;通过git的子模块来管理组件库。这样,最终我们只要不断的迭代组件库,丰富组件,就能实现快速生成页面的功能。

当然除了基本架构以外,还会有很多技术细节需要处理,篇幅有限就不展开了,这些在实际开发中具体处理即可。

源码

demo仓库

参考资料

⚠️ **GitHub.com Fallback** ⚠️