微前端說明書

爲何寫

互聯網公司技術選型三定律javascript

  1. 流行即正義
  2. 新鮮即正義
  3. 複雜即正義 —— 我

由於最近被問起當前公司的前端產品有沒有聚合爲微前端的可能性,因此又從新開始審視「微前端」這個話題。差很少一年前寫過一篇反駁美團微前端方案的文章。那篇文章更多的是關於「沒有必要這麼作」,可是「應該如何作」我也並無給出更好的方案。最近在參考了不少資料以後,對這個的問題的答案有了輪廓html

本文分爲兩個部分:「戰略」和「戰術」。前者關於爲何以及在什麼場景下使用微前端,後者關於採用什麼的技術實施微前端。這篇文章裏我會反對某些方案,同意某些方案,僅表明我的意見前端

開頭的「互聯網公司技術選型三定律」是我我的總結的,也是我在這篇文章裏極力反對的。這三條定律的產生有行業的緣由也有程序員職業的緣由。三定律的存在致使了某些技術的被曲解和濫用,其中就有微前端。在本文中也會引用這三定律作說明java

戰略篇

實現微前端一點都不難,我相信你也看過無數種微前端實施方案。但問題不在於咱們能不能作,而是咱們爲何要作。react

Dan Abramov(你應該知道他是誰) 在 Twitter 上提出過一個問題,他認爲微前端解決的問題經過好的組件模式就能解決,爲何須要微前端?git

有時候甚至不用經過組件,經過一個門戶網站將不一樣功能的站點收集在一個頁面上某種意義上也算微前端。因此咱們談論的微前端到底是什麼?程序員

微前端的概念衍生自微服務。在我看來微服務帶來的改進是是架構上的解耦,好比靈活替換和獨立部署發佈。注意這樣的解耦是架構上的而不是功能上的,在實際的的工做中,經常一個功能的迭代會帶來多個微服務的鏈式修改。在一個惡劣設計的極端狀況下,你劃分了十個微服務,可是每次功能修改都須要對十個微服務同時修改,那這和一個單體應用有什麼區別?在單體應用中若是你設計的足夠優秀,單體內部也能夠存在好的功能解耦。因此在當今微服務做爲標配的狀況下,微服務也並非絕對正義github

微前端和微服務類似,它帶來的也是僅僅是架構上的解耦。關於功能解耦我在戰術部分詳述npm

組件化的確是目前前端廣泛的開發模式,但並非全部的前端功能都須要走組件化這一條路。好比文檔性質的站點能夠經過 static site generator 生成;絢爛的活動頁面更適合利用動畫特效類庫進行編程。我想表達的是:微前端不是跨世代的通用解決方案,它也不是用於代替先用的組件模式。它只是給了咱們一個讓不一樣技術棧不一樣團隊開發同一個產品的機會。這個定義來自於 Luca Mezzalira 對 Dan Abramov 質疑的回覆,我很是贊同:編程

Let’s start with this, Micro-frontends are not trying to replace components, it’s a possibility we have that doesn’t fit in all the projects like components are not the answer for everything.

微前端適用於不一樣技術棧不一樣團隊須要對同一產品進行修改的開發模式,好比 Google Cloud:

從菜單欄咱們能夠看出谷歌雲提供不一樣類型的服務,可是這些服務之間都相互獨立,有的是通用性質的,有的是雲計算相關的,即便是在雲計算一欄下又劃分了不一樣類型的計算服務。(我猜想)不一樣的服務來自不一樣的團隊進行開發,雖然它們不相互干擾,可是又須要同一個產品予以體現。那麼使用微前端是最好的方式

注意這裏「同一產品」的定義僅僅是從視覺形態和用戶體驗方面考慮。若是 A 網站只是要用到 B 網站的數據,那麼經過接口提供就行了。

你可能會注意到騰訊旗下的(全部)站點的登錄框都是使用 iframe 集成。這也算是一種微前端:其餘的團隊只負責本身業務相關的頁面,而「登錄框」團隊負責維護統一登錄框供你們調用。他們之間不須要關心對方的技術棧,迭代週期(甚至甩鍋也變得方便了)。若是有一天 iframe 變成了統一的 Web Component,這種微前端關係仍然成立

美團的的微前端方案裏,咱們看看他們作微前端的訴求:

美團已是一家擁有幾萬人規模的大型互聯網公司,提高總體效率相當重要,這須要不少內部和外部的管理系統來支撐。因爲這些系統之間存在大量的連通和交互訴求,所以咱們但願可以按照用戶和使用場景將這些系統彙總成一個或者幾個綜合的系統。

