[譯] 微前端:將來前端開發的新趨勢 — 第四部分

作好前端開發不是件容易的事情,而比這更難的是擴展前端開發規模以便於多個團隊能夠同時開發一個大型且複雜的產品。本系列文章將描述一種趨勢,能夠將大型的前端項目分解成許多個小而易於管理的部分,也將討論這種體系結構如何提升前端代碼團隊工做的有效性和效率。除了討論各類好處和代價以外,咱們還將介紹一些可用的實現方案和深刻探討一個應用該技術的完整示例應用程序。javascript

建議按照順序閱讀本系列文章:html

跨應用通訊

微前端

咱們從咱們一直引用的全局渲染函數繼續。咱們應用的主頁是個可篩選的餐館列表,入口代碼以下所示:前端

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

window.renderBrowse = (containerId, history) => {
  ReactDOM.render(<App history={history} />, document.getElementById(containerId));
  registerServiceWorker();
};

window.unmountBrowse = containerId => {
  ReactDOM.unmountComponentAtNode(document.getElementById(containerId));
};
複製代碼

一般在 React.js 應用中,ReactDOM.render 的調用會在最頂層做用域,這意味着一旦腳本文件加載完成,它就會當即在一個硬編碼的 DOM 元素中開始渲染。對於這個應用,咱們須要可以同時控制渲染髮生的時間和位置,因此咱們將它包裹在一個接收 DOM 元素 ID 做爲參數的函數裏,並把這個函數添加到全局的 window 對象。相應的,咱們還能夠看到用於清理的 un-mounting 函數。java

雖然咱們已經看到當微前端集成到整個容器應用時這個函數將被如何調用,但這裏成功的最大標準之一是咱們是否可以獨立地開發和運行微前端。因此每一個微前端也有本身的 index.html,它帶有內聯的腳本,以便於在容器外以「獨立」模式渲染應用。react

<html lang="en">
  <head>
    <title>Restaurant order</title>
  </head>
  <body>
    <main id="container"></main>
    <script type="text/javascript">
      window.onload = () => {
        window.renderRestaurant('container');
      };
    </script>
  </body>
</html>
複製代碼

`order` 頁在容器外獨立運行的截圖

圖 9:每一個微前端均可以在容器外獨立運行。android

在此以前,微前端大部分只是普通的舊 React 應用。browse 應用從後端拉取餐廳列表,提供 input 標籤用於搜索和篩選,並渲染 React Router <Link> 元素導航到特定餐館。這時咱們要切換到 order 微前端,它負責渲染單獨的餐廳及其菜單。webpack

一個架構圖,顯示了上文導航的一系列步驟

圖 10:這些微前端僅經過路由變換相互做用,並不直接交互ios

有關咱們的微前端,最後一件值得一提的事是他們都使用了 styled-component 來完成它們全部的樣式。這個 CSS-in-JS 庫能夠很容易地將樣式和特定組件關聯,這樣咱們能夠保證微前端的樣式不會泄露以影響容器或其餘微前端的樣式。git

基於路由的跨應用通訊

咱們較早前提到跨應用通訊應該儘可能避免。在這個例子中,咱們惟一(須要跨應用通訊)的需求是瀏覽頁須要告訴詳情頁加載哪一個餐館。這裏咱們將看到怎樣用客戶端路由去解決這個問題。github

這裏涉及的三個 React 應用都使用了 React Router 進行聲明式路由,可是經過兩種略有差異的方式進行初始化。容器應用中,咱們建立了一個 <BrowserRouter>,它在內部會實例化一個 history 對象。這是咱們以前一直在「掩飾」的 history 對象。咱們用這個對象操做客戶端歷史記錄,同時咱們也用它將不一樣的 React Router 鏈接起來。在咱們的微前端裏,咱們像這樣初始化 Router:

<Router history={this.props.history}>
複製代碼

在這種狀況下,咱們使用的是容器應用的 history 實例,而並不會讓路由組件本身實例化一個新的 history 對象。全部的 <Router> 實例如今都已經鏈接在一塊兒,所以任何實例觸發的路由變化都會反映在全部實例中。這爲咱們提供了一種經過 URL 將「參數」從一個微前端傳遞到另外一個微前端的簡單方法。例如在 browse 微前端,咱們有一個這樣的連接:

<Link to={`/restaurant/${restaurant.id}`}>
複製代碼

當這個連接被點擊時,容器中的路由會被更新,該容器會看到新的 URL 而後決定掛載以及渲染 restaurant 微前端。該微前端本身的路由邏輯將從 URL 提取 restaurant ID 並渲染正確的信息。

