作者:京東科技 陳雲飛
在以往的業務場景中,使用者進入五花八門的選單體系中,往往會產生迷茫情緒,難以理解平臺名稱及具體作用,導致資料開發與管理學習成本較高,降低工作效率。為此我們整合從資料接入,資料開發,資料管理的全鏈路流程,期望讓使用者體驗一站式資料開發與管理的便捷性;並提供不同業務場景,方便根據業務場景進行進一步資料開發與管理工作,為資料應用平臺打下夯實規範的資料基礎,方便使用者在資料平臺裡,對於資料開發和資料應用進行便捷性的切換,因此我們設計目前的門戶基座,可以快速瀏覽各個平臺,同時串聯資料開發與管理的工作,減少使用者的試錯成本,提升工作效率。
基座子-專案互動簡圖如圖1;
1,基座的業務頁面比較簡單,主要包含:頂部邊欄、左側邊欄、公共子選單、頂級平臺選單;
2,點選左上角圖示,顯示頂級平臺選單,點選平臺,在基座左側邊欄動態顯示平臺一級選單;
3,點選基座左側邊欄,在公共子選單,動態顯示一級選單下邊的二級、三級選單;
4,點選基座左側邊欄或者公共子選單,需要基座排程,在子專案區域正確載入子專案及子計畫頁面;
圖 1
資料中臺新門戶基座要接入老資料平臺一、老資料平臺二等 多平臺的前端專案,並且原有前端子專案在門戶基座呈現任意子專案、任意子計畫頁面 任意混搭的需求;新門戶要接入的專案關係詳情如圖2;
圖 2
資料中臺融合指的是京東體系內,其他對外獨立交付的資料中臺,比如京東工業、京東城市等專案;資料中臺商業化的子專案不僅在新門戶容器內,也可以按需打包進其他資料中臺容器;下面簡稱 資料中臺融合;
原有資料中臺接入子應用的方式有多種:iframe 巢狀、@weus 微應用、連結跳轉等;
• url 不同步。瀏覽器重新整理 iframe url 狀態丟失、後退前進按鈕無法使用。
• UI 不同步,DOM 結構不共用。想象一下螢幕右下角 1/4 的 iframe 裡來一個帶遮罩層的彈框,同時我們要求這個彈框要瀏覽器居中顯示,還要瀏覽器 resize 時自動居中..
• 全域性上下文完全隔離,記憶體變數不共用。iframe 內外系統的通訊、資料同步等需求,主應用的 cookie 要透傳到根域名都不同的子應用中實現免登效果。
• 慢。每次子應用進入都是一次瀏覽器上下文重建、資源重新載入的過程。
•weus 是京東內部研發已經不再維護了,如果有新的問題需要自己解決,對微前端有新需求也需要自己去實現;
•weus 沒有嚴格的 css 沙箱、js 沙箱,而在我們的需求中,沙箱機制是剛需,我們要接入的子專案在 window 上掛在哪些變數,無法通過規範做到強有力的制約(因為要接入的專案是已經寫完了)
•weus 在微前端功能實現,沒有qiankun 豐富健全,比如全域性狀態管理、雖然 weus 實現了子應用的預載入,但是比較機械,是把所有註冊的子應用都快取,實際可能不需要,qiankun 就比較靈活可以根據需要手動快取等;
連結跳轉,指的是點選一個選單,跳轉到另一個頁面。這種方式不符合 「一站式 」巨量資料平臺產品定位;
通過幾種實現方式的對比,最終決定以 qiankun 微前端為基礎,結合我們的實際業務場景,通過許可權選單樹 和 子專案關聯來實現基座對子專案的排程,具體方案請參照 三,基座技術方案詳細描述;
跨子專案跳轉
指的是子專案沒通過基座自行跳轉,基座此時需要根據 url 匹配正確的基座顯示;
觸發節點
點選選單,選單對應的節點即為觸發節點,跨子專案跳轉(重新整理頁面)沒有觸發節點,以跳轉後url 對應的頁面節點為觸發節點;
頁面節點
許可權選單樹中,掛載了子計畫頁面的節點
門戶節點
狹義指的是,當前頁面節點對應的公共子選單父級節點;廣義指的是通過頁面節點向父系節點查詢,最終確認公共子選單、基座邊欄的顯示,並通過頁面節點確定選單高亮等行為;
第一子系節點
指的是在由觸發節點查頁面節點時,只關注子節點中的第一個,如果當前節點的第一個節點不是頁面節點,繼續在孫子節點的第一個,直到節點型別為頁面節點為止;
排程引擎
指的是在基座中,對許可權樹資料處理,接收基座業務層排程,通過對許可權樹遍歷、查詢操作對業務層輸出運算結果的一個抽象分層;
如圖3 為基座技術方案的整體流程圖,下面詳細介紹
圖 3
•如圖 3 所示,首先會在許可權中心設定許可權選單樹,許可權選單樹有很多級,比如一級平臺,二級平臺,二級平臺一級選單,二級選單等,三級選單等;
•然後需要在選單樹上,掛在子專案的頁面,子計畫頁面的一些許可權按鈕等;
•最後許可權中心對不同的使用者賦予許可權選單樹的子集;
•基座從介面拿到許可權選單樹關聯頁面資料後,觸發排程引擎初始化、並把基座的業務更新函數傳給排程引擎;
•基座獲取到選單樹以後,廣度優先演演算法(如圖4)遍歷整個樹結構,並建立節點間父子關聯關係。根據節點是否掛載了子計畫頁面定義前端節點型別 ;最終形成圖 5 的資料;
•在樹遍歷過程中,會統一在基座左側邊欄這一層級的節點,做一個門戶節點的標記;
圖4
•如圖 5 所示,前端排程關係中,把許可權選單樹分成 兩種節點型別,掛載了子計畫頁面的節點,定義為頁面節點。另一種沒掛載子計畫頁面的節點,定義為分組節點;在基座排程中,不關心許可權按鈕節點,在資料處理中,頁面節點就是整個許可權選單樹的葉子節點;
圖 5
•點選基座邊欄,觸發點選選單流程,點選的節點即為 「觸發節點」;
•由觸發節點,向第一子系節點查頁面節點,查頁面節點同時,會同時匹配門戶節點;
•沒有匹配到門戶節點,由觸發節點向父系節點匹配門戶節點;
•這時沒有直接的觸發節點,只能通過 url 上的標識和選單樹上的頁面節點進行匹配
•匹配到頁面節點以後,由頁面節點向父系節點查門戶節點;
•這裡的運算結果主要包含:基座左側邊欄、公共子選單選單樹列表、選單高亮、產品平臺名稱等一系列基座需要正確顯示所需要的資料;
•基座排程引擎,根據觸發排程的型別;如果是點選選單觸發會執行 切換頁面操作,重新整理和跨子專案跳轉則不需要觸發;
•基座排程引擎,在接收基座業務層排程指令後,通過對圖 5 許可權樹的遍歷、查詢操作,最終獲取運算結果;
• 呼叫基座業務層的更新函數,執行運算結果,到此基座排程流程結束;
•基座業務層主要包含:左側邊欄、頂部邊欄、頂級平臺選單等 UI顯示;
•基座排程層,以 qiankun 微前端為基礎,豐富擴充套件了許可權樹資料處理、排程運算、雙向通訊、子專案分組分步預載入策略等;
•基座在執行主題切換時候,通過 qiankun 的 setGlobalState 通知子專案;子專案在繫結專案空間等特定需求時,通過排程引擎封裝的 eventCenter 和基座反向通訊;
•跨子專案跳轉,子專案會自行觸發 pushState,點選基座選單跳轉流程,由排程引擎觸發 pushState 觸達
• qiankun 的 pushSate 劫持策略,觸發 popstate 子計畫頁面因此觸發更新;
圖 6
基座排程引擎通過監聽 popstate 來獲取重新整理、跨子專案跳轉的指令,從而觸發排程流程;以下為原理解析:
•以 Vue 為例,Vue 通過 主動觸發 pushState、replaceState 或者監聽 popstate 變化觸發頁面發生變化;
•但是跨子專案跳轉,執行 pushState 並沒有觸發 popState 基座排程引擎又怎麼能監聽到呢?
•通過閱讀 qiankun 中依賴的庫 single-spa 的原始碼,navigation 模組劫持了 pushstate 方法,只要觸發 pushstate,就會觸發 popstate 事件,關鍵程式碼如下所示:
function patchedUpdateState(updateState, methodName) {
return function () {
const urlBefore = window.location.href;
const result = updateState.apply(this, arguments);
const urlAfter = window.location.href;
if (!urlRerouteOnly || urlBefore !== urlAfter) {
if (isStarted()) {
window.dispatchEvent(
createPopStateEvent(window.history.state, methodName)
);
} else {
reroute([]);
}
}
return result;
};
}
window.history.pushState = patchedUpdateState(
window.history.pushState,
"pushState"
);
js 沙箱利用 qiankun 沙箱機制;
css 沙箱,qiankun 的 css 沙箱不健全;我們接入的又是老專案,目前的策略是 :
•基座通過特殊名稱空間 .susceptor、element-ui 通過 .spr 跟子專案隔離;
•子專案通過特殊名稱空間,例如 .datacenter-xxx 跟基座隔離
•子專案對 .el- 不執行隔離,目的是為了統一控制佈局、主題,同時做效能優化;
基座和子專案,屬於典型的主從結構,採用單向資料流通訊;為了滿足特殊場景子專案跟基座通訊需求,在 qiankun 的通訊基礎上,封裝了 eventCenter 僅用於 子專案 跟基座通訊;
•基座 -> 子專案,通過qiankun 的 onGlobalStateChange 通訊
•子專案 -> 基座,通過 subActions 封裝的 eventCenter 通訊
目前標品基座已經載入了 30多個前端子專案,這麼多前端子專案在首個子專案掛載後執行預載入,有可能會阻塞正常頁面存取;
分組指的是,通過產品劃定的平臺,只有切換到當前平臺才會載入到當前平臺的子專案;
分步指的是,當前平臺子專案過多時,一次預載入少量子專案,分多次預載入;
動態指的是,基座在首個子專案掛載後,會檢測網速,只有網速良好時才會執行預載入;
跨子專案跳轉的背景在於 BDP 一站式開發與管理平臺,是一個大產品,資料規劃、資料整合 等是其中的模組;模組之間存在一些跳轉,對於前端就是跨子專案跳轉;
我們目前的跨子專案跳轉,由 subActions 封裝,抹平了標品基座 和 資料中臺融合容器的區別;
基座跳轉邏輯如下:
crossAppJump: ({ subApp='', path= '', paramsStr='', target= ''})=> {
let jumpUrl = `/susceptor/${subApp}${path}`
if(paramsStr){
jumpUrl = `${jumpUrl}${paramsStr}`
}
if(target === '_blank'){
window.open(`${location.origin}${jumpUrl}`)
} else {
window.history.pushState({ portalPushState: true }, null, jumpUrl);
}
},
資料中臺融合跳轉邏輯如下:
crossAppJump: ({ subApp='', path= '', paramsStr='', target= ''})=> {
let jumpUrl = `/${subApp}${path}`
window.__datafuse_jssdk__.crossAppJumpFnGetter({
path: jumpUrl,
paramsStr,
target
})();
}
由於微前端的技術形態,子專案在基座中載入實質是基座容器的 一段 html,所有介面均是以基座的 域名進行介面轉發;由因為資料中臺融合,所以子專案請求後端介面均是以 /api/datacenter/專案名 開頭;
通過以下 nginx 範例,我們把基座、子專案 的頁面存取、介面存取鏈路 說明白;
# proxy.conf 範例(已做脫敏處理,均不是專案升級名稱)
server {
listen 80;
server_name unify.external.dadacenter;
charset utf-8;
location /sub-app{
alias /export/web/sub-app-web;
try_files $uri $uri/ /index.html =404;
}
location /api/datacenter/subapp {
proxy_pass http://server-sup-app/;
}
location /susceptor{
alias /export/web/susceptor; on;
try_files $uri $uri/ /index.html =404;
}
location /api{
proxy_pass http://sever-api/;
}
}
# server.conf 範例(已做脫敏處理,均不是實際專案)
upstream server-sup-app{
server 111.112.113.114:10001;
}
upstream sever-api{
server 111.112.113.114:1000;
}
鑑權,前端不需要開發,但是需要知道,後端是通過頂級域名種 cookie 鑑權的;例如:unify.external.bigdata 測試環境是在 .external.bigdatao 域名下,這也是為什麼本地開發需要設定 host:127.0.01 loca.external.dadacenter ;
本節提供兩個圖, 對上文介紹的微前端實踐,有進一步的能力提升,有感興趣的同學歡迎一起討論;
如圖 6 所示,設定流程:設定許可權選單樹,然後設定子專案-子計畫頁面兩級;最後把許可權選單樹 和 子專案-子計畫頁面關聯起來,形成如圖 7 的許可權樹-子專案關聯資料模型;
基座兩個排程流程,跟上文類似,但是多了子專案維度,基座在載入子專案的時候,就可以把子專案 在許可權樹的 許可權按鈕資訊全部給到子專案;
圖 6
上文介紹的基座排程流程是簡化後的版本,專案節點只有 分組節點、頁面節點;但是從能力層缺失了 子專案維度;在設計之初,如圖 7 所示:專案節點包含了 分組節點、子專案節點、子計畫頁面節點、頁面節點 4種節點型別;
圖 7