由於美團的HR系統所涉及項目比較多,目前由三個團隊來負責。其中:OA團隊負責考勤、合同、流程等功能,HR團隊負責入職、轉正、調崗、離職等功能,上海團隊負責績效、招聘等功能

這種團隊和功能的劃分模式,使得每一個系統都是相對獨立的,擁有獨立的域名、獨立的UI設計、獨立的技術棧。可是,這樣會帶來開發團隊之間職責劃分不清、用戶體驗效果差等問題

這裏我對他們要作微前端的動機感到有一些疑惑:

  • 「系統彙總」的方式有不少,除了門戶之外,咱們曾經嘗試過經過給每個系統添加一個公共共享的導航欄組件來讓系統之間的導航和跳轉更方便,效果也不錯
  • 「獨立的域名、獨立的UI設計、獨立的技術棧」——這不就是相互獨立的站點嗎?若是這麼多年用戶都能正常使用,爲何如今要把它們聚合在一塊兒?
  • 「這樣會帶來開發團隊之間職責劃分不清、用戶體驗效果差」,我不認爲微前端可以解決團隊之間職責劃分的問題;用戶體驗效果不是更應該從交互體驗和統一設計規範入手嗎?

我不是針對美團,但就微前端而言,就事論事我認爲這是一個好的反面例子,可以讓咱們從不一樣的角度進行反思。在後面的內容裏我也會再引用其中的內容。固然他們也不是這篇文章中惟一的反面教材。

我在閱讀 Martin Fowler 的 《Patterns Of Enterprise Application Atchitecture》時,最大的一點感觸是他歷來不排斥任何的技術方案:若是你想作業務相關的數據存儲,你固然能夠選擇 ORM 來實現 Domain Model 模式;你一樣能夠選擇簡單至極的 Transaction Script 模式 (A Transaction Script organizes all this logic primarily as a single procedure):

However much of an object bigot you become, don’t rule out Transaction Script. There are a lot of simple problems out there, and a simple solution will get you up and running much faster.

不少人(曾經包括我本身在內)在技術選型方面喜歡追求一種「宏大敘事感」:若是技術不夠複雜,不夠新,開發週期不夠長,動員團隊不夠多,怎麼在公司內彰顯個人影響力?咱們之因此敢這麼放肆是由於環境鼓勵咱們這麼作,每一個團隊都在這麼作。咱們一直在被暗示,項目的風險和可維護性不重要,反正三年以後我也不必定在這個公司,三年以後我可能成了管理人員,三年以後接手維護系統的人不是我。不管如何三年以後項目必定會推倒重來。咱們的簡歷上老是強調咱們作過多少系統,而不是把它們作的多好

從職業素養的要求上說做爲開發人員咱們應該關心風險和可維護性。減小項目風險和增長可維護性的措施之一就是讓代碼變得簡單。微前端從本質上說只是給了咱們一個解決選項而非標準答案。若是你有留意的微服務的發展趨勢的話,微服務生態已經很是的龐大,幾乎每個環節都能找到對應的第三方組件來完成工做。微前端也同樣,若是你願意你能夠找到無數種方案讓項目看上去高精尖,可是爲何明明一個 React 就能解決的問題必定要上 React 全家桶才甘心。

準確且謹慎的使用微前端,這是個人建議

Dan Abramov 提出的另外一個質疑是多種技術棧混合的產物實際上是一種互不妥協的結果:

首先我不認爲應該禁止遊戲混用引擎,好比某個 3D 遊戲的某個回憶復古關卡須要橫版射擊的表現形式,那麼就應該使用橫版射擊引擎,引擎應該忠於遊戲的展示。其次,前端框架與遊戲引擎不一樣,遊戲引擎不只決定了物理特效,還影響了畫面展示,可是前端框架只是決定了運行機制,真正的用戶體驗一致性取決因而否統一 UI 設計與用戶體驗

戰術篇

我沒法告訴你某個方案是最完美的。評價是一件多維的事情,這其中甚至還與你的團隊規模有關,因此我只是把這些可能的方案的 pros 和 cons 一一列舉出來。我也不會陷入到具體的技術細節中去,例如如何在 CSS 中避免 class 污染,像我以前說的,實現歷來都不是問題,問題是咱們爲何要去實現。

Web App 隔離

讓咱們從最簡單的 case 開始。

在美團的例子中,微前端是由三個團隊獨立開發的 Web App 組成,此時 App 是微前端架構裏的最小粒度。這樣的劃分和隔離是最安全的,由於 App 間幾乎沒有任何的從人到代碼的資源共享。