但願這個示例流程展現了簡潔 URL 的靈活性及強大功能。除了便於分享與作書籤之外,在這個特定的架構裏,它還能夠是微前端互相交流意圖的有用方式。基於這個目的使用 URL 有如下許多優點:

  • 其結構是定義明確的開放標準
  • 頁面上全部代碼皆可訪問
  • 其有限的空間鼓勵咱們僅發送少許數據
  • 它面向用戶,鼓勵咱們誠實地依據域組織數據模型
  • 它是聲明式的,非命令式。即它僅表示「這是咱們所在的地方」而非「請作這件事」
  • 它迫使微前端間接進行通訊,而非直接互相瞭解互相依賴

當咱們使用路由做爲微前端通訊的模式時,咱們選擇的路由構成了一個合約。在這種狀況下,咱們規定能夠在 /restaurant/:restaurantId 查看一個餐廳的詳細信息,而且咱們沒法在不更新全部引用該路由的應用的狀況下更新路由。鑑於此合約的重要性,咱們應該進行自動化測試以檢查合約是否被遵照。

公用內容

雖然咱們但願咱們的團隊以及咱們的微前端儘量獨立,但有些事情仍應該是共有的。咱們以前寫過關於公用組件庫如何幫助實現微前端的一致性的文章,但對於這些小 demo 來講公用組件庫有點殺雞用牛刀。因此做爲替代,咱們有一個小的公用內容倉庫,包含圖像,JSON 數據和 CSS,它們經過網絡提供給全部微前端。

還有同樣東西咱們能夠選擇在微前端之間共享:依賴庫。正如咱們將簡要描述,依賴的重複是微前端的常見缺點。即使在應用之間共享依賴也有自身的一些困難,但對於這個 demo 來講,談論一下如何完成它是值得的。

第一步是選擇要共享的依賴。編譯後代碼的快速分析顯示咱們 50% 的 bundle 體積是由 react 和 react-dom 貢獻的。拋開他們的體積不談,這兩個庫是咱們最「核心」的依賴,所以咱們知道全部微前端均可以從提取它們成爲公共依賴中受益。最後,這些都是穩定、成熟的庫,一般只在兩個主版本間引入重大更改,所以跨應用更新不會太困難。

至於實際提取操做,咱們要作的就是在 webpack config 中將庫標記爲 externals,咱們能夠抄抄前面

module.exports = (config, env) => {
  config.externals = {
    react: 'React',
    'react-dom': 'ReactDOM'
  }
  return config;
};
複製代碼

而後咱們在每一個 index.html 中加幾個 script 標籤,從咱們的共享內容服務器中獲取這兩個庫。

<body>
  <noscript>
    You need to enable JavaScript to run this app.
  </noscript>
  <div id="root"></div>
  <script src="%REACT_APP_CONTENT_HOST%/react.prod-16.8.6.min.js"></script>
  <script src="%REACT_APP_CONTENT_HOST%/react-dom.prod-16.8.6.min.js"></script>
</body>
複製代碼

在團隊中共享代碼一直是個很難作好的事情。咱們要確保咱們只共享咱們真正想公用且一次須要修改多處地方的東西。若是咱們對咱們共享與不共享的事情保持謹慎,咱們將從中真正受益。

基礎設施

該應用託管在 AWS,擁有核心基礎設施(S3 存儲桶,CloudFront 分配,域名,證書等),使用 Terraform 的集中倉庫一次性配置。每一個微前端都有本身的源碼倉庫,在 Travis CI 上有本身的連續部署管道,用於構建、測試以及部署靜態資源到 S3 存儲桶中。這平衡了集中式基礎架構管理的便利性與獨立部署的靈活性。

請注意,每一個微前端(以及容器)都有本身的存儲桶。這意味着它們能夠自由控制裏面的內容,咱們不須要擔憂來自其餘團隊或應用的對象名稱衝突或衝突的訪問規則。


缺點

在本文開頭,咱們提到過微前端技術的取捨,就像任何架構同樣。咱們提到過的好處確實有成本,咱們將在此介紹。

負載體積

獨立構建的 Javascript bundle 會形成公共依賴的重複,從而增長了咱們必須經過網絡發送給最終用戶的字節數。例如,若是每一個微前端都包含了本身的 React 副本,那麼咱們將迫使用戶下載 React n 次。頁面性能與用戶參與/轉換有直接關係,而且世界上大部分地區運行在比發達城市慢得多的網絡設施上,因此咱們有不少理由關心下載體積。

這個問題不容易解決。一方面咱們但願團隊獨立編譯他們的應用以便自主工做,另外一方面又咱們但願他們能夠共享公共依賴,這二者之間存在內在的緊張關係。一種解決辦法是像咱們前面 demo 描述的,將咱們編譯後代碼的常見依賴外置。一旦咱們沿着這條路走下去,咱們將從新引入一些微前端之間構建過程的耦合。如今它們之間有着一個隱含的合約:「咱們都必須使用這些依賴的明確版本」。若是其中一個依賴產生重大改動,咱們可能最終須要一個大的協調升級工做以及一次性的同步發版。這是咱們使用微前端最初想要避免的一切!

