看本文以前,不妨先看看:
1)MPM 賣場可視化搭建系統 — 要素設計html
這是 MPM 分享系列的第二篇,在上一篇 MPM 賣場可視化搭建系統 — 要素設計 中,咱們介紹了 MPM 做爲一個面向賣場場景的頁面可視化搭建系統,最基礎的系統要素都有哪些,並給出了系統要素的推導和設計過程。系統要素是一個龐大系統連接各個模塊的紐帶,也是系統設計的基石,而要素之上,纔是系統的架構和流程。前端
咱們所談到的 MPM,並不單純只是運營同窗直接面對的賣場編輯系統,MPM 生成的賣場頁面也是 MPM 一個重要的組成部分,所以,在架構上,MPM 主要由編輯系統和頁面解析引擎構成。vue
編輯系統就是運營直接使用的 MPM 賣場編輯界面,經運營用戶編輯配置,編輯系統生成一份頁面數據 PageData,在不一樣端環境下,PageData 經由不一樣的頁面解析引擎解析渲染,生成對應的賣場頁面。從 MPM 架構圖中咱們也能夠看到,整個 MPM 的架構流程是基於四大系統要素之上的,也就是說,編輯系統和解析引擎都有對四個系統要素的實現,用於完成對頁面的組裝呈現。web
在 MPM 架構中,PageData 是連接編輯態(編輯配置)和展現態(用戶瀏覽)的重要介質,它本質上是賣場頁面的一層抽象描述,基於 PageData,各端的頁面解析引擎可以知道如何生成對應的賣場頁面。它除包括主要的頁面配置數據外,還將承擔頁面在解析過程當中的一些中間數據產物,好比請求數據緩存等。另外一方面,利用 PageData 做爲隔離,咱們能夠方便地實現一份編輯、多端展現。所以,合理設計 PageData 是實現 MPM 架構的關鍵一步。數據庫
在 MPM 中,咱們用 JSON 來存放 PageData,這是 PageData 的標準結構:json
{
"pageId": "", // 頁面ID
"appType": 1, // MPM頁面類型:1-活動 | 2-館區 | 3-小程序
"pageConfig": {
"basic": {
"type": 2, // 業務類型:1-普通頁面 | 2-京喜頁面
"env": 2, // 渠道類型:1-手Q | 2-微信 | 3-M站
"bgColor": "#fff", // 頁面背景色
"createDate": "", // 建立時間
"customCode": null, // 附加代碼內容
"customCodeIn": 0, // 其餘信息中的附加代碼位置:0-插入頂部 | 1-插入底部
"expireTime": "", // 過時時間
"expireUrl": "", // 過時跳轉連接
"name": "", // 頁面名稱
"path": "", // 頁面路徑
"status": 1, // 頁面狀態:1-有效 | 0-無效
"floorSortId": "", // 樓層排序id
"floorSortType": "", //排序類型 0-樓層排序 | 1-tab排序
"forceLogin": 0 // 是否強制登陸:0-否 | 1-是
},
"search": {
"topShow": 0, // 是否展現頂部搜索框:0-否 | 1-是
"topRuleId": "", // 頂部搜索框金手指參數
"topAd": "", // 頂部搜索框暗文參數
"topBtnRd": "", // 點擊搜索按鈕rd
"topKeywordShow": 0, // 是否顯示底部輪播熱搜
"topKeywordRuleId": "", // 底部輪播熱搜金手指參數
"bottomShow": 0, // 是否展現底部搜索模塊:0-否 | 1-是
"bottomRuleId": "" // 底部搜索金手指參數
},
"share": {
"title": "京東購物", // 分享標題
"desc": "多·快·好·省", // 分享文案
"imgUrl": "//wq.360buyimg.com/img/mpm/defaultShare.jpg" // 分享圖片
}
},
"userInfo": {
"checkNewuser": false, // 校驗是否新人
"checkVip": false, // 校驗是否VIP
"checkPlus": false, // 校驗是否plus會員
"checkBind": false, // 校驗查詢是否綁定
"checkFirstBuy": false // 校驗是否全站首購
},
"componentConfig": [], // 組件配置,也就是樓層配置
"template": {
// 模板相關
"vueFnObj": {}, // Record<樣式id, 擴展方法>
"vueHook": {}, // Record<樣式id, 鉤子函數>
"styleTpl": {}, // Record<樣式id, 模板渲染函數>
// 直出頁用
"header": "", // 頁面頭部
"footer": "" // 頁面底部
},
"dataCache": {
// 要緩存的數據
"userInfo": {}, // 用戶身份數據
"floorSortData": {}, // 樓層排序數據
"dsCache": {} // 樓層業務接口數據緩存
}
}
複製代碼
這是一個頁面保存生成的一個 PageData,雖然有點複雜但結構還算清晰:小程序
一、pageId
後端
MPM 賣場頁面的惟一標識。數組
二、appType
緩存
MPM 的基礎頁面類型,目前共有三類:使用最多的活動類型(也是默認類型)、館區固化運營的館區類型、小程序類型。
三、pageConfig
頁面級別的配置。包括:
pageConfig.basic
:頁面的基礎配置,包括頁面名稱、頁面狀態、頁面建立/過時時間、頁面業務/渠道類型等;
pageConfig.search
:頁面的搜索框配置,包括頁面的頂部搜索框和底部搜索模塊的詳細配置;
pageConfig.share
:頁面的分享配置。
四、userInfo
用戶級別的配置,這些配置實際上不禁運營控制,而是 MPM 編輯系統在保存時自動分析並設置的該頁面的用戶配置,好比運營使用了一個須要判斷「用戶是否爲 VIP」的組件,則 MPM 將自動設置 userinfo.checkVip
爲 true
。在頁面解析引擎中,用戶級別的配置將在預加載(preload)階段被處理,咱們後續會說到。
五、componentConfig
樓層級別的配置,存放了運營詳細的樓層配置,也是解析引擎最關注的配置。componentConfig
是一個對象數組,每一個對象表明着一個樓層,其中包含了樓層組件的配置數據,包括樓層的公有屬性配置和私有屬性配置等,解析引擎將遍歷這個數組,逐個渲染樓層。
六、template
頁面的一些邏輯代碼,主要包括一些公共邏輯和頁面使用到的 MPM 模板。解析引擎會將模板和配置數據組裝成頁面內容進行展現,而這裏只有 h5 頁面纔會用到。MPM 模板具體結構可參見 MPM 賣場可視化搭建系統 — 要素設計。
template.vueFnObj
:map 對象,存放了模板和其對應的擴展函數的關係映射;
template.vueHook
:map 對象,存放了模板和其對應的聲明週期鉤子函數的關係映射;
template.styleTpl
:map 對象,存放了模板和其對應的 template 的關係映射;
template.header
:直出端使用的頁面頭,一段不完整的 HTML 代碼塊,包含了頁面的一些公共優先邏輯,同時出於 CSS 優先原則,MPM 模板的樣式代碼也會在編輯保存階段被存放到這裏;
template.footer
:直出端使用的頁面尾,一段不完整的 HTML 代碼塊,包含了頁面的一些公共置底邏輯。
七、dataCache
頁面請求接口後的一些數據緩存,主要做用是避免重複請求,尤爲是避免直出端數據已完成拉取後,到達客戶端再被重複請求一次。
PageData 是由編輯系統生成的,並實時維護在一個 SQL 數據表中。當頁面被建立時,MPM 會初始化這個頁面的 PageData,並新增一條 SQL 記錄;當保存編輯內容時,編輯系統會將 PageData 實時同步到 SQL 數據庫;而在發佈頁面時,PageData 會被處理成標準 JSON 結構,提供給各端解析引擎處理。
在編輯系統,運營人員建立一個賣場頁面時,系統將生成一個默認的頁面 id,來惟一標識這個頁面,同時生成一份初始化的 PageData,與之一併寫入到 SQL 數據庫中。SQL 數據庫專門設計了一張數據表來存放運營建立的頁面,它除做爲惟一標識的 page_id 外,還包括了頁面名稱 page_name
、頁面路徑 page_path
、頁面類型 page_type
、建立人 page_creator
、建立時間 page_create_date
、頁面內容 page_content
等表字段,其中頁面內容 page_content
就是 PageData 中的組件配置 componentConfig
,是一個序列化的 JSON 字符串。
PageData 在數據表中的結構模型和其標準結構稍微有所不一樣,這裏可能有些人會產生疑問:爲何要改動 PageData 的結構模型呢?頁面名稱、頁面類型、建立時間等等這些,不都是屬於 PageData 中的數據屬性嗎,爲何要單獨拎出來,開設一個表字段來存放,直接序列化整個 PageData 進行存放不行嗎?之因此這麼設計,是由於不管是對於運營人員仍是 MPM 管理人員而言,MPM 都須要提供一些必要的檢索功能,譬如「搜索 XX 時間以前建立的全部頁面」,這個時候,獨立的表字段存放可讓咱們十分方便地完成檢索,甚至更復雜的多表鏈接查詢。
那問題又來了,既然這樣,page_content
爲何就是序列化成 JSON 字符串,而不展開存儲呢?這是由於 page_content
的內部結構多變且難以保持一致(新增模板就會出現新的屬性字段組合),且目前的檢索需求少有涉及深刻到組件配置中的苛刻檢索,因此 page_content
展開存儲的話,維護成本大且收益小,所以咱們直接簡單地將它序列化後,存放在一個表字段中。
在 MPM 編輯系統中,一個賣場頁面的編輯通常會經歷加載、編輯、保存、發佈四個階段,這也是 PageData 生成的過程。
加載也是編輯系統的初始化環節。在這個環節裏,MPM 會從服務端拉取當前要編輯的 MPM 頁面的 PageData,它包括了原先全部的配置數據。這裏須要注意的是,服務端吐出的 PageData 還不是標準結構,這在前邊也提到了。
固然,加載環節所作的事情遠不止這些,包括用戶鑑權、系統配置、新建頁面等等的判斷執行操做,都將在這個環節完成。
在這個環節,PageData 會隨着用戶的編輯配置而實時變化。同時,藉助於 Vue 的監聽能力(watch),咱們實現了一個高效方便的編輯預覽,用戶每一次編輯的效果都能實時展現到預覽區。
編輯中途對頁面進行保存操做時,MPM 會將最新的 PageData 再度提交到服務端,並更新到數據庫。
同時,保存操做也將生成一個預覽頁面的連接,便於在終端上真實瀏覽,所以在提交數據庫的同時,咱們也會把 PageData 轉化成標準的 JSON 結構,供給頁面解析引擎進行解析。對於不一樣端環境的頁面,保存過程也會有些區別,更具體的細節咱們會在後續各端解析過程講到。
保存操做並不會將編輯結果更新到線上,僅僅只是生成一個替代的預覽連接,要變動線上結果,須要對頁面進行發佈。發佈的時候會默認進行一次保存操做,並根據不一樣頁面類型(不一樣的端環境),執行不一樣的發佈流程,更詳細的咱們將在接下來的 PageData 解析中分狀況說明。
除此以外,在發佈前、咱們會進行頁面診斷、RD 生成等前置操做,發佈後,咱們也會執行頁面可訪問性檢測、自動化測試等後置操做。
MPM 在不一樣端環境下,提供了不一樣的頁面解析引擎,解析流程基本類似但也有些不一樣。MPM 支持 H5 和小程序兩種頁面類型,其中 H5 頁面默認支持直出訪問和靜態訪問兩種訪問形態,因此 MPM 涉及的端環境一共有靜態 H五、直出 H五、小程序三類環境。
靜態 H5 提供了一個靜態連接,頁面實際上是一個包含了必要 JS(如 H5 端解析引擎)的靜態 HTML 文件,因此,靜態 H5 解析其實是一種基於 Vue 的客戶端渲染。此前,靜態訪問曾做爲線上 MPM 頁面的主要訪問形態,後來 MPM 逐步推廣直出服務,才讓靜態連接退化爲容災連接,僅做爲容災訪問和預覽訪問。
在保存階段,咱們作了一個頁面組裝,來生成靜態頁面。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>{{title}}</title> <!-- 頭部 JS/CSS --> <script>{{topScript}}</script> <style>{{topCss}}</style> </head> <body> <!-- 應用容器 --> <div class="mpm-app"></div> <script> // page data window.__PAGE_DATA__ = {{pageData}} // 渲染模板 window.__COM_TPL__ = {{template}} // 模板擴展方法 window.__COM_FNOBJ__ = {{fnObj}} // 組件鉤子函數 window.__COM_VUEHOOK__ = {{vueHook}} </script> <!-- 引擎 --> {{engineCore}} <!-- 底部 JS --> <script>{{bottomScript}}</script> </body> </html> 複製代碼
這是一個簡化的組裝模板,它是一個字符串,咱們能夠看到裏邊有不少的雙花括號寫法,這實際上是個簡單的佔位符。經過這個組裝模板,咱們把標準化後的 PageData,連同這個頁面依賴到的 MPM 模板元素,包括 template
、fnObj
、vueHook
,一併替換到對應占位符上,掛載在生成頁面的 window 對象上。而依賴到的 CSS 代碼,其實已經被合併到 topCss
中,放在頁面的最頂部了。
除此以外,MPM 還將經過接口獲取當前版本的解析引擎 engineCore
,一併組裝在頁面底部。engineCore
實際上是一段外部 JS 的引用代碼:
<script src="//wq.360buyimg.com/martpagemakerv3/web/src/wq.vue2.engine.simplified.acf4f576ed9c5cb9f76d.js" crossorigin="true"></script> 複製代碼
發佈時,咱們把這個組裝完成的 HTML 字符串(其實也就是生成的整個頁面),提交到服務端,服務端存爲一個 .html
靜態文件,讓線上可訪問。
當客戶端加載頁面,引擎代碼得以執行。因爲代碼執行順序,此時 window 已經掛載了 PageData(也就是頁面數據)以及相關的渲染模板,引擎能夠直接用於構建頁面。
一、預加載
在構建頁面以前,引擎其實還有一個預加載(preload)環節。這個階段,引擎會根據用戶身份配置 __PAGE_DATA__.userInfo
和樓層排序配置 __PAGE_DATA__.pageConfig.basic.floorSortId
提早經過接口獲取用戶身份信息和頁面樓層排序信息。顯而易見,預加載環節主要處理一些非樓層級別的數據依賴。
二、排序 & 過濾
當預加載接口請求所有到位後,引擎開始執行樓層排序。根據獲取到的樓層 BI 數據,對原有的頁面樓層配置 __PAGE_DATA__.componentConfig
作排序。緊接着,引擎的過濾管道(FilterPipe),再進一步對一些不須要展現的樓層進行排除,包括不在有效時間內的、不知足用戶身份的、不知足渠道類型的,最後,基於新的 __PAGE_DATA__.componentConfig
,引擎組裝了一個樓層骨架,添加到 mpm-app
節點中,做爲後續樓層渲染的容器。
<div class="mpm-app"> <div id="com_1001_con"><div com-root></div></div> <div id="com_1002_con"><div com-root></div></div> <div id="com_1003_con"><div com-root></div></div> <div id="com_1004_con"><div com-root></div></div> </div> 複製代碼
三、Vue 組件/指令的註冊
以後,引擎會對 Vue 組件和 Vue 指令進行統一的全局註冊,保證在後續頁面渲染的時候,模板中能夠正常使用。這裏值得一提的是,爲了減小引擎 JS 的體積,咱們創造性地將引擎拆分爲兩個版本 —— 全量版和簡化版,全量版引擎包含了全部的 Vue 組件/指令,而簡化版引擎只包含一些經常使用的 Vue 組件/指令,其大小比全量版引擎少了近 150 kb。而一個頁面究竟是使用全量版引擎仍是簡化版引擎,咱們會在編輯系統保存頁面的時候進行代碼靜態分析,來判斷這個頁面是否使用了簡化版引擎未囊括的 Vue 組件/指令,若是是,就改用全量版引擎。經過按期的統計維護,咱們讓 85% 以上的頁面都能使用到簡化版引擎。
固然這樣依然存在很多的無用代碼(unused code),那爲何咱們不在頁面保存的時候,根據依賴分析,動態打包出屬於頁面自身的引擎包呢?這主要是考慮到動態打包的 JS 代碼沒有通過嚴謹測試直接上線,存在必定風險,所以咱們才選用了保守的打包方式,在未有完善的測試方案以前,雙版本引擎依然是比較合理的實踐方案。
四、樓層渲染
完成這些前置步驟以後,只須要遍歷 __PAGE_DATA__.componentConfig
,利用 Vue 逐步實例節點並掛載,就能完成渲染了。
// 頁面渲染入口 function renderPage () { __PAGE_DATA__.componentConfig.forEach(createComponent); // ... } function createComponent (com) { const comId = com.id; // el 是在步驟 2 中生成的頁面節點,是樓層容器 const el = document.querySelector(`#${comId}_con>[com-root]`); const data = utils.copy(__PAGE_DATA__[comId]); // 深拷貝一份配置數據 data.fnObj = __COM_FNOBJ__[comId]; // 渲染函數 new Vue({ el, // 容器 data, // 配置數據 render: __COM_TPL__[comId], // 渲染函數 mounted () { __COM_VUEHOOK__[comId]['mounted'](); // 鉤子函數 } }); } 複製代碼
至於樓層自身的業務接口數據,此前在 MPM 賣場可視化搭建系統 — 要素設計 中已經講過,是經過一個 ds 組件完成的。ds 存在於模板中,會在上邊樓層實例化時被執行到,進而發起請求,接收數據,並再次觸發渲染。
針對靜態 H5 首屏體驗差的問題,MPM 打造了一個高可用的 Node 服務,爲全部 H5 頁面提供直出能力。在直出端,MPM 頁面解析引擎只負責渲染首屏內容,頁面餘下內容會等到頁面到達客戶端後,再由客戶端進行渲染補充。
H5 在保存發佈時,一方面會組裝生成靜態 H5 做爲直出容災用,另外一方面則將 PageData(其中包括了配置數據、MPM 模板、頁面頭尾等未組裝的代碼塊)提交到 Node 服務端,寫入到 Redis 中。
MPM 的 Node 直出端基於 Express 框架設計,承載着 MPM 的直出解析引擎。一樣地,MPM 直出端引擎也內置了和靜態 H5 引擎邏輯相同的一套 Vue 組件。
一、讀取 Redis
直出端以訪問爲單元,每趟訪問都有一個獨立的上下文。當用戶訪問某個直出頁面時,經過 Express 中間件,MPM 直出端首先會初始化一個訪問上下文,它包含了頁面的 URL 參數、請求 Cookie、User-Agent 等信息,同時,咱們也將根據請求參數 pageid
,從 Redis 中獲取到頁面數據,併入訪問上下文,用於後續頁面組裝。
二、預加載、排序 & 過濾
預加載、排序過濾環節在直出端一樣存在,與靜態 H5 端沒有太大差別,這裏再也不贅述。
三、頁面數據請求
這個環節是直出端模型和客戶端模型最大的區別之一。在客戶端咱們能夠有屢次渲染,因此咱們利用 Vue 的響應式更新,讓數據請求滯後處理,但在直出端,咱們實現的是流式渲染,有且只有一趟渲染,渲染前要求渲染數據必須所有到位,所以在直出端,咱們必須提早進行頁面數據請求。
因此在這個環節裏,咱們實際上是提早完成了 ds 組件所作的事情。咱們收集了每一個樓層即將發起的業務接口請求,統一處理並存入緩存對象,等到渲染執行到 ds 組件時,ds 會先去檢查緩存對象中是否有緩存數據,若是有,則直接使用。固然了,這裏咱們只考慮前二十個樓層,以此做爲頁面首屏,其他樓層的渲染由客戶端負責。
實際上,這裏邊具體的實現方案並不是最佳,相反存在着諸多不合理,因此我不在這裏做細緻講解。目前咱們也正準備對此進行改造,在後續文章咱們會針對 MPM 數據模型的演化再跟你們深刻探討,什麼樣的數據模型可以更好地契合 MPM 的先後端渲染。
四、頁面渲染
數據就位後,咱們利用 vue-server-renderer
來完成 Vue 的服務端渲染:
import { createRenderer } from 'vue-server-renderer'; const renderer = createRenderer(); // 頁面渲染入口 async renderPage () { // 輸出頁面頭 await context.res.write(pageTop); // 輸出頁面樓層內容 await renderApp(); // 輸出頁面尾 await context.res.write(pageBottom); } // 渲染頁面主體 renderApp () { return Promise((resolve, reject) => { // 建立 app 的 Vue 實例 let app = new Vue({ data: rootData, methods: rootMethods, render: renderFn }); // 建立渲染流 const stream = renderer.renderToStream(app); // 分段輸出 stream.on('data', chunk => { // ... context.res.write(chunk); }); stream.on('end', () => { // ... app.$destroy(); resolve(); }); }) } 複製代碼
在這一步,咱們再也不是每一個樓層各自建立一個 Vue 實例,而是將 PageData 和模板所有合併,只用一個根 Vue 實例來渲染,這樣咱們才能使用 renderToStream
來建立一個渲染流(render stream),完成向客戶端的分段輸出。
MPM 事先在業務小程序中打造了一套與系統組件/模板一一對應、UI 百分百還原的小程序組件,小程序渲染其實就是根據 PageData,把這些已經備好的小程序組件拿出來組合成指望的頁面。
MPM 的小程序頁面是和 H5 頁面區分開來的兩種不一樣頁面類型,編輯流程也是獨立的。MPM 小程序頁面在保存發佈時,僅僅只是將標準化後的 PageData 提交給服務端,生成了一份 JSON 文件。
一、獲取 PageData
在小程序中打開 MPM 搭建的頁面時,引擎首先會請求獲取該頁面對應的 PageData。
二、渲染頁面
小程序頁面的解析,本質上也是一種客戶端渲染,所以這一步其實跟靜態 H5 渲染沒什麼不一樣了,一樣地,先進行預加載和樓層排序,而後根據 PageData 的樓層配置,選擇渲染對應的小程序組件,最終渲染出整個頁面。
瞭解完整個 MPM 的流程機制以後,其實很容易發現一個問題:不一樣端引擎的實現邏輯存在太多相似的了!每一次有需求迭代的時候,老是要同時改動靜態端、直出端、小程序端多處代碼,重複測驗,這給咱們形成了極大的人力損耗,也十分難維護(你真的很難保證寫在三個地方的同一份代碼不會出現差別)。在這種狀況下,三端同構,做爲一個有效的解決方案出如今咱們面前,「一處編寫,三端運行」無疑是一個美好的願景。可是,除了三端同構的技術難度外,MPM 發展至今已經十分龐大,其內部邏輯十分複雜,要完成這種體量的重構,在如何實現同構編譯、如何完成系統兼容過渡等等問題上,還須要投入更多的思考。
若是你以爲這篇內容對你有價值,歡迎點贊並關注咱們前端團隊的 官網 和咱們的微信公衆號 WecTeam,每週都有優質文章推送~