當組織團隊達到必定的開發規模時,頁面可視化搭建是一個減小冗復開發、釋放生產力的最有效方案。因爲專人專責,在平時的實際工做中,咱們接觸的大多都是一些比較固定的業務,慢慢地,你很容易發現,咱們一直在不停地作不少重複的東西。在這種狀況下,咱們會去思考組件化開發,試着把通用的東西抽離複用,但這依然遠遠不夠。每一次需求下達,咱們依然要花上至少兩三天的時間去構建開發,但這些內容可能大多都是已經作過、或者大同小異的。所以,咱們須要一個更加靈活、更加完全的解決方案,最理想狀況是實現零開發響應需求。html
頁面可視化搭建,就是這樣的一種解決方案。你大能夠發現,不管行業,一旦你的組織規模夠大,開發資源跟日益增加的需求量不匹配時,總會誕生這樣性質的一個系統。利用頁面可視化搭建系統,需求方能夠在不通過開發流程的狀況下,經過簡單的編輯操做,在極短期內迅速搭建出一個複雜的頁面,併發布上線。這樣一來,不只成倍地提升了需求的響應效率,更是有效解放了開發側的生產力,讓咱們能夠再也不把時間精力耗費在冗復開發中,而得以聚焦到其餘亟待關注的場景。前端
MPM(Mart Page Maker)是京東自研的一個賣場可視化搭建系統,自 2016 年以來,MPM 歷經三個大版本迭代,現在已經發育成爲一個組件模板豐富、配置功能強大、受衆羣體普遍的運營系統。vue
上線服務四年來,MPM積累了豐富的組件和模板,除去已下架的外,MPM 現有 30+ 個組件、500+ 個模板,業務能力覆蓋商品、導購、營銷等多個場景。json
對於許多手工開發的頁面,實現直出仍然是一個困難重重的事情,而從運營手裏搭建出來的 MPM 頁面默認就支持首屏直出。咱們打造了一個高可用的 Node 直出層,來負責獲取頁面配置數據、聚合請求接口,並最終渲染出頁面首屏內容,從而突破了複雜賣場頁面的首屏體驗瓶頸。數組
除了首屏直出支持外,MPM 還具有其餘一些強大的功能,如:微信
樓層 BI 排序:千人千面,根據不一樣用戶屬性呈現不一樣的樓層優先級排序;markdown
自動化埋點:自動建立用於數據統計的 RD 標識並埋點到頁面上,規避手工開發過程當中 RD 錯埋、漏埋的問題;網絡
頁面健康診斷:對頁面配置進行校驗診斷,並給出診斷單,包括配置數據的合法性、有效性驗證,以及頁面中一些組件可能存在影響的檢測,如多個導航組件是否存在吸頂衝突、部分組件要求強制登陸是否符合預期。併發
歷年來的屢次大促活動都少不了 MPM 的身影,好比 2019 年的 11.十一、12.12 大促活動,深圳京東業務 90% 以上的大促會場都是由 MPM 搭建,包括主會場、全部一級會場和大部分的二級會場。async
系統要素是構成系統的基本組成元素,是設計實現一個系統以前最須要考慮的核心點。推導系統要素,首先要對系統的設計背景、解決場景具有深刻的認知和理解。做爲一個賣場可視化搭建系統,MPM 面臨的場景被約束在了賣場上,也就是說,咱們要搭建的再也不是一切頁面,而只是賣場頁面,這是 MPM 一切設計的根基。所以,咱們須要對賣場有一個充分的瞭解。
在電商行業中,賣場是一個重要的售賣頻道入口,一般狀況下,賣場聚集了衆多不一樣品類的商品進行統一售賣,可以有效地營造 「逛」 的氛圍,進而提升訂單轉化。經過分析,咱們概括出賣場具有這樣三個明顯的特徵。
賣場的樓層大多呈瀑布流自上而下鋪列分佈,樓層與樓層之間相互獨立,關聯較少。相比之下,像商品詳情這類的頁面,全部板塊的內容都與同一個商品有關,其樓層之間的關聯也相對較多。
賣場的職能主要仍是吸引購買,因此賣場基本上可能是一些商品物料、圖文素材的展現,少有像玩法活動同樣複雜的交互邏輯。
也正是由於賣場強大的引流能力,各個業務線都但願在賣場上可以佔據到屬於本身的資源位,所以在這種狀況下,賣場天然要承載起各類各樣的業務場景,其涉及到的業務接口也就變得十分地多。
那麼基於以上分析的賣場特徵,咱們如何推導出 MPM 的系統要素呢?
首先咱們知道,對於任何一個頁面可視化搭建系統,屬性都是必不可少的。以你們比較熟悉的 H5 製做工具 iH5 爲例,其配置方式大抵就是「拖一個按鈕,配置按鈕文字」這樣的操做,這其中,配置按鈕的文字就是屬性,這也不難看出,屬性是一個頁面可視化搭建系統的最小配置單元。
其次,配置結構必定是分層的,屬性之上,須要粒度更粗的配置形態。對於這種形態,iH5 以控件(圖片、文字、按鈕)來實現,如上邊例子的按鈕,因此 iH5 的配置結構實際上是 控件
- 屬性
。然而 MPM 並不適合使用這套配置結構,這是由於雖然配置的粒度越細,配置能夠更加靈活,但配置成本也相應變大。賣場是個內容豐富的頁面,以控件來搭建頁面,那麼搭建一個賣場勢必就要花費很大的時間和精力。而且,賣場樓層擁有不少複雜的數據展現邏輯,好比字段 A 有值就展現 A,不然兜底展現字段 B。若是以控件爲維度去構建頁面,那麼這樣的邏輯實現就會落到運營手上,但運營不想要也不該該關心這些。咱們但願當運營想要頁面擁有某個樓層的時候,直接增長並簡單配置就能呈現出來。
所以,MPM 使用了粒度更粗的兩種配置形態 —— 組件/模板。組件是業務場景的第一載體,而模板則相似於組件的皮膚,爲其提供強大的 UI 展現、表達能力。組件/模板是一個樓層,這樣的粒度極大地下降了運營的配置成本,而 組件
- 模板
- 屬性
三層配置結構也有效保障了賣場搭建的靈活性。
再者,前邊提到,賣場場景所承載的業務接口特別多,若是咱們簡單地把接口請求的邏輯交給組件來作,一來組件各自發起請求,請求沒法獲得有效管理,二來接口邏輯和組件邏輯耦合,沒法組合和複用。所以咱們須要一個東西來接管全部組件原應承擔的數據交互邏輯,統一管理全部接口請求,這就是數據源。
組件、模板、屬性、數據源,是 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 提供了一個專門的解析器來解析這樣的結構。它具有 style
、template
、script.extends
、script.methods
、script.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 解析後呈現的屬性配置如上圖。每一個 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 } 複製代碼
此外,屬性能夠利用 type
、regex
字段對用戶的配置進行簡單的正則校驗。
一些特殊的配置類型默認具有必定的校驗能力,應用了這類類型的屬性,配置外觀與 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 還須要爲數據源提供合併請求、分發響應的能力。
針對這塊的設計,咱們提供了一個數據源中心和若干個數據源。
數據源是一個類,它根據不一樣的用戶配置建立不一樣的請求對象,一個請求對象表明了一個請求動做,將至少包括接口地址、請求參數、響應處理:
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,每週都有優質文章推送~