MPM 賣場視覺化搭建系統之要素設計

WecTeam發表於2020-01-17

當組織團隊達到一定的開發規模時,頁面視覺化搭建是一個減少冗復開發、釋放生產力的最有效方案。由於專人專責,在平時的實際工作中,我們接觸的大多都是一些比較固定的業務,慢慢地,你很容易發現,我們一直在不停地做很多重複的東西。在這種情況下,我們會去思考元件化開發,試著把通用的東西抽離複用,但這依然遠遠不夠。每一次需求下達,我們依然要花上至少兩三天的時間去構建開發,但這些內容可能大多都是已經做過、或者大同小異的。因此,我們需要一個更加靈活、更加徹底的解決方案,最理想情況是實現零開發響應需求。

頁面視覺化搭建,就是這樣的一種解決方案。你大可以發現,無論行業,一旦你的組織規模夠大,開發資源跟日益增長的需求量不匹配時,總會誕生這樣性質的一個系統。利用頁面視覺化搭建系統,需求方可以在不經過開發流程的情況下,通過簡單的編輯操作,在極短時間內迅速搭建出一個複雜的頁面,併發布上線。這樣一來,不僅成倍地提高了需求的響應效率,更是有效解放了開發側的生產力,讓我們可以不再把時間精力耗費在冗復開發中,而得以聚焦到其他亟待關注的場景。

MPM 是什麼

MPM(Mart Page Maker)是京東自研的一個賣場視覺化搭建系統,自 2016 年以來,MPM 歷經三個大版本迭代,如今已經發育成為一個元件模板豐富、配置功能強大、受眾群體廣泛的運營系統。

元件模板豐富

上線服務四年來,MPM積累了豐富的元件和模板,除去已下架的外,MPM 現有 30+ 個元件、500+ 個模板,業務能力覆蓋商品、導購、營銷等多個場景。

配置功能強大

對於許多手工開發的頁面,實現直出仍然是一個困難重重的事情,而從運營手裡搭建出來的 MPM 頁面預設就支援首屏直出。我們打造了一個高可用的 Node 直出層,來負責獲取頁面配置資料、聚合請求介面,並最終渲染出頁面首屏內容,從而突破了複雜賣場頁面的首屏體驗瓶頸。

除了首屏直出支援外,MPM 還具備其他一些強大的功能,如:

  • 樓層 BI 排序:千人千面,根據不同使用者屬性呈現不同的樓層優先順序排序;

  • 自動化埋點:自動建立用於資料統計的 RD 標識並埋點到頁面上,規避手工開發過程中 RD 錯埋、漏埋的問題;

  • 頁面健康診斷:對頁面配置進行校驗診斷,並給出診斷單,包括配置資料的合法性、有效性驗證,以及頁面中一些元件可能存在影響的檢測,如多個導航元件是否存在吸頂衝突、部分元件要求強制登入是否符合預期。

承載重點業務

歷年來的多次大促活動都少不了 MPM 的身影,比如 2019 年的 11.11、12.12 大促活動,深圳京東業務 90% 以上的大促會場都是由 MPM 搭建,包括主會場、所有一級會場和大部分的二級會場。

MPM 編輯介面 - 元件配置

MPM 編輯介面 - 頁面配置

MPM 生成頁面

MPM 的要素設計

系統要素是構成系統的基本組成元素,是設計實現一個系統之前最需要考慮的核心點。推導系統要素,首先要對系統的設計背景、解決場景具備深入的認知和理解。作為一個賣場視覺化搭建系統,MPM 面臨的場景被約束在了賣場上,也就是說,我們要搭建的不再是一切頁面,而只是賣場頁面,這是 MPM 一切設計的根基。因此,我們需要對賣場有一個充分的瞭解。

賣場三大特徵

在電商行業中,賣場是一個重要的售賣頻道入口,通常情況下,賣場彙集了眾多不同品類的商品進行統一售賣,能夠有效地營造 “逛” 的氛圍,進而提高訂單轉化。通過分析,我們歸納出賣場具備這樣三個明顯的特徵。

樓層相對獨立

