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