內在的緊張關係是個困難的問題,但並不全是壞消息。首先,即使咱們對於重複的依賴不採起任何措施,每一個單獨頁面仍可能比咱們構建整個前端更快地加載。緣由是經過獨立編譯每一個頁面,咱們有效地以咱們本身的形式實現了代碼分割。在傳統的前端中,應用中的任何頁面加載完成時,咱們一般會一次性下載全部頁面的源碼和依賴。經過獨立構建,任何單獨的頁面加載將只會下載那個頁面的源碼和依賴。這可能致使更快的首頁加載,但隨後的導航速度會變慢,由於用戶必須在每一個頁面上從新下載相同的依賴。若是咱們嚴格地不用沒必要要的依賴使咱們的微前端膨脹,或者咱們知道用戶在應用中一般訪問的一兩個頁面,即使有重複依賴,咱們也極可能在性能方面達到淨增益

在前一段有不少「可能」和「也許」,代表了每一個應用一般都有它們本身獨特的性能特徵。若是你想確切地知道特定的變化會形成什麼性能影響,只能靠實際測量,並且最好是在生產環境中。咱們見過不少團隊僅僅爲了下載數兆大小的高清圖像或者對一個運行很是慢的數據庫進行昂貴的查詢額外多寫幾千字節的 JavaScript 代碼。所以,儘管考慮每一個架構決策的性能影響很重要,但請確保你知道真正的瓶頸在哪裏。

環境差別

咱們應該可以開發一個單一的微前端,而無需考慮其餘團隊正在開發的全部其它微前端。咱們可能甚至應該在「獨立」模式下,在空白頁面上運行咱們的微前端,而不是運行在將在生產環境中承載微前端的容器應用內部。這可使開發變得更加簡單,特別是當真正的容器是一個複雜的遺留代碼庫的時候,而一般狀況下咱們使用微前端來逐步從舊世界遷移到新世界。可是,在與生產環境徹底不一樣的環境中開發存在風險。若是咱們的開發時容器與生產容器的行爲不一樣,那麼咱們可能會發現咱們的微前端被破壞,或者在咱們部署到生產環境時表現不一樣。特別值得關注的是可能由容器或其餘微前端帶來的全局樣式。

這裏的解決方案與咱們不得不擔憂環境差別的任何其餘狀況沒有什麼不一樣。若是咱們在一個與生產環境不一樣的本地環境開發,咱們須要確保按期將咱們的微前端集成和部署到像生產環境的環境中,而且咱們應該在這些環境中進行測試(手動以及自動化)以儘早發現集成問題。這不會徹底解決問題,但最終這是一個取捨:簡化開發環境的生產力提高是否值得冒集成出問題的風險?答案取決於項目!

運維複雜度

最後的缺點是與微服務直接平行的缺點。做爲一個更加分散的架構,微前端將不可避免地致使須要管理更多的東西 —— 更多的存儲庫,更多的工具,更多的構建/部署管道,更多的服務器,更多的域等等。所以,在採用這樣的架構以前,你應該考慮幾個問題:

  • 你是否有足夠的自動化可行地提供以及管理額外所需的基礎設施?
  • 你的前端開發、測試和發佈進程是否會擴展到許多應用中?
  • 你是否對圍繞工具和開發的實踐變得更加分散且不易控制的決策感到滿意?
  • 你將如何確保你的多個獨立前端代碼庫中的最低代碼質量,一致性或代碼管理?

咱們可能會另寫一篇文章討論這些主題。咱們但願提出的主要觀點是,當你選擇微前端時,根據定義,你選擇建立許多小東西而不是一個總體。你應該考慮你是否有采用這種方法所需的技術和組織成熟度,從而不形成混亂。


總結

隨着前端代碼庫在過去幾年中變得愈來愈複雜,咱們看到對更具可擴展性的架構的需求不斷增加。咱們須要可以劃清界限,在技術和域實體之間創建正確的耦合和內聚層級。咱們應該可以跨獨立開發團隊可擴展地進行軟件交付。

雖然遠非惟一方案,但咱們已經看到許多微前端提供了這些好處的真實案例,而且咱們已經可以逐漸將該技術應用於遺留代碼庫以及新代碼庫。不管微前端是否適合你和你的組織,咱們只能但願這將成爲持續趨勢的一部分,在這個趨勢中,前端工程化和前端架構以咱們應有的嚴肅性被對待。


若是您發現此文章有用,請分享它。我很感激反饋與鼓勵。

致謝

很是感謝 Charles Korn,Andy Marks,和 Willem Van Ketwich 的全面審校和反饋。

同時感謝 Bill Codding,Michael Strasser,和 Shirish Padalkar 在 ThoughtWorks 內部郵件列表提供的意見。

感謝 Martin Fowler 的反饋意見,併發布在本身的網站上給了文章一個家。

最後,感謝 Evan Bottcher 和 Liauw Fendy 的鼓勵和支持。

建議按照順序閱讀本系列文章:

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索