1. 程式人生 > 其它 >微前端調研及簡析SPA實現原理

微前端調研及簡析SPA實現原理

技術標籤:java面試程式語言分散式資料庫

最近對微前端討論很多,梳理下自己對微前端的理解以及業內的一些微前端嘗試反饋。

第零部分:自己對微前端理解

第一部分:基於Single-SPA微前端的一些demo

第二部分:Single-SPA微前端實現原理簡析

第三部分:微前端業內一些總結

第零部分:自己對微前端理解

在判斷自己專案是否需要使用微前端前,只要記住一句話即可:殺雞焉用牛刀。

  • 如果專案很簡單,請不要沒有困難創造困難;
  • 如果專案太大,受夠了iframe的種種掣肘,同時你還有一幫陪你肝新玩法的同事,能夠準備好面對意想不到的快樂和意想不到的痛苦,深吸一口氣,來,我們搞起。

在內部的兩次討論,能夠看到不少優點,但同時也需要開發者有一個規範約束,才能發揮微前端的能力。有幾個點需要注意:

  1. 狀態隔離與否 - 狀態共享需要規範
  2. 樣式隔離 - 需要規範
  3. 註冊應用 - 需要規範
  4. 三方依賴不統一
  5. 向下相容方案

第一部分:Single-SPA微前端demo

  • single-spa-learn-kit:一個基於SPA的基礎demo,可以直接run起來
  • 微前端 single-spa:圖文並茂,方案有差異,提出幾個坑點,留意下:
    • 在配置systemJs引用時會有跨域問題,這時候可以配置nginx的返回頭進行解決,詳情倉庫見。
    • 在構建vue專案時,App.vue檔案的主div id必須為你專案構建的id,因為第一次構建後你的html上的div會消失。


  • Single-Spa微前端實踐:相同方案,有一張生命週期圖很好。專案倉庫:
    https://github.com/zhaiyy/single-spa
    。總結優點:
    敏捷性 - 獨立開發和更快的部署週期: 開發團隊可以選擇自己的技術並及時更新技術棧。 一旦完成其中一項就可以部署,而不必等待所有事情完畢。
    降低錯誤和迴歸問題的風險,相互之間的依賴性急劇下降。
    更簡單快捷的測試,每一個小的變化不必再觸碰整個應用程式。
    更快交付客戶價值,有助於持續整合、持續部署以及持續交付。


第二部分:Single-SPA微前端實現

如果第一部分的demo已經跑起來,希望較為完整的理解微前端,可閱讀此節。如不關心如何實現,跳過此節即可。

瞭解Single-SPA實現,循序漸進,有幾個關鍵的庫需要了解下:


1.

systemjs

雖然我們的重頭戲是single-spa,但是在配置時候會發現都需要載入systemjssystemjs是什麼,簡單來說,就是Dynamic ES module loader動態模組載入器,動態載入我們每個依賴的編譯後的指令碼檔案。也正是因為system.js存在,你不會在程式碼中看到大量script指令碼插入的痕跡。

那麼問題又來了,我們載入資源都會建立script標籤,為什麼Systemjs的head中很清爽,答案就是systemjs留了一手,動態建立後又刪除了script.


systemJSPrototype.instantiate = function (url, firstParentUrl) {
 const loader = this;
 return new Promise(function (resolve, reject) {
   const script = systemJSPrototype.createScript(url);
   script.addEventListener('error', function () {
     reject(Error('Error loading ' + url + (firstParentUrl ? ' from ' + firstParentUrl : '')));
   });
   script.addEventListener('load', function () {
     document.head.removeChild(script);
     // Note that if an error occurs that isn't caught by this if statement,
     // that getRegister will return null and a "did not instantiate" error will be thrown.
     if (lastWindowErrorUrl === url) {
       reject(lastWindowError);
     }
     else {
       resolve(loader.getRegister());
     }
   });
   document.head.appendChild(script);
 });
};


至此引出另外一個問題:動態刪除script,全域性的變數和函式還會保留嗎?答案是肯定的。測試確認如此。

官方例項:systemjs-examples


2. single-spa-react

微前端常用的三個生命週期:bootstrap,mount,unmount,不同框架如何和single-spa關聯,答案就是通過single-spa-react中介軟體實現這幾個方法,從而支援下一步single-spa進行註冊registerApplication


3. single-spa

總算到了我們的重頭戲,無論是乾坤,還是我們目前準備的方案,基本都離不開single-spa。

通過上一步的registerApplication註冊,將所有的子模組載入到全域性變數app資料中,並儲存各種狀態,用於後邊的各種裝載和解除安裝。

v2-b96936f206592901c27f51067226155e_b.jpg


single-spa關鍵的幾步是:

  • 初始:預設劫持瀏覽器事件,等註冊應用完成後執行
  • registerApplication註冊應用,觸發reroute
  • start初始化第一次執行,觸發reroute
  • reroute根據不同情況,實現了載入、解除安裝、更改元件生命週期狀態、並延遲執行執行瀏覽器事件

