本文主要介绍如何设计一个高扩展的在线网页制作平台,会交代一些背景和最终的效果以及核心设计方案。
背景
2018 年 3 月份开始,随着运满满的快速发展,开始在频繁的迭代各种活动,那时最快的方式就是拷贝老的活动项目,然后按需求修改,接着上线,然而这种方式很快就遇到了瓶颈,迫使运营团队也会去寻找一些第三方平台去满足自己的运营要求,不过由于定制化弱和用户信息没打通导致没办法大量使用,还是只能等待前端资源排期,两个比较突出的问题。
产品每个活动都需要前端人员介入,甚至替换一个简单的图标和简单的布局,都需要排期等待,吃掉了 50%的前端资源。
市面上可使用的一些在线制作推广平台制作的页面又不能很好地结合到自己的业务流程里面。
转盘抽奖,如果使用第三方平台需要在活动结束后把抽奖名单导出,然后导入自己的平台里面去做匹配,然后在筛选中奖名单,很不方便。
拉新送红包,使用第三方的平台如果用户提交了拉新的手机号。需要定期去同步数据然后送红包,不能对接自己的平台做到实时。
针对这些问题团队迫切需要一个平台来提供运营快速创建活动,开发也能在这平台做一些功能扩展。最好能满足已下几个要求:
丰富的组件提供运营能自主创建页面。
每个做好的页面都可以设置为模板页面,提供运营下次快速通过模板创建页面简单修改然后发布。
提供常用动画然运营能创建炫酷效果的活动。
提供每个活动完整的数据分析方便运营查看效果,常规的 pv,uv,以及自定义页面的元素点击打点和统计功能。
提供灵活的页面管理,方便运营按组按项目维度给其他同事分配权限统一管理。
开发人员可以为组件植入脚本灵活扩展该活动的功能,方便运营使用。
提供统一的组件开发规范,方便开发新的业务组件为运营提供更友好的使用方式。
针对这些要求我们做了码良平台,码良是一个在线 H5 编辑器,用于快速制作 H5 页面。用户无需掌握复杂的编程技术,通过简单拖拽、少量配置即可制作精美的页面,可用于营销场景下的页面制作。同时,也为开发者提供了完备的编程接入能力,通过脚本和组件的形式获得强大的组件行为和交互控制能力。
核心设计
下面会分享下我们的核心设计,这次主要重点说明下面几方面内容
我们会介绍整体的架构来了解一般的编辑产生页面的基本思路,基于数据编程。
我们会介绍核心的组件如何设计,确保可以自由扩展组件能力
我们会介绍如何设计编辑器达到可自定义属性的控制面板
备注(由于整体项目实现使用的 VUE,所以后面有部分介绍具体技术实现的时候会以 VUE 的使用角度说明。用其他框架的自行脑补)
整体架构
1. 整体架构
整体架构相对简单,核心就是定义一套标准的数据规范,提供一个编辑器去编辑这个数据,同时提供一个解析器去解析该数据,然后渲染出页面,流程如下。
2. 数据结构
通过上面的图看到每个页面是由很多节点组成(node),每个节点可以嵌套子节点。而每个节点包括的基本信息如下,备注文章后续提到的 nodeInfo 都是该节点对应的如下数据
{
"id": "truck/button1l",
"type": "truck/button",
"label": "按钮1l",
"version": "0.1.4",
"visible": true,
"style": {
"position": "absolute",
"width": "100px",
"height": "40px"
},
"animate": [],
"props": {
"text": "输入文字",
"type": "danger",
"click": []
},
"path": "https://ymm-maliang.oss-cn-hangzhou.aliyuncs.com/truck/button/0.1.4/index.js",
"script": "",
"events": []
}
复制代码
每个组件比较核心的元素由如下几部分组成
id 元素的唯一编号。方便代码获取和操作
type 组件的类型。会根据不同的类型加载不同的脚本资源,然后运行加载完的脚本会创建一个 VUE Component,然后会把这个 Component 挂载到 VUE 全局,由于每个组件节点都是一个 动态的 Component 组件。这时候只需要修改动态组件的 :is 数据进行内容替换就好了。
label 组件别名。方便运营理解使用
version 组件版本。 每个组件都是有自己的版本的。
style 组件样式
props 组件参数。每个组件都是有一些初始化参数的,这些参数都是营销人员在编辑器里面填写的。这些参数就存放在这里面,在扩展编辑器属性能力里面会详细说明
script 扩展脚本。每个组件可以插入一些脚本代码扩展组件的功能。这些脚本创建的对象会 mixin 到该组件对象里面,在组件设计里面会详细介绍
event 组件绑定事件。 每个组件可以绑定常见 dom 事件。
child 孩子节点。
path 脚本路径。 通过该路径加载脚本创建组件对象。
上图的页面包括一个图片,图片下面两个文字,图片兄弟节点有个按钮元素。对应页面的详细数据结构如下,可以感受下完整结构。
{
"id": "node",
"type": "node",
"visible": true,
"style": {
},
"props": {},
"child": [
{
"id": "truck/image15j",
"type": "truck/image",
"label": "图片15j",
"version": "0.1.4",
"visible": true,
"style": {
"position": "absolute",
"width": "320px"
},
"animate": [],
"props": {
"url": "https://ymm-maliang.oss-cn-hangzhou.aliyuncs.com/ymm-maliang/access/ymm_1533366999689.png",
"click": []
},
"path": "https://ymm-maliang.oss-cn-hangzhou.aliyuncs.com/truck/image/0.1.4/index.js",
"script": "",
"events": [],
"child": [
{
"id": "truck/text3l",
"type": "truck/text",
"label": "文本3l",
"version": "0.1.4",
"visible": true,
"style": {
"position": "absolute"
},
"animate": [],
"props": {
"text": "文字内容1",
"click": []
},
"path": "https://ymm-maliang.oss-cn-hangzhou.aliyuncs.com/truck/text/0.1.4/index.js",
"script": "",
"events": []
},
{
"id": "truck/text3l5g",
"type": "truck/text",
"label": "文本3l",
"version": "0.1.4",
"visible": true,
"style": {
"position": "absolute",
"width": "114px"
},
"animate": [],
"props": {
"text": "文字内容2",
"click": []
},
"path": "https://ymm-maliang.oss-cn-hangzhou.aliyuncs.com/truck/text/0.1.4/index.js",
"script": "",
"events": []
}
]
},
{
"id": "truck/button1l",
"type": "truck/button",
"label": "按钮1l",
"version": "0.1.4",
"visible": true,
"style": {
},
"animate": [],
"props": {
"text": "输入文字",
"type": "danger",
"click": []
},
"path": "https://ymm-maliang.oss-cn-hangzhou.aliyuncs.com/truck/button/0.1.4/index.js",
"script": "",
"events": []
}
],
"script": [],
"animate": [],
"version": "0.1.0",
"events": []
}
复制代码
一句话小结:页面是由很多节点递归生成,每个节点包含布局,事件,脚本,参数,版本等信息,然后编辑器编辑这些信息,解析器解析这些信息。
组件设计
一个页面都是由一个个递归嵌套的组件组成,组件是整个项目的最核心的一部分,为了让组件具有扩展能力,我们对组件的功能使用了 mixin 方式,通过基础组件逻辑+自定义脚本的形式来生成组件。下面介绍下整体组件结构和初始化流程,方便理解我们是如何实现的。
上图左部分可以看到整个页面都是由一个一个 node 节点组成,他们是一个树状结构,每个 node 节点下面包含着一个组件对象做功能展示,下面是 node 节点的 dom 结构,可以看到每个节点都是递归节点,每个节点内部都包含一个动态组件,每个动态组件的通过 nodeinfo.id 为 key 的组件进行渲染。
<div class="node" v-show="visible" :style="nodeInfo.style">
<component :is="nodeInfo.id" v-bind="nodeInfo.props" :ref="nodeInfo.id" :style="componentStyle"></component>
<node v-if="nodeInfo.child" :info="item" v-for="item in nodeInfo.child " :key="item.id"></node>
</div>
复制代码
上图右部分可以看到渲染流程。为了达到组件的高扩展性,每个组件的功能包含两个主要部分
组件代码 ,每个组件都是有特定参数和特定功能的脚本实现,比如 图片,富文本,分享,九宫格等组件,组件代码通过对于的 type 和 path 参数去加载对于的脚本获取对象。
组件通过编辑器添加的脚本 , 编辑器可以为每个组件动态添加脚本来增强对组件的操作能力。如下操作,可以看到一个组件可以添加多个脚本。每个脚本其实就是一个的 vue 组件,终这里面的代码会创建对象 mixin 到最终的 vue 组件里面,所以你可以为组件扩展各种功能进行支持你的特殊业务。
一个节点的逻辑功能=组件逻辑+脚本 1+脚本 2+脚本 3…
每个组件在根据自己的类型加载对应 js 脚本后,会对该组件 nodeInfo.script 里面的 逻辑进行 mixin. 然后创建一个最终的组件注册到 Vue.component 里面方便后续使用,核心代码如下
// 通过加载到的组件脚本获得的全局对象创建vue对象 window['image_1.0.3'] load组件脚本运行后会生成的对象
var component = Vue.extend( window['image_1.0.3'])
// 遍历所有加入的脚本混合组件对象中
nodeInfo.script.forEach((value)=>{
component =component.extent(value)
})
// 以节点id为key,注册最终组件对象
Vue.component(nodeInfo.id,component)
// 修改该节点的动态组件 :is 参数为 该节点id
// done
复制代码
一句话小结:通过不断的 mixin 新的自定义脚本进来扩展组件能力
组件属性编辑设计
属性编辑主要目的是开发组件的人会暴露一些可配置的参数给运营人员在编辑器里面填写和修改。
比如选择一个组件后再右侧属性面板可以对这个组件进行一些属性设置。
为了便于维护和扩展,我们觉得一个组件的可配置数据包括简单数据,复杂逻辑数据,对应可编辑属性的部分也分为两部分
编辑器提供基础属性编辑
编辑器能提供扩展编辑编辑能力,主要针对运营方便操作,特征性的开发组件属性的编辑功能,提供对运营友好的操作体验
下面针对这两块比较核心的内容说明下我们如何做的。
编辑器基础属性编辑能力
对于一个组件的开发者来说,一是定义该组件那些参数需要暴露到编辑器让运营操作,二是定义该属性对应的值通过什么控件操作。
上文在整体架构数据结构中提到了每个 node 节点都有一个 props 属性,该属性就是存放着该组件可配置的参数所配置的最终值,在初始化组件的时候会把这个 props 的数据传入组件进行初始化。而定义一个组件能接受那些参数则是在每个 Vue 组件的 props 属性上定义, 而编辑器的作用就是通过编辑器去获取到每个对象定义的 props,然后根据每个参数的类型提供不同的编辑控件,比如 boolean 我们会提供 切换按钮,image 我们会提供选择图片控件等等。扩展脚本同样可以扩展组件的可编辑属性,下面是一个扩展脚本的例子。主要说明支持的那些类型,可定义的格式。整体流程如下。
下面我们先看一下每个组件可定义的 props 例子。
/**
*
* @param type: 字段类型,支持原生类型以及【码良输入类型】
*
* 码良输入类型:
* input 单行输入框
* text 多行输入框
* enum 列表单选 需提供选项字段defaultList, 支持数组、map结构
* image 图片选择
* audio 音频选择
* video 视频选择
* richtext 富文本
* number 数字
* function 方法设置
* data json数据
* date 时间选择
* checkbox 多选框 同enum 不提供defaultList字段时,输入值为布尔类型
* radio 单选框 同enum
*
*/
return {
props: {
// 原生类型
foo: {
type: String
},
// 图片输入
fooImage: {
type: String,
editer: {
type: 'image'
}
},
// 日期
fooDate: {
editer: {
type: 'date'
}
},
// checkbox 多选
fooCheckbox: {
type: Array, // 此项必须为Array
default: () => { // 且需提供初始值
return [] // ['day', 'hour', 'min', 'sec']
},
editer: {
label: '显示精度',
type: 'checkbox',
defaultList: [ // array 形式的选项
'day',
'hour',
'min',
'sec',
]
}
},
// checkbox 布尔
fooCheckboxBool: {
type: Boolean, // 此项必须为Boolean
editer: {
type: 'checkbox'
}
},
// enum 含选项
fooEnum: {
default: 'value1',
type: String,
editer: {
label: '我是字段名', // 将字段名显示为可读性更强的文本,不提供此项时,显示字段名
desc: '我是帮助文本', // 为字段提供提示信息,帮助理解字段的意义
type: 'enum',
defaultList: { // map结构的选项 key为值,value为显示文本
'value1': '条件1',
'value2': '条件2',
'value3': '条件3',
}
}
},
// 条件属性
ifFoo1: {
type: [Number],
default: 0,
editer: {
work: function () {
return this.fooEnum == 'value1' // 只有当 `fooEnum` 字段取值为 'value1' 时才显示此项
},
label: '条件属性1',
type: 'number',
}
},
ifFoo2: {
type: [Date, String],
default: null,
editer: {
work: function () {
return this.fooEnum == 'value2' // 只有当 `fooEnum` 字段取值为 'value2' 时才显示此项
},
label: '条件属性2',
type: 'date',
}
},
},
mounted: function () {
console.log('hello ' + this.foo)
console.log('hello ' + this.fooImage)
// ...
}
}
复制代码
上面脚本扩展的组件对应的增加的可配置的属性如下图。
这里面的的主要设计在于每个 props 属性里面添加了一个 editer 字段进行该字段在编辑器环境下提供什么组件对该属性进行编辑。editer 的字段主要包括如下。
{
label: '我是字段名', // 将字段名显示为可读性更强的文本,不提供此项时,显示字段名
desc: '我是帮助文本', // 为字段提供提示信息,帮助理解字段的意义
type: 'enum',
ignore: true, // 不在编辑器显示
work:function(){
// 如果满足什么条件才会显示
},
defaultList: { // map结构的选项 key为值,value为显示文本
'value1': '条件1',
'value2': '条件2',
'value3': '条件3',
}
}
复制代码
label 在编辑器显示的名称
desc 该字段在编辑器详细描述
type 编辑该属性的组件类型
ignore 负略在编辑器显示,一般在该属性提供了高级编辑模式需要隐藏掉默认的模式。
work 一个方法,该方法返回 true 会在编辑器显示该属性,一遍用于联动隐藏和显示一些编辑属性
defaultList 一些默认数据,一般提供单选,下拉等默认可选择的值。
一句话小结:编辑器通过获取每个组件的 props,遍历每一个属性,按类型提供不同的操作控件,编辑生成最终的数据放到 nodeInfo.props 上。
扩展编辑属性能力
很多时候一个组件可配置的属性按我们的规划来说就下面几种类型。
/**
*
* @param type: 字段类型,支持原生类型以及【码良输入类型】
*
* 码良输入类型:
* input 单行输入框
* text 多行输入框
* enum 列表单选 需提供选项字段defaultList, 支持数组、map结构
* image 图片选择
* audio 音频选择
* video 视频选择
* richtext 富文本
* number 数字
* function 方法设置
* data json数据
* date 时间选择
* checkbox 多选框 同enum 不提供defaultList字段时,输入值为布尔类型
* radio 单选框 同enum
*
*/
复制代码
如果按每个类型提供一个基本的编辑组件就能完成 90%的需求,不过在随着组件的复杂度增加,每个组件可配置的属性变得千奇百怪,各种需求都可能。比如一个简单的多选,原来的可选项只能写死,现在需要自己请求接口获取。但这些逻辑我们不能做到统一的编辑器里面,也不能做到组件里面,所以只能在做组件的时候提供一种机制让开发组件的同学开发组件的同时,还能对这个组件开发一个自定义的编辑器,并能整合到我们的属性编辑面板中。
整体架构如下,最终效果可以参考上图的自定义面板部分
一个组件打包完一般会有两个必要的脚本,一个是组件对应的 js。一个是该组件对应编辑器的脚本 js。
整个平台对编辑器的功能扩展都是相通的,通过加载脚本,创建对象,注册到 Vue,然后通过动态组件渲染。对编辑器属性的扩展也是一样。加载对应组件的编辑器脚本,然后按相同的方法进行植入。这里就不在细讲。这里简单分享下我们对一个组件的开发最终的结果。如下图
组件开发过程中的界面
组件发布后在码良编辑器里面的样子
组件动画展示
运营活动对一些简单的动画提供支持,方便做一些入场和出场的动画,提升活动的交互感,我们使用了 animate.css 提供的一套 css 动画。下面提供简单的展示
合成组件思考
合成组件就是选择已有的节点保存为一个通用的组件,方便下次直接使用
使用组合组件
导出组合组件。
模板页面
页面模板的目的和组合组件类似,都是提供已经做好的内容,运营快速选择使用达到快速上线活动的目的,下面是简单演示
总结
为了提供一套对运营友好,并且高扩展的 h5 活动制作平台我们做了这个码良平台。现在码良的平台现在支撑着运满满每天新增 5-10 个的新活动页面的需求,已有活动模板的活动 95% 可以营销人员通过模板创建,做些样式或图片的修改,然后发布到线上,整个过程就几分钟。活动的模板和组件模板也在不断沉淀,相信沉淀一段时间后随着模板越来越全,对营销活动的快速制作和可选择性都会更强。
作者介绍:
王坤明,满帮集团运满满公司上海前端负责人。高级前端开发工程师,长期关注 web 前端,nodejs 等领域。善于利用工具和提出工具来提高协作效率。
评论 8 条评论