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
数据,作为页面组件渲染的原始数据。
有了数据我们就可以渲染组件了。下面我们来对比下视窗区实时预览的两种实现方式:
- 页面挂载:项目需要需引入组件库,提供组件库组件的渲染环境(安装相同的依赖包), 需要时刻关注着组件库的变化同步更新。但是,此方式实时渲染速度快,能很好的实现编辑功能。
// 用拖拽框将组件包裹,尽量减少无关标签
<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 ...
- 后台渲染:平台页面通过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的子组件需要设置监听来触发视图的更新
负责生成最终页面的项目,是上述iframe所要引入的页面。渲染页面只要根据数据库(预览时为postmessage)返回数据,动态加载组件即可。
// 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的子模块来管理组件库。这样,最终我们只要不断的迭代组件库,丰富组件,就能实现快速生成页面的功能。
当然除了基本架构以外,还会有很多技术细节需要处理,篇幅有限就不展开了,这些在实际开发中具体处理即可。