reroute執行時機:

  • registerApplication初始化註冊應用
  • start第一次執行
  • 瀏覽器更新路由hashchange/popstate - urlReroute(navigation event)


第三部分:微前端業內一些總結

這部分一些是業內的方案介紹和總結,也有國外對微前端的爭論。


1. 這可能是你見過最完善的微前端解決方案!

阿里系,介紹了使用JS Entry vs HTML Entry區別,提出使用的是html entry 方案。內容中有一段介紹微前端的實現:

由於我們的子應用都是 lazy load 的,當瀏覽器重新重新整理時,主框架的資源會被重新載入,同時非同步 load 子應用的靜態資源,由於此時主應用的路由系統已經啟用,但子應用的資源可能還沒有完全載入完畢,從而導致路由登錄檔裡發現沒有能匹配子應用 /subApp/123/detail 的規則,這時候就會導致跳 NotFound 頁或者直接路由報錯。
這個問題在所有 lazy load 方式載入子應用的方案中都會碰到,早些年前 angularjs 社群把這個問題統一稱之為 Future State : https://ui-router.github.io/guide/lazyloading#future-states
解決的思路也很簡單,我們需要設計這樣一套路由機制:
主框架配置子應用的路由為 subApp: { url: '/subApp/**', entry: './subApp.js' },則當瀏覽器的地址為 /subApp/abc 時,框架需要先載入 entry 資源,待 entry 資源載入完畢,確保子應用的路由系統註冊進主框架之後後,再去由子應用的路由系統接管 url change 事件。同時在子應用路由切出時,主框架需要觸發相應的 destroy 事件,子應用在監聽到該事件時,呼叫自己的解除安裝方法解除安裝應用,如 React 場景下 destroy = () => ReactDOM.unmountAtNode(container)
要實現這樣一套機制,我們可以自己去劫持 url change 事件從而實現自己的路由系統,也可以基於社群已有的 ui router library,尤其是 react-router 在 v4 之後實現了 Dynamic Routing 能力,我們只需要複寫一部分路由發現的邏輯即可。這裡我們推薦直接選擇社群比較完善的相關實踐 single-spa


2.Bifrost微前端框架及其在美團閃購中的實踐

美團的應用,沒有開源倉庫,不過在兩篇文章明顯有一個不同觀點,就是:是否需要不同子模組間的狀態共享。bifost是支援的,上邊的是講究隔離。所以這類可以根據專案決定是否需要配置全域性狀態。


3.前端微服務在位元組跳動的落地之路

訪談類

InfoQ:微前端最適合的落地場景是?
艾石光:前端微服務最明顯最適合落地的場景是各種中後臺專案,尤其是那些傳統的 iframe 工程。實際試用過 frames 開發架構的同學會知道真的使用時,如何打造實用可理解的 deeplink 就已經很麻煩了。況且還有很多技術細節。比如那些底層複用、父子通訊、session 打通、服務發現等。實際上已經默默承受了很多在微服務裡解決得很好且最終效果可以好得多的問題。
前端微服務需要解決的難題涵蓋了從整合開發環境到服務轉移到部署和流量識別和承載的後端架構,具體涉及到的面很廣,細節還是很多的。
我們團隊把前端微服務抽象成了服務發現、執行隔離和環境一致三個方面,分別對應了了從開發到釋出到線上執行的全部環節
InfoQ:採用微前端有哪些風險與挑戰?
艾石光:前端微服務整體上的安全性和可控性還是比較不錯的。但是如前面所述目前還沒有標準和底層深入支援,所以也新產生了很多獨特挑戰需要去應對。我覺得最值得提的新引入風險還是來自開發除錯過程。
我們認為 docker 等技術是服務端微服務的一個重要優勢,容器技術的成熟使微服務在線上線下有非常一致的環境表現。而前端更接近非容器的、類似於各種 BaaS 的模式。像 firebase、GAE 等不借助容器的微服務體系都有整套的除錯解決方案和執行監控資料回收方案,可以保證部署前有條件充分除錯,也有可靠的測試,能先測試再部署。而微前端的本地開發的環境與除錯用的程式碼和最終上線執行會有不小的區別。不僅如此,線上合併後的環境變化極快,其他平行的模組加起來更新頻率非常高。所以這時發現和復現問題都是一個重要的環節。
我們也提供了很深入的解決方案,投入了很大的研發精力去應對這個挑戰。我們建設了完整的開發鏈工具,比如配置程式碼的修復和消毒,還有融合雲打包的指令碼植入、自動化驗證等。除錯方面有整套的代理服務植入到開發環境的瀏覽器內,有獨立的除錯命令也有充分與 webpack-dev-server 結合的方式。這些一起實現了讓使用者開發時,雖然寫的程式碼僅僅是寄生在 masterpage 內的程式碼片段,但是開發體驗幾乎和傳統的頁面開發完全一致、開發環境看到的執行表現也幾乎完全模擬線上。
其次是服務發現過程的模組上下線,是一個需要小心對待的需要極高可用性的中心繫統。對此我們做了從 ETCD、Redis 到前端 localstorage 的多層容災。為了更好地支撐流量,我們還在進一步嘗試更多的部署架構。
另外我們也做了更詳細的線上錯誤回收,並且打通到了使用者反饋系統。在我們微服務框架裡的 console log 都會被收集和整理,並且一起儲存時會自動裝載上 call stack。


