1. <dd id="erndk"></dd>
                1. 基于 qiankun 的微前端應用改造踩坑記

                  Splendour 2020/7/28 11:04:09

                  前言 隨著業務發展,我們的系統變得越來越龐大,給構建速度、靜態資源大小以及應用性能帶來了極大的挑戰 一個系統是由眾多小模塊組成的,大部分用戶都不會擁有所有模塊的權限,所以我們的第一個優化方式就是 code split,將每個小模塊的代碼分割出來,按需加載,也取得了…

                  前言

                  隨著業務發展,我們的系統變得越來越龐大,給構建速度、靜態資源大小以及應用性能帶來了極大的挑戰

                  一個系統是由眾多小模塊組成的,大部分用戶都不會擁有所有模塊的權限,所以我們的第一個優化方式就是 code split,將每個小模塊的代碼分割出來,按需加載,也取得了一定的效果

                  然而,當系統數量越來越多時,用戶開始抱怨入口太多,希望由統一的入口來完成所有的功能,這個場景有幾種解決方案

                  • 合并所有系統到一個大系統中

                    • 優點
                      • 用戶體驗可以做到最好,一個單頁應用的操作流暢度較高
                    • 缺陷
                      • 容易變成一個巨石應用,開發、構建時都會產生性能問題
                      • 任何一個小模塊的修改都可能導致整個大系統不可用
                      • 限制了開發框架,未來難以升級
                  • 做個應用框架,用 IFrame 嵌入目標系統

                    • 優點
                      • 改造成本低,只需要開發應用框架
                      • 可以支持同時打開多個系統并通過標簽進行切換
                      • 切換系統時天然地可以維持頁面的狀態,讓用戶繼續之前的操作路徑
                      • 各應用獨立部署,互不干擾
                    • 缺陷
                      • IFrame 中的路由變化無法體現在應用框架的 URL 上,用戶一刷新就會回歸到初始頁面,影響體驗,需獨立開發一套通訊機制讓應用框架保存 IFrame 中系統的路由,需要對現有系統做改造
                      • IFrame 加載速度慢
                      • 若界面上 IFrame 較多,dom 結構會變得復雜,影響系統性能
                  • 開發統一導航欄,替換各系統的導航欄,在導航欄中通過 <a> 標簽實現系統切換

                    • 優點
                      • 改造成本相對較低,需要開發可以快捷集成到不同系統中去的導航欄;若是需要統一域名,則各系統需要改造,所有請求須攜帶特有的子路徑
                      • 基本不影響用戶使用單個系統的體驗
                    • 缺陷
                      • 系統間的切換本質上是打開了一個新的系統,加載性能會影響用戶體驗,用戶只是 看上去 像在使用一個系統,若是用戶切換的頻率較高,則感受更強烈
                      • 系統間的通訊只能依賴 localStorage/sessionStorage 等瀏覽器存儲
                      • 不支持同時打開多系統,無法天然恢復頁面狀態
                  • 采用微前端架構,對應用進行改造

                    • 優點
                      • 真正可以做到在一個入口使用所有功能
                      • 不同應用間的切換體驗較好,除了第一次切換需要消耗一定時間做 js 解析,后續的切換則較為平滑
                      • 主應用可以提供通用功能供子應用使用
                      • 不同應用可以由不同團隊、使用不同的技術棧開發
                    • 缺陷
                      • 有一定的改造工作量
                      • 主應用承載所有流量入口,無形中增大了系統壓力

                  再來梳理一下現狀

                  • 所有系統都是基于內部的統一框架開發,擁有統一樣式的頂部欄和側邊欄

                  • 所有系統都擁有自己的 Nodejs 層,用于頁面渲染和 API 請求轉發

                  • 所有系統都擁有不同的域名,沒有特定的域名子路徑

                  • 不同的系統有自己的小團隊在開發,部分使用不同版本的 React 和 Ant Design

                  • 開發普遍要求未來新功能模塊的開發可以使用與時俱進的技術

                  基于現狀分析,微前端是一個可以去嘗試的方向,于是便開始了踩坑之路,將現有的系統改造成為微前端的子應用

                  為了統一語言,現有的系統在下文稱為子應用

                  踩坑之路

                  選型

                  我們使用 qiankun 來作為微前端的實現庫,(據說)可以快速實現改造

                  應用改造

                  增加子路徑

                  qiankun 是基于 single-spa 封裝的,其內部實現的子應用加載機制,是基于瀏覽器 url 來實現的,通過第一段子路徑來決定要加載哪個子應用,比如

                  • ${你的域名}/appA/......:表示加載 a 應用
                  • ${你的域名}/appB/......:表示加載 b 應用

                  所以,為每個子應用改造使得所有的訪問都增加子路徑,是我們要做的第一步

                  • 為每個路由增加前綴,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 的前綴,如果不是在統一的地方處理的,改動起來會非常麻煩?;诂F狀,我們用了一個取巧的方式:攔截所有 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%;
                    }
                  }
                  復制代碼

                  子應用暴露生命周期函數,UMD 格式打包

                  此步驟參考官方文檔即可

                  另外,如果希望子應用也能單獨訪問,則可以在入口 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 校驗問題

                  終于來到了非常關鍵的一個環節,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 請求的校驗過程

                  • 從請求的 cookie 中取出 x-auth-token(這個 key 是我們的項目規定的,不是固定的)
                  • 通過這個 token,判斷是否有與之對應的有效的 session,如果有,則取出用戶信息
                  • 通過用戶信息生成 jwt,并透傳其他參數,轉發到真正的后端 API

                  不難看出,主應用登陸后生成的 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,通過對原有系統的改造,以及開發一個主應用來作為容器,實現了多應用合并的效果,在應用間切換時的用戶體驗得到了很大的提高;同時,也考慮了兼容的問題,支持子應用單獨訪問,也兼容了原有的鏈接,自動重定向到正確的鏈接

                  微前端不是銀彈,只有真正遇到業務問題,需要提高用戶體驗的時候,再考慮去引入。不過,在未來任何應用開發的初期,都可以預先考慮到 共享域名、微前端改造 等的需求,保證所有請求都有唯一子路徑

                  隨時隨地學軟件編程-關注百度小程序和微信小程序
                  關于找一找教程網

                  本站文章僅代表作者觀點,不代表本站立場,所有文章非營利性免費分享。
                  本站提供了軟件編程、網站開發技術、服務器運維、人工智能等等IT技術文章,希望廣大程序員努力學習,讓我們用科技改變世界。
                  [基于 qiankun 的微前端應用改造踩坑記]http://www.yachtsalesaustralia.com/tech/detail-145198.html

                  贊(1)
                  關注微信小程序
                  程序員編程王-隨時隨地學編程

                  掃描二維碼或查找【程序員編程王】

                  可以隨時隨地學編程啦!

                  技術文章導航 更多>
                  国产在线拍揄自揄视频菠萝

                        1. <dd id="erndk"></dd>