重複代碼

但這樣的獨立策略難免讓人擔憂代碼的重複:假設團隊 A 使用 React 技術棧開發了一個 Dialog 組件,團隊 B 也使用了 React 技術棧也開發了一個 Dialog 組件,那麼貌似這那個 Dialog 可以合併成一個 Dialog 來減小維護成本。

這的確符合常識,咱們耳目濡染的接收了不少關於不要作重複代碼的薰陶:好比 DRY(Don't Repeat Yourself) 原則,DIE 原則(Duplication is Evil).在絕大多數狀況下它們都是正確,但在微前端中並不是如此。「微 (Micro)」 這個詞並不是僅僅是字面上「小」的意思,而是表明獨立和自治。以 Dialog 爲例,不一樣的 App 隸屬於不一樣業務,不一樣的業務對 Dialog 功能有着不一樣的需求。每一個小團隊對本身的業務纔是最熟悉的,若是須要對 Dialog 進行變化的話他們可以對本身維護的 Dialog 準確快速的作出決定。把不一樣團隊的 Dialog 合併成爲一個以後,看似代碼量減小了,可是期間的溝通成本和維護成本反而增長了。原本是爲了解耦架構的微前端由於組件共享又被耦合在了一塊兒。

即便不站在微前端的角度上,我依然不推薦抽象共享組件。抽象最好應該是在項目穩定的後期,看到了確切的功能重疊部分,再考慮把它們共享出來。由於在需求快速變化的前期,不一樣業務的需求會致使共享組件變成並集而非交集的結果。

最後抽象並不是是無敵的,前提是你要知道如何抽象,錯誤的抽象比重複代碼維護起來還要難受

編排層

隔離方案中另外一個須要解決的問題是應用的啓動和切換,此時咱們須要一個相似於 Orchestration Layer(編排層) 的東西。它負責協調不一樣的 App 之間的活動,好比:

  • 管理 App 的生命週期
  • 加載、卸載 App
  • 微前端路由管理
  • 提供公共功能

編排層不是什麼新鮮的東西,在 SOA 架構中就已經存在。你能夠把編排層和 App 理解爲 steam 平臺和平臺上游戲的關係,也能夠把 BFF 看成針對接口的編排層。在美團的方案中,編排層就是他們口中的 Portal 項目。

可是我反感方案美團 Portal 方案的關鍵緣由是,編排層對 App 代碼進行了入侵。好比:

爲了避免侵入「子項目」,咱們採用構建過程當中替換的方式來作,「Portal項目」把公共庫引入進來,從新定義,而後經過window.app.require的方式引用,在編譯「子項目」的時候,把引用公共庫的代碼從require('react')所有替換爲window.app.require('react'),這樣就能夠將JS公共庫的版本都交給「Portal項目」來控制了

這段話自相矛盾:段落的開頭說「爲了避免入侵子項目」,結尾則說「這樣就能夠將JS公共庫的版本都交給 Portal 項目來控制了」。這樣一來,微前端中最寶貴的獨立技術棧的優點被削弱了,全部 App 的公共類庫都要交給 Portal 控制。

若是它們指的是 Java 的 Portal 概念的話,我以爲再這裏也不適用,由於 Portal 指的是動態碎片聚合成單個網頁:

A portlet is a Web-based component that will process requests and generate dynamic content.

在這裏我想特別的強調編排層的職責,編排層不是 manager,它相似 broker、coordinator 甚至 glue。編排層是爲 App 服務,而不是 App 爲編排層服務。你不會見到 BFF 對上游的後端接口提需求;你也不會見到 Application Layer 對 Domain Layer 指手畫腳。

關於編排層另外一點我想強調的是,編排層不侷限於在 client 端實現,咱們也能夠擁有 server 端的編排層。例如當用戶從應用中登出以後,由後端返回一個包含須要登錄的頁面,而前端則不須要再關心權限控制。這實際上是回到了傳統 MVC 的那一套。若是選擇 server 端的編排層,一方面咱們能夠考慮用上 server rendering;另外一方面咱們也須要擔憂 App 間數據共享的問題

以組件爲單位

以組件爲單位聚合成微前端是目前你能看到的主流理想的實現方式。

  • 爲何說是主流?由於你能搜索到的絕大部分關於微前端的實現案例,都是基於組件化的。
  • 爲何說是理想?由於這些案例一般是來自某一位開發人員之手,而非是某個團隊實踐以後的結果。

若是你在 Google 上搜索 Micro Frontends, 排名靠前的是一個名叫 Micro Frontends 的開源項目。項目裏舉了一個例子,來描述用組件聚合微前端的一個場景,在這個挑選商品的頁面中,它須要調度三種框架來編寫組件來協同完成工做:

咱們就以這個 case 爲例,看看以組件爲單位的微前端須要解決什麼問題

Communication

通訊是頭等大事。在上面的例子中,當用戶在產品列表中選擇不一樣類型的玩具時,須要通知購買按鈕的價格進行調整。然而項目做者在父子組件通訊實現方案中選擇直接修改購買按鈕對應的 DOM:移除舊 DOM、插入新 DOM 或者修改 DOM 屬性。在這個開源項目中,做者認爲 DOM 就是組件間相互通訊的 API 。

我支持做者後半段敘述的使用 DOM Event 來進行子組件到父組件以及同輩組件之間的消息傳遞。可是直接修改 DOM 絕對是一個很是糟糕的設計。直接修改 DOM 比如我直接經過 IP 訪問網站,比如 React 父組件經過找到子組件的 DOM 來修改子組件。不只耦合性強,之後每增長一處須要感知變化的組件時,都要在父組件中添加代碼。可是經過事件,我只須要添加消費方便可。

Synchronization

僅僅是消息機制每每是不夠的,有時候咱們將數據狀態進行同步。假設如今須要支持用戶勾選多個商品並統一進行結算,且支持優惠滿減活動。此時購物按鈕組件須要存儲目前購物總金額,才能計算出優惠以後的金額。

此時我想到三個辦法:

  1. 保持原狀,用戶選擇商品時依然發佈事件。購物按鈕接收到事件後經過 Event Sourcing 計算當前總額
  2. 商品列表追蹤當前商品總額,並在用戶操做商品時經過事件同步給購物按鈕
  3. 用戶選擇商品時發佈事件,不過由全局存儲的模塊接收事件並計算總金額。購物按鈕從全局存儲模塊得到當前總金額

方案一的缺陷在於,若是有多個組件同時須要知道當前總額時,多個組件須要重複相同的工做,一份相同含義和價值的數據會存儲被存儲多份

方案二的問題在於,商品列表在業務邏輯上來講是不須要知道商品總額的,模塊的職責劃分出現了錯誤

因此目前看來方案三才是最佳的選擇

Package Manage

通訊和數據共享都沒法迴避一件事情:契約。不管是組件間直接通訊仍是經過 event 進行通訊,它們都須要和對方預約消息格式;須要共享數據的組件之間也須要約定數據的 schema。不管組件如何的迭代,契約始終要和其餘組件保證一致。

由於組件之間獨立的緣故,不一樣的組件迭代節奏不盡相同,天然組件間就會出現版本差別。然而如何保證不一樣版本間的契約不會被破壞?文檔能夠,契約測試也能夠。然而更大的問題是,如何保證組件協做產生的功能不被破壞?獨立組件或許有測試可以覆蓋到本身的功能,但這不意味着合併以後的功能依舊正常,因而在 App 中,咱們彷佛還須要端到端的測試來保證交付功能的正常

若是團隊若是真的獨立開發組件的話,我建議在組件的發佈階段加上 pipeline,持續集成以免影響其餘功能

Responsibility and Team Work

使用組件聚合最(令我)頭疼的問題之一,是如何爲組件找到對應的團隊負責,以及如何在組件聚合的模式下劃分團隊。

團隊劃分一般有兩類劃分模式,這兩種模式的叫法有不少,我在這裏姑且稱之爲 Component Team(如下簡稱 CT) 和 Feature Team(有如下簡稱 FT)

  • Component Team: 康威定律告訴咱們組織的溝通方式會在系統設計上有所表達。若是你有四個小組開發編譯器,那麼你會獲得一個四步編譯器。CT 模式即組織和架構一致。在這個模式下團隊的劃分是按照分層架構或者說垂直技術棧進行劃分的,例如前端、後端和運維。CT 模式的問題首先在於領域知識散落在不一樣的技術架構中,產生了耦合;其次在須要協同工做的狀況下缺乏 ownership,每一個團隊只關心本身的KPI,缺乏知識的共享和傳承

  • Feature Team: 這個模式也被成爲逆康威模式(Inverse Conway Maneuver),團隊按照業務架構而非技術架構進行劃分,一個團隊負責單一業務上的功能,可是在技術上,它們能夠須要同時修改端到端的代碼以及多個微服務,你能夠理解爲全棧。這個模式的問題是,在容許多個團隊修改同一個服務的狀況,缺乏服務 owner 容易致使服務代碼的質量降低

在敏捷開發和 DDD 的影響之下 FT 模式逐漸變得流行。我我的也推薦 FT 模式,由於我曾在某司深受 CT 模式其害,當組織越龐大,垂直的組織壁壘就越多,你能想象我在某司的時候運維部門的最大願望是但願咱們不要上線嗎

然而在使用組件聚合的狀況下,咱們應該如何劃分組件和團隊?

首先我不同意上面例子中如此細粒度的劃分技術棧的劃分組件和團隊。這樣的會致使每一個如此之小的功能的修改都要涉及好幾個(未知)團隊的協做開發,CT 模式下的壁壘又從新顯現。我更加反對將 componets 在公司內部做爲獨立的組件庫由獨立的團隊進行開發,這會致使業務團隊與組件團隊沒法對齊。

那 FT 模式呢?當我在設想以 FT 模式進行組件劃分時,我又陷入了一種粒度的糾結當中。以上面選擇商品而且結算爲例,該團隊負責的範圍就此爲止了嗎?若是下方還有商品評論和商品推薦的相關內容,我是否應該繼續交給這個團隊繼續負責?

我認爲 DDD 是多是一個解藥。DDD 理論可以幫助咱們劃分出不一樣的領域模型,幫助咱們界定上下文。好比商品的購買屬於核心域,可是商品的評論屬於支撐子域。這樣咱們就有理由不將它們交給一個 FT 團隊負責。這樣前端團隊和後端也方便對齊。

注意在 DDD 的模式下請避免組件的跨域複用,這會致使上下文和領域的重疊。

另外若是以 DDD 劃分的話,說不定由於範圍夠大而致使組件聚合升級成了 App 聚合

以組件爲聚合的解決方案

很多微前端解決方案基本都是以組件爲劃分的,不過它們定義的組件和咱們理解的組件並不相同。最終的解決思路又十分類似

IKEA

你沒看錯,宜家

Experiences Using Micro Frontends at IKEA 一文中,宜家架構師 Kotte 介紹它們採用了一種相似於 transclusion mechanism 的形式。客戶端 transclusion 的例子即是圖片標籤。標籤擁有 src 屬性用於指向一個 URL。瀏覽器會在渲染時將改標籤替換爲一個真實的圖片。

在服務端他們的 Edge Side Includes(ESI) 便對應圖片標籤,不過指向的不是圖片而是 HTML。他們擁有頁面 (Page) 和碎片 (Fragment) 的概念,一個團隊同時須要負責碎片和頁面的開發,頁面經過 ESI 引用那些碎片。碎片的引用是跨越團隊邊界的。好比一個產品團隊擁有產品縮略圖的的碎片,其餘的團隊就能夠引用這個縮略圖碎片而不用本身再重寫相同的功能。

由於頁面由不一樣團隊的的碎片組成,可能使用的不一樣技術,爲了可以使它們組件時相互兼容,團隊採用了一種自包含(self-contained)技術,即碎片自己就包含了它本身須要的全部資源,好比 CSS 和 Javascript,可以獨立運行,而不須要思考碎片的依賴。

OpenComponents

OpenComponents(如下簡稱 OC) 是一種端到端的解決方案。在關於 OC 的Architecture overview中,項目開宗明義的指出:

OpenComponents' heart is a REST API. It is used for consuming and publishing components.

你能夠把它理解爲一個進階版的 npm 系統。除了是獨立的組件包以外,它還封裝了業務請求,甚至已經渲染完畢,加載即用,不須要再二次開發。若是你須要在頁面上引用使用一個縮略圖功能的 OC, 只須要在頁面引用

<oc-component href="http://localhost/thumbnails">
複製代碼

目前解決微前端的另外一個思路是將前端是站在消費者的角度考慮聚合:可能模塊 A 是由 React 編寫,模塊 B 是由 Vue 編寫,不要緊在服務端統一編譯成瀏覽器須要 html 與 es5 碎片返回,最終將它們組合再一塊兒,對於編排層來講一視同仁。OC 是這個思路,宜家也是這個思路,

Project Mosaic

Mosaic 是整套的從微服務到微前端的解決方案。從它官網的圖例即可以理解它的架構:

它也是經過組件化加碎片化的方式聚合前端

這三種方案都沒有明確說明如何解決我上面提出的各類問題。

結語

最後借用 Simon Brown 的一條 twitter 來結束這篇文章:

I'll keep saying this ... if people can't build monoliths properly, microservices won't help.

若是你連單體應用都寫很差,微前端也幫不上什麼忙


本文同時也發佈在個人知乎專欄,歡迎關注

相關文章
相關標籤/搜索