賣場的樓層大多呈瀑布流自上而下鋪列分佈,樓層與樓層之間相互獨立,關聯較少。相比之下,像商品詳情這類的頁面,所有板塊的內容都與同一個商品有關,其樓層之間的關聯也相對較多。

重陳列輕互動

賣場的職能主要還是吸引購買,所以賣場基本上多是一些商品物料、圖文素材的展示,少有像玩法活動一樣複雜的互動邏輯。

業務場景繁多

也正是因為賣場強大的引流能力,各個業務線都希望在賣場上能夠佔據到屬於自己的資源位,因此在這種情況下,賣場自然要承載起各種各樣的業務場景,其涉及到的業務介面也就變得十分地多。

系統要素推導

那麼基於以上分析的賣場特徵,我們如何推匯出 MPM 的系統要素呢?

首先我們知道,對於任何一個頁面視覺化搭建系統,屬性都是必不可少的。以大家比較熟悉的 H5 製作工具 iH5 為例,其配置方式大抵就是「拖一個按鈕,配置按鈕文字」這樣的操作,這其中,配置按鈕的文字就是屬性,這也不難看出,屬性是一個頁面視覺化搭建系統的最小配置單元。

其次,配置結構一定是分層的,屬性之上,需要粒度更粗的配置形態。對於這種形態,iH5 以控制元件(圖片、文字、按鈕)來實現,如上邊例子的按鈕,所以 iH5 的配置結構其實是 控制元件 - 屬性。然而 MPM 並不適合使用這套配置結構,這是因為雖然配置的粒度越細,配置可以更加靈活,但配置成本也相應變大。賣場是個內容豐富的頁面,以控制元件來搭建頁面,那麼搭建一個賣場勢必就要花費很大的時間和精力。並且,賣場樓層擁有很多複雜的資料展示邏輯,比如欄位 A 有值就展示 A,否則兜底展示欄位 B。如果以控制元件為維度去構建頁面,那麼這樣的邏輯實現就會落到運營手上,但運營不想要也不應該關心這些。我們希望當運營想要頁面擁有某個樓層的時候,直接增加並簡單配置就能呈現出來。

因此,MPM 使用了粒度更粗的兩種配置形態 —— 元件/模板。元件是業務場景的第一載體,而模板則類似於元件的皮膚,為其提供強大的 UI 展示、表達能力。元件/模板是一個樓層,這樣的粒度極大地降低了運營的配置成本,而 元件 - 模板 - 屬性 三層配置結構也有效保障了賣場搭建的靈活性。

再者,前邊提到,賣場場景所承載的業務介面特別多,如果我們簡單地把介面請求的邏輯交給元件來做,一來元件各自發起請求,請求無法得到有效管理,二來介面邏輯和元件邏輯耦合,無法組合和複用。因此我們需要一個東西來接管所有元件原應承擔的資料互動邏輯,統一管理所有介面請求,這就是資料來源。

MPM 四大系統要素

元件、模板、屬性、資料來源,是 MPM 賣場視覺化搭建系統的四大系統要素。

MPM 賣場視覺化搭建系統之要素設計

元件

元件是業務場景的第一載體,每一類業務場景在 MPM 中都對應了一個元件,因此按照業務屬性劃分,元件現有包括商品元件、秒殺元件、優惠券元件等。

在賣場中,我們用獨立的 MPM 元件例項來構建每個樓層,這是基於賣場 “樓層相對獨立” 的特徵來設計的。這樣處理的好處是:在不考慮賣場特徵的時候我們面對的是一個一般的頁面,頁面結構是明顯的樹形結構,樹形是極難進行操作處理的,而當我們考慮賣場樓層無關聯的特徵時,賣場的頁面結構就從一個節點樹形結構直接被簡化為一個樓層序列結構,說白了就是樓層的陣列列表,這極大地簡化了 MPM 搭建頁面的實現。

每個元件代表了一個業務場景,所以作為三層配置結構最頂級的元件,它的職責主要是實現業務場景的通用邏輯,比如:導航元件負責實現導航定位、優惠券元件負責實現查券和領券。

基於 Vue,我們很容易聯想到利用 Vue 元件來實現一個 MPM 元件:

