隨着業務發展,咱們的系統變得愈來愈龐大,給構建速度、靜態資源大小以及應用性能帶來了極大的挑戰css
一個系統是由衆多小模塊組成的,大部分用戶都不會擁有全部模塊的權限,因此咱們的第一個優化方式就是 code split
,將每一個小模塊的代碼分割出來,按需加載,也取得了必定的效果html
然而,當系統數量愈來愈多時,用戶開始抱怨入口太多,但願由統一的入口來完成全部的功能,這個場景有幾種解決方案前端
合併全部系統到一個大系統中webpack
作個應用框架,用 IFrame 嵌入目標系統git
開發統一導航欄,替換各系統的導航欄,在導航欄中經過 <a>
標籤實現系統切換github
看上去
像在使用一個系統,如果用戶切換的頻率較高,則感覺更強烈localStorage/sessionStorage
等瀏覽器存儲採用微前端架構,對應用進行改造web
再來梳理一下現狀redis
全部系統都是基於內部的統一框架開發,擁有統同樣式的頂部欄和側邊欄json
全部系統都擁有本身的 Nodejs 層,用於頁面渲染和 API 請求轉發bootstrap
全部系統都擁有不一樣的域名,沒有特定的域名子路徑
不一樣的系統有本身的小團隊在開發,部分使用不一樣版本的 React 和 Ant Design
開發廣泛要求將來新功能模塊的開發可使用與時俱進的技術
基於現狀分析,微前端是一個能夠去嘗試的方向,因而便開始了踩坑之路,將現有的系統改形成爲微前端的子應用
爲了統一語言,現有的系統在下文稱爲子應用
咱們使用 qiankun 來做爲微前端的實現庫,(聽說)能夠快速實現改造
qiankun 是基於 single-spa 封裝的,其內部實現的子應用加載機制,是基於瀏覽器 url 來實現的,經過第一段子路徑來決定要加載哪一個子應用,好比
因此,爲每一個子應用改造使得全部的訪問都增長子路徑,是咱們要作的第一步
爲每一個路由增長前綴,koa 的代碼示例以下
// 直接訪問根路徑,轉發增長路由前綴 router.get('/', controller.redirect); // 渲染頁面,這裏的 authMiddleware 是校驗中間件,實現登錄校驗邏輯 router.get('/appA/*', authMiddleware, controller.index); // 這裏使用 ${子應用名} + '_apis' 來表示特定應用的 api 請求, // 方便在主應用中作區分進行轉發,同時也方便 Nginx 配置轉發(共享域名) router.use('/appA_apis/*', authMiddleware, controller.transfer); // 剩下的路由忽略 ... 複製代碼
修改每一個在頁面上的 api 請求,使之匹配 ${子應用名} + '_apis'
這一步相對比較麻煩,現有的子應用在頁面代碼中都寫了 /apis
的前綴,若是不是在統一的地方處理的,改動起來會很是麻煩。基於現狀,咱們用了一個取巧的方式:攔截全部 Ajax 請求,並根據須要修改其前綴。具體代碼以下
(() => { if (!XMLHttpRequest.prototype['nativeOpen']) { XMLHttpRequest.prototype['nativeOpen'] = XMLHttpRequest.prototype.open; const customizeOpen = function(method, url, ...params) { if ( // 不須要修改前綴的請求,若是狀況比較多,能夠單獨抽取出來 url.indexOf('hot-update.json') < 0 ) { // 將 /apis 前綴轉化爲 /appA_apis 前綴,這裏是在框架裏 // 處理成 routerPrefix 注入到 window 對象的 url = `${window['routerPrefix']}_${url.slice(1)}`; } this.nativeOpen(method, url, ...params); }; XMLHttpRequest.prototype.open = customizeOpen; } })(); 複製代碼
修改靜態文件的路徑,修改原先的 /statics 路徑,使之匹配 ${子應用名} + '_statics'
這一步大體就是 webpack 的配置了,主要是修改 output 和 publicPath 相關的配置,根據項目實際去操做便可,此處再也不贅述
通過以上步驟,子應用已經能夠支持子路徑的訪問了,但這裏還少了一步比較關鍵的,它不影響你的改造,可是會影響你改造以後用戶的正常訪問。好比,用戶在收藏夾中保存了你的系統某個頁面的地址,例如 xxx.site.com/pages/user
,此時若是你進行了部署,則會致使用戶的訪問出現 404,因此還須要在路由文件進行兼容
// 直接經過 URL 訪問舊路由時,重定向到新的匹配路由,redirectToNewPrefix 的實現很簡單,取出 ctx.url 而且替換掉原先的路由前綴便可 router.get('/pages/*', controller.redirectToNewPrefix); 複製代碼
至此,咱們算是完成了 爲子應用增長路由前綴
的工做。
參考官網,搭建一個最簡單的主應用,只須要有一個用於掛載子應用的節點
<div id="subViewport"></div> 複製代碼
而後調用 registerMicroApps
方法註冊一會兒應用便可
registerMicroApps( [ { name: 'appA', entry: appAEntryMap[process.env.NODE_ENV], // 根據運行環境,加載應用對應的入口,如 'http://localhost:3000/appA' container: '#subViewport', activeRule: '/appA' }, { name: 'appB', // app name registered entry: appBEntryMap[process.env.NODE_ENV], container: '#subViewport', activeRule: '/appB' } ] ); setDefaultMountApp('/appA'); // 設置默認加載的應用,當路由匹配不到時會觸發 start(); 複製代碼
這裏可能會出現 #subViewport
掛載的子應用沒有佔滿容器的現象,查閱官方 issue,給出一個可解決的方案是經過 css 去控制,讓該節點下渲染的子 div 佔滿容器(該 div 會注入 hash,故沒法根據 id 或 class 去處理)
#subViewport {
width: 100%;
height: 100%;
> div {
width: 100%;
height: 100%;
}
}
複製代碼
此步驟參考官方文檔便可
另外,若是但願子應用也能單獨訪問,則能夠在入口 js 處增長代碼
// 不是在 qiankun 框架中裝載的時候,直接渲染 if (!window['__POWERED_BY_QIANKUN__']) { bootstrap().then(mount); } 複製代碼
啓動主應用,訪問頁面,發現一片空白,查看控制檯,出現了跨域問題
Access to fetch at 'http://localhost:3000/appA' from origin 'http://localhost:4001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. 複製代碼
qiankun 是使用 fetch 來獲取子應用的 html 文件的,因此出現了跨域問題。處理起來也比較簡單,因爲自己是由 Nodejs 渲染出來的,只須要增長 koa2-cors
中間件便可解決問題
這裏注意,若是是在開發模式下,須要 webpack-dev-server
也支持跨域,可參考 這篇文章
終於來到了很是關鍵的一個環節,API 請求的處理,這也是官網和 Demo 沒有說起的環節,但倒是最重要的,決定了你的微前端改造是否成功
若是主應用、子應用以及後端 API 都是同一個域名,則自然地不用解決這個問題
如下方案都基於一個大前提:主應用、子應用都有各自的 Node 端處理頁面渲染、登錄校驗和 API 轉發工做
首先要清楚,qiankun 子應用在瀏覽器端發 api 請求時,其實是請求了主應用的 Node 端,url 爲 /appA_apis/xxx
/appB_apis/xxx
這樣的格式,而主應用的 Node 端是沒有處理這些路由的邏輯的,故須要添加轉發邏輯,把這些請求都轉發到子應用的 Node 端去
先在主應用的配置文件添加子應用配置
subApps: [ { name: 'appA', prefix: '/appA_apis', // 子應用的 host,例如 http://localhost:3000 host: process.env['subApps.appA.host'] }, { name: 'appB', prefix: '/appB_apis', host: process.env['subApps.appB.host'] } ] 複製代碼
而後在主應用的路由配置處,增長轉發
subApps.forEach(subApp => { router.all(`${subApp.prefix}/*`, (ctx, next) => { // 轉發請求到 `${subApp.host}/${ctx.url}`,注意參數要透傳,content-type 也要保持一致,此處實現方式多種,不在此贅述 ... }) }) 複製代碼
轉發後會發現,API 請求在子應用的 Node 端沒法經過校驗,咱們先來看下 API 請求的校驗過程
x-auth-token
(這個 key 是咱們的項目規定的,不是固定的)不難看出,主應用登錄後生成的 x-auth-token
並無辦法被子應用的 Node 端識別爲有效的 session id
這裏有兩種作法
主應用和全部子應用共享同一個 session 存儲,咱們項目用的是 redis,因此就是讓全部應用共用同一個 redis
優勢:簡單粗暴,工做量較小
缺陷:共用存儲可能會產生一些衝突,某一子應用的開發不注意時可能錯誤地覆蓋掉其餘子應用的關鍵數據;各子應用沒法擁有特殊的用戶信息(好比在 subA 的用戶信息裏面有一個主應用和其餘子應用都沒有的特別的字段)
子應用提供一個特殊的 SSO 接口,主應用在登錄後,調用全部子應用的 SSO 接口並傳輸這個 x-auth-token
和加密後的用戶帳號,讓各子應用生成各自的 session
優勢:存儲分離;各子應用能夠根據須要維護特殊的用戶信息
缺陷:須要開發新接口;子應用數量較多時,登錄動做的響應時間變長(須要確保每一個子應用的 SSO 接口都成功)
基於現狀,咱們選擇的是第二個方案,對用戶帳號採用 RC4 對稱加密,每一個子應用維護單獨的 salt,主應用維護全部的 salt,子應用配置變成了
subApps: [ { name: 'appA', prefix: '/appA_apis', salt: 'appA', // 子應用的 host,例如 http://localhost:3000 host: process.env['subApps.appA.host'] } ] 複製代碼
而後,在主應用登錄完成後,調用子應用提供的 SSO 接口
for (const subApp of subApps) { // 阻塞調用接口,確保每一個請求都正確 await ... } 複製代碼
通過以上步驟,咱們的頁面請求問題就基本上解決了
最後,是子應用間的切換。一開始使用 React Router 的 Link 標籤,發現沒法從一個子應用切換到另外一個子應用,由於每一個子應用都擁有本身的路由,而每個路由的 history 都是調用 createBrowserHistory()
方法建立的
再次查看 qiankun 的文檔,發現一句話
當微應用信息註冊完以後,一旦瀏覽器的 url 發生變化,便會自動觸發 qiankun 的匹配邏輯,全部 activeRule 規則匹配上的微應用就會被插入到指定的 container 中,同時依次調用微應用暴露出的生命週期鉤子。
關鍵就在於觸發這個瀏覽器 url 的變化。這裏使用 window.history.pushState
方法,達成目的
history.pushState(null, linkPath, linkPath); 複製代碼
完成了子應用的切換,又發現了另外一個現象:當子應用 A 切換到某一個路由時,切換到子應用 B 並進行操做;而後再次切換回子應用 A,url 並非子應用 A 剛剛卸載時的路徑,但子應用 A 從新裝載後會回到剛剛的頁面。這對用戶操做體驗是好的,可是產生了 url 地址和真實呈現的界面不一致的現象
解決思路就是切換到子應用時,跳轉至以前的路由,因此須要存儲當前路由。因爲只能影響當前打開的界面,故選擇將該值存儲到 sessionStorage
中
首先,須要切換子應用以前,記錄當前的路由
sessionStorage.setItem('appA-currentRoute', window.location.href); 複製代碼
而後,在子應用裝載後,獲取當前路由並跳轉,而後刪除記錄的路由
const currentRoute = sessionStorage.getItem('appA-currentRoute'); if (currentRoute) { history.pushState(null, currentRoute, currentRoute); sessionStorage.setItem('appA-currentRoute', ''); } 複製代碼
經過以上方案,實現了子應用切換的應用狀態維護和 url 的匹配
至此,咱們完成了微前端的初步實踐,基於微前端框架 qiankun,經過對原有系統的改造,以及開發一個主應用來做爲容器,實現了多應用合併的效果,在應用間切換時的用戶體驗獲得了很大的提升;同時,也考慮了兼容的問題,支持子應用單獨訪問,也兼容了原有的連接,自動重定向到正確的連接
微前端不是銀彈,只有真正遇到業務問題,須要提升用戶體驗的時候,再考慮去引入。不過,在將來任何應用開發的初期,均可以預先考慮到 共享域名、微前端改造
等的需求,保證全部請求都有惟一子路徑