4.每日優鮮供應鏈前端團隊微前端改造

我最直白的感受是實現了專案級別的模組化,把不同專案變成了一個個模組來拼裝組合,也就是說模組化從專案內提升到了專案本身
總結一下使用這套架構收到的好處,分為以下幾點:
  • 縮小專案打包體積(平均每個子專案bundle不到100k),而整合後的公共資源只需載入一次,效能得到很大提升 (技術角度)
  • 使用者體驗更好,使用者感知不到自己在使用多個不同的專案,更加平順流暢 (產品角度)
  • 不同git的專案經過改造後,可以隨意以專案內每個路由頁面為單元拼裝成一個新專案,產品靈活性本質上得到提升 (產品/技術角度)
  • 技術嘗新,使用業界比較先進的微前端理念,幾十個專案,成千上百個功能也能很好的分模組管理。 (管理角度)

也是有很多麻煩之處,需要消耗一定成本:

  • 因為多個vue例項在同一個document裡,需要避免全域性變數汙染、全域性監聽汙染、樣式汙染等,需要制定接入規範。
  • 使用了external抽離公共模組(比如Vue、Vue-router等)後,建構函式(或者Class)的汙染也需要避免,比如Vue.mixin、Vue.components、Vue .use等等都需要做一些額外的工作去避免它們產生衝突。
  • 如果你也想要tab切換不重新整理(使用keep-alive),那需要做的工作更多,主要是處理快取,防止堆記憶體溢位(用chrome自帶的performance monitor檢視),還有專案間切換時路由鉤子等等的處理。

不過跟收益比起來,這些成本就不算什麼了~
最後要說一下,並不是所有場景都適合微前端,尤其是專案規模小、數量少的場景不建議使用。 什麼樣的場景適合這套架構呢?一般有以下特徵:

  • 專案很,規模很,都是每個專案獨立使用git此類倉庫維護的、技術棧為vue/react/angular的這類應用
  • 需要整合到統一平臺上,你正在尋找可能比iframe更合適的替代方案
  • 專案A有功能A1、A2、A3,專案B有功能B1、B2、B3,產品經理要你把A2、B1、B3組合成一個包含這些功能的新專案

可能你會問:為什麼不一開始就把所有需要整合的功能用一個git來維護? 答:理想是美好的,誰也沒有先知能力,隨著公司業務發展亦或是組織架構的改變、人員更迭,以上場景是幾乎不可避免的;我很難想象十多個專案的好幾百個功能都在一個git裡管理起來有多困難。 可能你還會問,那我把需要整合的業務整合成到一個git倉庫呢? 答:這當然是一個解決辦法,前提是整合的成本你能接受;並且將來還有這類需求呢?每次都要手動整合業務程式碼到同一個git倉庫嗎?假設所有人都只維護這個整合完的git倉庫,並行的需求線多了,上線時間會不會擁擠?一個功能產生了致命錯誤,會不會所有功能跟著出問題?
最後我想說:
我們做這套框架的初衷是解決眼前的問題,然而發現它附帶的潛力價值卻比想象的多得多。


5.Microfrontends: the good, the bad, and the ugly 微前端之黃金三鏢客:好傢伙,醜傢伙,壞傢伙

國外對微前端的爭論,只拉出基本論點。

  • The Good: Organizational Flexibility and alignment:分而自治,大專案拆分
    • Separate deploys for separate services
    • Ability of autonomous teams to independently iterate and innovate
    • Ability to organize teams around business units or products
  • The Bad: Operational Complexity操作複雜
    • Needing to have many different applications running in development to test a complete experience
    • Tracking and debug problems across the entire system
    • Dealing with versioning across the system
  • The Ugly: Performance, incoherent experiences,效能及不統一的體驗
    • With each team making their own technology choices, browsers may end up downloading multiple frameworks & duplicate code
    • Users don’t experience your company or product in isolation. This is one of the arguments against completely scoping styles to component - the problem may be even worse with completely separated teams.
    • Some of the implementations of microfrontends (particularly looking at embedding iframes) can cause huge accessibility challenges.


6.其他:

李熠:微前端說明書


作者:李衛東【滴滴出行專家工程師】


滴滴雲-為開發者而生 滴滴雲使者