/**
 * 秒殺元件
 */
import Vue from 'vue';
import utilMixins from './utils';

/**
 * 註冊 Vue 元件
 */
export default function register () {
  Vue.component('seckill', {
    props: ['params'],
    mixins: [utilMixins],
    data () {
      return {
        // ...
      };
    },
    created () {
      // ...
    },
    methods: {
      // ...
    }
  })
}
複製程式碼

在 MPM 中,每個 MPM 元件都被註冊為一個對應的 Vue 全域性元件,元件中實現通用邏輯。每個 Vue 元件都有一個固定的 props 屬性 params,存放的是使用者對於這個樓層的配置資料。由於是全域性元件,組裝頁面時我們就可以直接遍歷配置,逐個渲染樓層並掛載展示。

並且值得留意的是,我們在 Vue 元件中並不指定 template 屬性,這是因為我們設計要素時把配置分成了元件和模板兩層,可想而知,MPM 模板其實就是 Vue 元件的 template,我們將它抽離出來,在其他步驟中再動態注入。

模板

模板是元件的 UI 層,MPM 要求元件具備靈活的 UI 表現能力,因此我們將元件的 UI 層單獨拆分出來,動態配置。元件之下有多個模板,所以元件-模板是 1-N 的關係。但模板又絕不是純粹的UI層,在實際需求中,模板總是會包含一些或簡單、或複雜的私有邏輯,比如商品元件的一些模板可能要求攜帶預約或領券動作,這就要求我們的模板具備承擔這些私有邏輯的能力。

對於 MPM 模板,我們以一個固定格式的 HTML 來描述:

<!-- 模板的CSS程式碼 -->
<style>
  .rank_2212_215 {
    background: #fff;
  }
</style>

<!-- 模板的HTML程式碼,基於Vue編寫 -->
<template>
  <div>
    <p>Welcome to develop a template of MPM! </p>
  </div>
</template>

<!-- 私有屬性 -->
<script class="extends">
  const com_extend = [
    { "name": "標題", "nick": "title", "type": "text" }
  ]
</script>

<!-- 私有邏輯 -->
<script class="methods">
  const com_js = {
    priceFormat () {
      // ...
    }
  };
</script>

<!-- 生命週期 -->
<script class="hooks">
  const com_vueHook = {
    mounted () {
      // ...
    }
  }
</script>
複製程式碼

這個 HTML 並不是規範的結構,而是以一個我們自定義的格式呈現,MPM 提供了一個專門的解析器來解析這樣的結構。它具備 styletemplatescript.extendsscript.methodsscript.hooks 幾個最基礎的組成部分:

  • style:模板的 CSS 程式碼,MPM 解析提取後,會將 CSS 程式碼直接注入到全域性生效;

  • template:模板的 template 程式碼,MPM 解析提取後,通過 Vue.compile 編譯成 render function 注入到元件中;

  • script.extends:模板的私有屬性,MPM 解析提取後,會將私有屬性的配置掛載到元件資料 data.extend 上;

  • script.methods:模板的私有方法,是一個補充元件 methods 的工具方法巨集,MPM 解析提取後,會將私有屬性的配置掛載到元件資料 data.fnObj 上;

  • script.hooks:模板的生命週期函式,對應 Vue 的元件生命週期,MPM 解析提取後,將會在該元件的生命週期內相應進行呼叫。

這種形態其實跟 Vue 單檔案元件的結構很類似,而我們之所以選用 HTML 來實現 MPM 模板,是因為當時 Vue 單檔案還沒有出現,用 HTML 能為我們提供現成的編輯器高亮和語法提示支援。因此實際上,我們大可以也自行定義一種 .mpm 檔案來存放 MPM 模板,並提供相應的編輯器外掛和一個編譯流程來解析這樣的檔案,當然這是後話了。

屬性

屬性是 MPM 配置的最小單元,靈活組合的配置屬性是實現賣場多樣化的原動力。由於配置場景多樣,MPM需要提供多種型別的配置屬性,包括日期選擇、文字填寫、圖片上傳、顏色選取等。

