iframe 接班人-微前端框架 qiankun 在中后台系统实践
「福利」 ?? ヽ(°▽°)ノ ?:文章最后有抽獎,轉轉紀念 T 恤,走過路過不要錯過哦
背景
在轉轉的中臺業務中,交易流轉、業務運營和商戶賦能等功能,主要集中在兩個系統中(暫且命名為 inner/outer )。兩個系統基座(功能框架)類似,以 inner 系統為例,如圖:
inner系統基座業務現狀問題
維護迭代,隨時間延續是不可避免的
至今,inner/outer 均有以下特點:
頁面結構繁雜 分類較多,菜單頁面多;布局五花八門,不統一
技術棧不統一 歷史原因,存在 jquery、靜態模板、react 等技術棧
權限不統一 不同用戶,權限不一樣,使用的功能模塊不同
項目管理不統一 部分功能模塊是由業務方維護;同一功能模塊面向不同用戶角色,也需要在不同系統中使用
初次接觸上述問題時,閃現在腦海里的是:用 iframe 呀。確實,剛開始也是這樣做的。
問題暴露,在維護迭代中是個契機
系統在一個長時間跨度的運行下,隨著維護人員的變遷、使用人群的增多,更多的問題也接踵而至:
樣式不統一
由于沒有統一規范,每個功能模塊在不同的開發者鍵盤下設想的結構不同,輸出的風格也不統一,使整個系統看起來略顯雜亂。
瀏覽器前進/后退
首先,iframe 頁面沒有自己的歷史記錄,使用的是基座(父頁面)的瀏覽歷史。所以,當iframe 頁在內部進行跳轉時,瀏覽器地址欄無變化,基座中加載的 src 資源也無變化,當瀏覽器刷新時,無法停留在iframe內部跳轉后的頁面上,需要用戶重新走一遍操作,體驗上會大打折扣。
彈窗遮罩層覆蓋可視范圍
iframe 頁產生的彈窗,一般只能遮罩 iframe 區域。
頁面間消息傳遞
與基座非同源下,iframe 無法直接獲取基座 url 的參數,消息傳遞需要周轉一下,如使用postmessage來實現;而動態創建的 iframe 頁,或許還需要借助本地存儲等。
頁面緩存
iframe 資源變更上線后,打開系統會發現 iframe 頁依舊是老資源。需要用時間戳方案或強制刷新。
加載異常處理
與基座非同源下,onerror 事件無法使用。使用 try catch 解決此問題,嘗試獲取 contentDocument 時將拋出異常
以上問題,從業務價值看,對用戶的使用體驗會有損失;從工程價值看,希望能通過技術提升業務體驗的同時,也提高系統的維護性。
改進實踐 - 微前端
實踐新技術,在問題暴露時是方向
大多數工程師,包括我,一邊兒嘴里說著:學不動啦!一邊兒想嘗試一些新方式來優化系統。
結合問題分類,有思考一些嘗試方向,如:
中后臺 UI 規范:歷經迭代,百花齊放,然而更需要的是找到合適我司的風格,保持一致性。
此部分這次不再細說,可以 關注我們公眾號 - 大轉轉 FE,后續我們會有專門的文章講這部分。
另外,大互聯網時代,從工程角度看,社區對類似系統的探索有很多,除了 iframe 外,也有不少相對成熟的替代方案:
1. single-spa
2. qiankun
提起這兩個,就要提一下微前端理念,目前社區有很多關于微前端架構的介紹,這里簡單提一下:
Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. — Micro Frontends
大致是說,微前端有以下特點:
技術棧無關:基座不限制子應用的技術棧
完全獨立:子應用獨立部署維護,接入時基座同步更新;又可獨立運行
基于此,不難想到:iframe 也是符合微前端理念的。那其他方案又是如何做的呢?
single-spa
社區里 single-spa 介紹也不少。根據 demo 比葫蘆畫瓢,可以知道它的架構分布:
single-spa架構啟動服務的配置主要是在single-spa-config 文件中,包含項目名稱、 項目地址、路由配置等:
//?single-spa-config.js import?{registerApplication,?start?}?from?'single-spa';//?子應用唯一ID const?microAppName?=?'react';//?子應用入口 const?loadingFunction?=?()?=>?import('./react/app.js');//?url前綴校驗 const?activityFunction?=?location?=>?location.pathname.startsWith('/react');//?注冊 registerApplication(microAppName,loadingFunction,activityFunction );//singleSpa?啟動 start();single-spa 讓基座和子應用共用一個 document,那就需要對子應用進行改造:把子項目的容器和生成的 js 插入到基座項目中。
不需要 HTML 入口文件
js 入口文件導出的模塊,必須包括 bootstrap、mount 和 unmount 三個方法
不過這種方式需要對現有項目的打包方式和配置項進行改造,成本很大。所以,對于已有的工程項目,我選擇了放棄使用。
qiankun
qiankun 也是社區提到比較多的一個開源框架,是基于single-spa 實現了開箱即用。可以采用html entry 方式接入子應用,且子應用只需暴露一些生命周期,改動較少。【少】這個點,真是讓我躍躍欲試。
目前我司業務場景是單實例模式(一個運行時只有一個子應用被激活),我們可以根據一張圖來看看單實例下以html entry方式 qiankun 實現流程:
qiankun原理如上圖所示,一個子應用的全過程有:
初始化配置,匹配出子應用
初始化子應用,加載對應的 html 資源,以及創建 JS 沙箱環境
掛載子應用,執行生命周期鉤子函數
卸載子應用,當切換路由時,執行各卸載鉤子函數,以及卸載 JS 沙箱環境,清除容器節點
具體實現細節,大家可以參考qiankun源碼。
實踐
基座
從規范化開發角度,我司的中后臺系統是基于 umi 開發(詳細可參考我們之前的文章 umi 中后臺項目實踐)。在構建主應用使用了配套的 qiankun 插件:@umijs/plugin-qiankun。
1. 初始化配置項,注冊子應用
插件安裝之后,我們可以在入口文件里配置:
此處主要以運行時為例
//?app.js export?const?qiankun?=?Promise.resolve().then(()?=>?({//?運行時注冊子應用信息apps:?[{//?結算單管理name:?'settlement',?//?唯一id,與子應用的library?保持一致entry:?'//xxx',?//?html?entryhistory:?'hash',?//?子應用的?history?配置,默認為當前主應用?history?配置container:?'#root-content',?//?子應用存放節點mountElementId:?'root-content'?//?子應用存放節點},?{//?公告消息name:?'news',?//?唯一id,與子應用的library?保持一致entry:?'//xxx',?//?html?entryhistory:?'hash',?//?子應用的?history?配置,默認為當前主應用?history?配置container:?'#root-content',?//?子應用存放節點mountElementId:?'root-content'?//?子應用存放節點}],jsSandbox:?{?strictStyleIsolation:?true?},?//?是否啟用?js?沙箱,默認為?falseprefetch:?true,?//?是否啟用?prefetch?特性,默認為?truelifeCycles:?{//?see?https://github.com/umijs/qiankun#registermicroappsbeforeLoad:?(props)?=>?{return?Promise.resolve(props).then(()?=>?loading())},afterMount:?(props)?=>?{console.log('afterMount',?props)},afterUnmount:?(props)?=>?{console.log('afterUnmount',?props)}} }))2. 裝載子應用,在路由配置中使用microApp來獲取相應的子應用名稱:
//?router.config.js export?default?[{path:?'/',component:?'../layouts/BasicLayout',routes:?[...{path:?'/settlement/list',name:?'結算單管理',icon:?'RedEnvelopeOutlined',microApp:?'settlement',??//?子應用唯一id},{path:?'/settlement/detail/:id',name:?'結算單管理',icon:?'RedEnvelopeOutlined',microApp:?'settlement',?//?子應用唯一idhideInMenu:?true,},......{component:?'./404',},],},{component:?'./404',}, ]以上就是基座的改動點,看起來代碼侵入性很少。
子應用
在子應用中,需要做如下的配置
1. 入口文件設置 baseName,及暴露鉤子函數
//設置主應用下的子應用路由命名空間 const?BASE_NAME?=?window.__POWERED_BY_QIANKUN__???"/settlement"?:?"";//?獨立運行時,直接掛載應用 if?(!window.__POWERED_BY_QIANKUN__)?{effectRender(); }//?在子應用初始化的時候調用一次 export?async?function?bootstrap()?{console.log("ReactMicroApp?bootstraped"); }export?async?function?mount(props)?{console.log("ReactMicroApp?mount",?props);effectRender(props); }//卸載子應用的應用實例 export?async?function?unmount(props)?{const?{?container?}?=?props?||?{};ReactDOM.unmountComponentAtNode(document.getElementById('root-content')); }2. webpack 配置中,需要設置輸出為 umd 格式:
//?設置別名 merge:?{plugins:?[new?webpack.ProvidePlugin({React:?'react',PropTypes:?'prop-types'})],output:?{library:?`[name]`,?//?子應用的包名,這里與主應用中注冊子應用名稱一致libraryTarget:?"umd",?//?所有的模塊定義下都可運行的方式jsonpFunction:?`webpackJsonp_ReactMicroApp`,?//?按需加載} }?//自定義webpack配置OK,配置完成!
理論上,啟動項目,部署等都應該沒有問題了。咦,打開地址,頁面一直在 loading,控制臺一堆報錯,看起來要踩一踩坑了。
踩坑
1. 版本一致性
如果主應用和子應用都是基于 umi 框架,在使用 @umijs/umi-plugin-qiankun 插件時,要使用同一個版本,否則子應用報錯。
2. 跨域
qiankun 是通過 fetch 去獲取子應用資源的,所以必須支持跨域
const?mountDOM?=?appWrapperGetter(); const?{?fetch?}?=?frameworkConfiguration; const?referenceNode?=?mountDOM.contains(refChild)???refChild?:?null;if?(src)?{execScripts(null,?[src],?proxy,?{fetch,strictGlobal:?!singular,beforeExec:?()?=>?{Object.defineProperty(document,?'currentScript',?{get():?any?{return?element;},configurable:?true,})};}) }比如:基座地址為 b.zhuanzhuan.com, 子應用為 d.zhuanzhuan.com 。當基座去加載子應用時,會出現跨域錯誤。
曾經有采用通過 Node 服務做一層中轉,跳過跨域問題:
??....maxDays:?3,?//?保留最大天數日志文件 }//?代理 config.httpProxy?=?{'/cors':?{target:?'https://d.zhuanzhuan.com',pathRewrite:?{'^/cors'?:?''}} };return?config但考慮應用的訪問量,以及線上線下環境維護成本,覺得必要性不是很大,最終選擇通過 nginx 解決跨域。
3. 子應用內部跳轉
子應用內部跳轉,需要在基座路由上提前注冊好,否則在跳轉后,頁面識別不到。
{path:?'/settlement/detail/:id',name:?'結算單管理',icon:?'RedEnvelopeOutlined',microApp:?'settlement',hideInMenu:?true, },4. css 污染
qiankun 只能解決子應用之間的樣式相互污染,不能解決子應用樣式污染基座的樣式。比如:當切換到某個子應用時,左側菜單欄突然往右移了。
系統右移查看控制臺,不難發現,子應用的相同模塊覆蓋了基座:
樣式覆蓋這個問題,可以通過改變基座的前綴來解決,搞一個postcss 插件給不同的組件添加不同的前綴。
這里補充一個 css 隔離常用的方式如:css前綴、CSS Module、動態加載/卸載樣式表。
qiankun 中 css沙箱機制 采用的是 動態加載/卸載樣式表。
重寫 HTMLHeadElement.prototype.appendChild 事件
當子應用加載時,在 head 插入 style/link ; 當卸載時,直接移除。
看起來很完美,但有時候會出現,基座樣式丟失的問題。這個跟子應用卸載的時機有關系:當切換子應用時,當前子應用沙箱環境還未被卸載,但基座 css 已被插入,當卸載時會連帶基座 css 一起被清除。
5. 錯誤捕獲,降級處理
若子應用加載失敗,需要給相應的提示或動態插入iframe頁:
//?iframe.js export?default?({?sourceUrl?})?=><iframesrc={sourceUrl}title="xxxx"width="100%"height="100%"border="0"frameBorder="0"/>import?{?render?}?from?'react-dom';//?全局未捕獲異常處理器 addGlobalUncaughtErrorHandler((event)?=>?{console.error(event);const?{?message,?location:?{?hash?}?}?=?event;//?加載失敗時提示if?(message?&&?message.includes("died?in?status?LOADING_SOURCE_CODE"))?{Modal.Confirm({content:?"子應用加載失敗,請檢查應用是否可運行"onOk:?()?=>?import('./Inframe.js')});} });6. 路由懶加載樣式丟失
子應用中存在按需加載的路由,在加載時頁面樣式丟失,這是官方庫產生的問題,issue 里已有大佬提 PR 啦,可參考 https://github.com/umijs/qiankun/issues/857
以上,就是我們的不完全踩坑。
應用間的通信,在我司的業務場景中復雜度不高,使用官方提供的方案就可以解決,此處沒有詳說。
后續
持續性思考會帶來的技術紅利
此次接入 qiankun,也只是處于表面應用。后續我們更要思考接入它之后更深的工程價值,如:
- 自動接入 qiankun
結合我司已有的腳手架和 umi 模板,額外添加一個命令,自動注冊子應用,做到自動化。
- 子應用間組件共享
基座和子應用大概率都用到了 react/dva 等,是否可以在基座加載完之后,子應用直接復用?當然,淺顯思考應該少不了 webpack 的 externals。
文末福利
轉發本文并留下評論,我們將抽取第 10 名留言者(依據公眾號后臺排序),送出轉轉紀念 T 恤一件,大家轉發起來吧~
? ?
?
總結
以上是生活随笔為你收集整理的iframe 接班人-微前端框架 qiankun 在中后台系统实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深度学习入门笔记(十四):Softmax
- 下一篇: 从“读万卷书”到“行万里路”,如何做到知