如何設計高擴展的在線網頁製作平臺

背景

2018年3月份開始,隨着運滿滿的快速發展,開始在頻繁的迭代各種活動,那時最快的方式就是拷貝老的活動項目,然後按需求修改,接着上線,然而這種方式很快就遇到了瓶頸,迫使運營團隊也會去尋找一些第三方平臺去滿足自己的運營要求,不過由於定製化弱和用戶信息沒打通導致沒辦法大量使用,還是隻能等待前端資源排期,兩個比較突出的問題。

  1. 產品每個活動都需要前端人員介入,甚至替換一個簡單的圖標和簡單的佈局,都需要排期等待,吃掉了了50%的前端資源。
  2. 市面上可使用的一些在線製作推廣平臺製作的頁面又不能很好地結合到自己的業務流程裏面。
    • 轉盤抽獎,如果使用第三方平臺需要在活動結束後把抽獎名單導出,然後導入自己的平臺裏面去做匹配然後在篩選名單,很不方便。
    • 拉新送紅包,使用第三方的平臺如果用戶提交了拉新的手機號。需要定期去同步數據然後送紅包,不能對接自己的平臺做到實時。

針對這些問題團隊迫切需要一個平臺來提供運營快速創建活動,開發也能在這平臺做一些功能擴展。最好能滿足已下幾個要求:

  1. 豐富的組件提供運營能自主創建頁面。
  2. 每個做好的頁面都可以設置爲模板頁面,提供運營下次快速通過模板創建頁面簡單修改然後發佈。
  3. 提供常用動畫然運營能創建炫酷效果的活動。
  4. 提供每個活動完整的數據分析方面運營查看效果,常規的pv,uv,以及自定義頁面的元素點擊打點統計功能。
  5. 提供靈活的頁面管理,方便運營按組,按項目維度給其他同事分配權限統一管理。
  6. 開發人員可以爲組件植入腳本靈活擴展該活動的功能,方便運營使用。
  7. 提供統一的組件開發規範,方便開發新的業務組件爲運營提供更友好的使用方式。

針對這些要求我們做了碼良平臺,碼良是一個在線H5編輯器,用於快速製作H5頁面。用戶無需掌握複雜的編程技術,通過簡單拖拽、少量配置即可製作精美的頁面,可用於營銷場景下的頁面製作。同時,也爲開發者提供了完備的編程接入能力,通過腳本和組件的形式獲得強大的組件行爲和交互控制能力。

核心設計

下面會分享下我們的核心設計,這次主要重點說明下面幾方面內容

  1. 我們會介紹整體的架構來了解一般的編輯產生頁面的基本思路,基於數據編程。
  2. 我們會介紹核心的組件如何設計,確保可以自由擴展組件能力
  3. 我們會介紹如何設計編輯器達到可對組件自定義屬性控制面板
    備註(由於整體項目實現使用的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": []
}

每個組件比較核心的元素由如下幾部分組成

  1. id 元素的唯一編號。方便代碼獲取和操作
  2. type 組件的類型。會根據不同的類型加載不同的腳本資源,然後運行加載完的腳本會創建一個VUE Component,然後會把這個Component 掛載到VUE全局,由於每個組件節點都是一個 動態的 Component 組件。這時候只需要修改動態組件的 :is 數據進行內容替換就好了。
  3. label 組件別名。方便運營理解使用
  4. version 組件版本。 每個組件都是有自己的版本的。
  5. style 組件樣式
  6. props 組件參數。每個組件都是有一些初始化參數的,這些參數都是營銷人員在編輯器裏面填寫的。這些參數就存放在這裏面,在擴展編輯器屬性能力裏面會詳細說明
  7. script 擴展腳本。每個組件可以插入一些腳本代碼擴展組件的功能。這些腳本創建的對象會 mixin 到該組件對象裏面,在組件設計裏面會詳細介紹
  8. event 組件綁定事件。 每個組件可以綁定常見dom事件。
  9. child 孩子節點。
  10. 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>
  • 上圖右部分可以看到渲染流程。爲了達到組件的高擴展性,每個組件的功能包含兩個主要部分
    1. 組件代碼 ,每個組件都是有特定參數和特定功能的腳本實現,比如 圖片,富文本,分享,九宮格等組件,組件代碼通過對於的type 和 path 參數去加載對於的腳本獲取對象。
    2. 組件通過編輯器添加的腳本 , 編輯器可以爲每個組件動態添加腳本來增強對組件的操作能力。如下操作,可以看到一個組件可以添加多個腳本。每個腳本其實就是一個的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新的自定義腳本進來擴展組件能力

組件屬性編輯設計

屬性編輯主要目的是開發組件的人會暴露一些可配置的參數給運營人員在編輯器裏面填寫和修改。
比如選擇一個組件後再右側屬性面板可以對這個組件進行一些屬性設置.

爲了便於維護和擴展,我們覺得一個組件的可配置數據包括簡單數據,複雜邏輯數據,對應可編輯屬性的部分也分爲兩部分

  1. 編輯器提供基礎屬性編輯
  2. 編輯器能提供擴展編輯編輯能力,主要針對運營方便操作,特徵性的開發組件屬性的編輯功能,提供對運營友好的操作體驗

下面針對這兩塊比較核心的內容說明下我們如何做的。

編輯器基礎屬性編輯能力

對於一個組件的開發者來說,一是定義該組件那些參數需要暴露到編輯器讓運營操作,而是定義該屬性對於的值通過什麼控件操作。
上文在整體架構數據結構中提到了每個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',
   }
 }

  1. label 在編輯器顯示的名稱
  2. desc 該字段在編輯器詳細描述
  3. type 編輯該屬性的組件類型
  4. ignore 負略在編輯器顯示,一般在該屬性提供了高級編輯模式需要隱藏掉默認的模式。
  5. work 一個方法,該方法返回true 會在編輯器顯示該屬性,一遍用於聯動隱藏和顯示一些編輯屬性
  6. 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,然後通過動態組件渲染。對編輯器屬性的擴展也是一樣。加載對應組件的編輯器腳本,然後按相同的方法進行植入。這裏就不在細講。這裏簡單分享下我們對一個組件的開發最終的結果。如下圖

  1. 組件開發過程中的界面
  2. 組件發佈後在碼良編輯器裏面的樣子

總結

本文提供了一種思路,如何打造一個運營能方便使用,開發也能快速介入增強運營產生的頁面功能的整體方案。希望對讀者有所啓發,我們自己按照這方法打造的碼良平臺,能支持每天8-10個的新活動上線,並且很少需要前端資源介入,通過平臺沉澱了越來越多的模板頁面方便二次使用。極大地解放了運營方面的壓力。也希望對同樣掙扎在這條路上的你有所幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章