另一方面,為了和分層結構契合,MPM 屬性還需要分為公有屬性和私有屬性,公有屬性是元件級別的屬性,比如商品組元件的商品組 id;私有屬性是模板級別的屬性,主要是一些模板私有邏輯依賴的屬性。

此外,對於一些關鍵配置,如連結、素材 id、獎池標識等,MPM 屬性還需要對其進行合法性校驗。

基於這些訴求,我們以一個固定結構的物件來描述配置屬性:

[
  { "name": "日期", "nick": "date", "type": "date" },
  { "name": "標題", "nick": "title", "type": "text" },
  { "name": "圖片", "nick": "image", "type": "img" },
  { "name": "顏色", "nick": "color", "type": "color" },
  { "name": "單選", "nick": "radio", "type": "radio", "data": [
    { "name": "選項一", "value": 1 },
    { "name": "選項二", "value": 2 }
  ], "value": "1" },
  { "name": "多選", "nick": "option", "type": "option", "data": [
    { "name": "選項一", "value": 1 },
    { "name": "選項二", "value": 2 }
  ], "value": ["1"]},
  { "name": "範圍", "nick": "range", "type": "range", "min": 230, "max": 280 }
]
複製程式碼

MPM 賣場視覺化搭建系統之要素設計

上邊程式碼被 MPM 解析後呈現的屬性配置如上圖。每個 object 對應了一個配置,object 的 type 屬性用於指定配置的型別,我們提供了多達 10+ 類的配置型別,以滿足不同的配置場景。最後經使用者配置,我們大概會儲存為這樣的資料格式:

{
  "date": "2020-01-01 00:00:00",
  "title": "我是配置的標題",
  "image": "//a.com/image.png",
  "color": "#FFFFFF",
  "radio": 1,
  "option": [1, 2],
  "range": 250
}
複製程式碼

此外,屬性可以利用 typeregex 欄位對使用者的配置進行簡單的正則校驗。

一些特殊的配置型別預設具備一定的校驗能力,應用了這類型別的屬性,配置外觀與 text 無異,但能實時地對配置資料應用預設的校驗規則,如 type=url 用於校驗 url 連結 ,type=id 用於校驗純數字且不超過 30 位的 id,type=char 使用者校驗英文、數字、下劃線組合的標識,等。

[
  {
    "name": "類目id",
    "nick": "cateid",
    "type": "id"
  }
]
複製程式碼

如果現有正則校驗規則不滿足,你還可以通過 regex 欄位來自定義你的校驗規則,同時,為了更好地複用已有正則規則,我們允許以 $ + type 的格式來指定引用系統自帶的正則規則,如下方程式碼利用 $id 引用了 id 的校驗規則,來實現「多個 id 以英文逗號分隔」的校驗需求,十分簡便易讀。

[
  {
    "name": "類目id",
    "nick": "cateid",
    "type": "id",
    "regex": "^$id(,$id)*$",
    "tips": "格式有誤,請檢查符號和空格!",
    "ps": "多個id用英文逗號分隔"
  }
]
複製程式碼

MPM 賣場視覺化搭建系統之要素設計

資料來源

前邊提到,賣場承載了許多業務場景,涉及的介面繁多,如果任由各元件各自請求資料、處理資料,那麼資料請求將變得難維護、不可控。因此,我們需要為 MPM 設計一個資料中心,由它來統一管理和維護所有介面請求。

資料中心包括了若干個資料來源,每個資料來源對應著一個介面,或者更準確來說,每個資料來源對應著一類請求動作,包括介面地址、入參處理、響應處理等。此外,MPM 的請求是各樓層獨立發出的,假如沒有一個合適的機制來保證,那麼就很可能導致同一個 MPM 頁面發出很多個的朝向相同介面的請求,而如果介面本身其實支援批量請求,那麼這就是極大的網路資源浪費。因此,MPM 還需要為資料來源提供合併請求、分發響應的能力。

針對這塊的設計,我們提供了一個資料來源中心和若干個資料來源。

資料來源是一個類,它根據不同的使用者配置建立不同的請求物件,一個請求物件代表了一個請求動作,將至少包括介面地址、請求引數、響應處理:

