如何設計高擴充套件的線上網頁製作平臺
背景
2018年3月份開始,隨著運滿滿的快速發展,開始在頻繁的迭代各種活動,那時最快的方式就是拷貝老的活動專案,然後按需求修改,接著上線,然而這種方式很快就遇到了瓶頸,迫使運營團隊也會去尋找一些第三方平臺去滿足自己的運營要求,不過由於定製化弱和使用者資訊沒打通導致沒辦法大量使用,還是隻能等待前端資源排期,兩個比較突出的問題。
- 產品每個活動都需要前端人員介入,甚至替換一個簡單的圖示和簡單的佈局,都需要排期等待,吃掉了了50%的前端資源。
- 市面上可使用的一些線上製作推廣平臺製作的頁面又不能很好地結合到自己的業務流程裡面。
- 轉盤抽獎,如果使用第三方平臺需要在活動結束後把抽獎名單匯出,然後匯入自己的平臺裡面去做匹配然後在篩選名單,很不方便。
- 拉新送紅包,使用第三方的平臺如果使用者提交了拉新的手機號。需要定期去同步資料然後送紅包,不能對接自己的平臺做到實時。
針對這些問題團隊迫切需要一個平臺來提供運營快速建立活動,開發也能在這平臺做一些功能擴充套件。最好能滿足已下幾個要求:
- 豐富的元件提供運營能自主建立頁面。
- 每個做好的頁面都可以設定為模板頁面,提供運營下次快速通過模板建立頁面簡單修改然後釋出。
- 提供常用動畫然運營能建立炫酷效果的活動。
- 提供每個活動完整的資料分析方面運營檢視效果,常規的pv,uv,以及自定義頁面的元素點選打點統計功能。
- 提供靈活的頁面管理,方便運營按組,按專案維度給其他同事分配許可權統一管理。
- 開發人員可以為元件植入指令碼靈活擴充套件該活動的功能,方便運營使用。
- 提供統一的元件開發規範,方便開發新的業務元件為運營提供更友好的使用方式。
針對這些要求我們做了碼良平臺,碼良是一個線上H5編輯器,用於快速製作H5頁面。使用者無需掌握複雜的程式設計技術,通過簡單拖拽、少量配置即可製作精美的頁面,可用於營銷場景下的頁面製作。同時,也為開發者提供了完備的程式設計接入能力,通過指令碼和元件的形式獲得強大的元件行為和互動控制能力。
核心設計
下面會分享下我們的核心設計,這次主要重點說明下面幾方面內容
- 我們會介紹整體的架構來了解一般的編輯產生頁面的基本思路,基於資料程式設計。
- 我們會介紹核心的元件如何設計,確保可以自由擴充套件元件能力
- 我們會介紹如何設計編輯器達到可對元件自定義屬性控制面板 備註(由於整體專案實現使用的VUE,所以後面有部分介紹具體技術實現的時候會以VUE的使用角度說明。用其他框架的自行腦補)
整體架構
- 整體架構 整體架構相對簡單,核心就是定義一套標準的資料規範,提供一個編輯器去編輯這個資料,同時提供一個解析器去解析該資料,然後渲染出頁面,流程如下。
- 資料結構 通過上面的圖看到每個頁面是由很多節點組成(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% 可以營銷人員通過模板建立,做些樣式,圖片的修改,然後釋出到線上,整個過程就幾分鐘。活動的模板和元件模板也在不斷沉澱,相信沉澱一段時間後隨著模板越來越全,對營銷活動的快速製作和可選擇性都會更強。
待續
後面會持續更新的內容
- 元件事件設計
- 資料統計設計
- 開源計劃
後續還會對我們在配合營銷做一些思考和優化進行分享,同時我們也在準備對碼良的開源,需要完善的還有很多,希望能更快和大家見面。
作者和貢獻者
- 王坤明,魏明圓 碼良核心架構設計,碼良專案主要開發
- 潘阿茹 核心模板貢獻者
- 柳剛 碼良後臺服務程式碼主要貢獻者
- 彭輝 表單,步驟條等元件貢獻者