export default class GroupBuying {
  constructor (option) {
    // 引數處理
    this.params = {
      activeid: option.groupid
    }
  }
  // 請求地址
  url = '//wqcoss.jd.com/mcoss/pingou/show';
  // 請求引數
  params = {};
  // 請求回撥
  callback (result) {
    // ...
    return result;
  }
}
複製程式碼

資料來源中心被表達為一個 Vue 全域性元件 ds,它接受來自於 props 的一個入參欄位 mpmsource,這個欄位指定了使用哪個資料來源,也就是根據這個欄位我們可以分別走不同介面的請求邏輯:

/**
 * 資料來源中心
 */
import Vue from 'vue';
import requester from './requeter';
import utilMixins from './utils';
import * as dataSourceMap from './data-source-map';

export default function register () {
  Vue.component('ds', {
    props: ['params'],
    mixins: [utilMixins],
    data () {
      return {
        // ...
        result: null
      };
    },
    async created () {
      const { mpmsource } = this.params;
      // 獲取對應的資料來源類
      const DataSource = dataSourceMap[mpmsource];
      // 例項化一個請求物件
      const req = new DataSource(this.params);
      // 發起請求
      const result = await requester.fetch(req);
      // 掛載介面資料
      this.data.result = result;
    },
    methods: {
      // ...
    }
  })
}
複製程式碼

建立一個 ds 例項主要完成這一系列動作:首先根據 mpmsource 獲取對應的資料來源 class,傳入配置資料,我們可以例項化得到一個請求物件,MPM 自制的請求器 requester 能夠理解請求物件,發起請求並處理資料,最後掛載 data。

而我們只需要在 MPM 模板中這樣使用:

<template>
  <ds :params="{ mpmsource: 'groupbuying', ... }" inline-template>
    <p>拉取到的拼購商品數量為:{{result.list.length}}</p>
  </ds>
</template>
複製程式碼

Vue 內聯模板允許動態指定元件的 template,在這裡經由 ds 元件請求資料,我們就可以在 ds 元件的內聯模板中直接使用獲取到的資料了。

此外,為了支援介面合併和響應分發,我們為資料來源提供了自定義介面合併及分發策略的能力:

export default class GroupBuying {
  // ...
  batch = {
    // 限制20個
    limit: 20,
    // 合併請求
    merge (reqlist) {
      return {
        activeid: reqlist.map(req => req.data.activeid).join(',')
      }
    },
    // 分發響應結果
    unpack (result, reqlist) {
      const ret = {};
      reqlist.forEach(req => {
        const key = md5(JSON.stringify(req));
        ret[key] = result[req.data.activeid];
      });

      return ret;
    }
  }
}
複製程式碼

batch 描述了該資料來源的請求合併和分發策略,當資料來源具有 batch 屬性時,請求並不會被立刻發起,而是進入了等待佇列。batch.limit 規定了合併的請求數量上限,當請求等待佇列達到了這個上限,亦或是達到了預設的最大等待時間時,請求就會經由 batch.merge 函式打包,構建出新的、合併後的請求引數,然後發出請求。

等請求響應之後,響應資料會首先進入 batch.unpack 函式進行拆包分發。拆包結果是一個對映物件,鍵是請求物件的md5值,值是與該請求物件對應的資料,MPM 的請求器 requester 會自動對這個對映物件進行分揀,將資料分發到各個請求物件,再進入響應處理函式進行處理。

後話

基於賣場構建場景,我們提煉並重點設計了 MPM 賣場視覺化搭建系統的四大系統要素,這也是 MPM 其他流程設計的基礎。估計大家看完之後可能存在不少疑惑:MPM 編輯流程如何設計?儲存釋出如何進行?同構直出是怎麼實現的?...,依然覺得對 MPM 沒有一個完整的認知。這是當然的,MPM 是個龐大且複雜的系統,我們沒辦法一次性讓大家完全理解它。所以在後續我們還將整理出更多關於 MPM 的有意思的設計,分享給大家,希望多多關注。


如果你覺得這篇內容對你有價值,請點贊,並關注我們的官網和我們的微信公眾號(WecTeam),每週都有優質文章推送:

WecTeam

相關文章