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

微前端:將來前端開發的新趨勢 — 第三部分

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

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

跨應用通訊

關於微前端最多見的問題就是如何讓它們互相交流。通常來講,咱們建議通訊越少越好,由於這一般會從新引入不恰當的耦合,而這種耦合是咱們首先想要避免的。node

也就是說,某種程度上的通訊一般是須要的。自定義事件容許微前端直接通訊,這是最小化直接耦合的好方法,雖然它確實使肯定和執行微前端之間存在的約定變得更加困難。另外,向下傳遞迴調和數據(在這裏是從容器應用向下到微前端)的 React 模型也是一種好方法,它使得約定更加明確。第三種方法是使用地址欄做爲通訊機制,後面咱們會談到更多細節react

若是你在使用 redux,經常使用的方法是創建一個對於整個應用單1、全局、共享的 store。然而,若是微前端都應該是它本身的獨立應用,那麼它們每一個都有本身的 redux store 是有意義的。Redux 文檔甚至提到「將 Redux 應用程序隔離爲更大應用程序中的組件」做爲擁有多個 store 的正當理由。android

不管選擇哪一種方式,咱們但願微前端經過發送消息或者事件來彼此通訊,避免任何狀態共享。就像跨微服務共享數據庫,只要咱們共享數據結構和領域模型,就會產生大量的耦合,這會變得很是難以維護。webpack

與樣式同樣,有幾種不一樣的方法能夠在這方面起到很好的做用。最重要的事情是對你正在引入的耦合考慮深遠,以及你將如何保持約定。就像微服務之間的集成同樣,若是沒有跨不一樣應用程序和團隊的協調升級過程,你就沒法對集成作出重大變動。ios

你也應該考慮如何自動驗證集成沒有掛掉。功能測試是一種方式,但咱們更但願限制編寫功能測試的數量,以便控制實現和維護它們的成本。或者你能夠實施某種形式的消費者驅動的約定,這樣每一個微前端能夠指定它對於其餘微前端的依賴,無需在瀏覽器中實際集成和運行它們。git


後端通訊

若是咱們有獨立的團隊在前端應用程序上獨立工做,那麼後端開發呢?咱們很是相信全棧團隊的價值,他們從可視化代碼到 API 開發、數據庫和基礎架構代碼負責整個應用的開發。BFF 模式在這裏發揮了做用,每個前端應用都有一個對應的後端來單獨知足前端的需求。雖然 BFF 模式最初可能意味着每一個前端通道(web、mobile 等)的專用後端,它能夠很容易地擴展爲每個微前端的後端。github

這裏有不少因素須要考慮。BFF 多是自包含的,具備本身的業務邏輯和數據庫,或者也可能只是一個下游服務的聚合器。若是有下游服務,那麼擁有微前端及其 BFF 的團隊可能有或可能沒有意義,來擁有一些這樣的服務。若是微前端只有一個與之通信的 API ,而且它至關穩定,那麼構建 BFF 就根本沒有多大價值了。有一個指導原則是,團隊構建一個特定微前端時不該該必須得等其餘團隊來爲他們構建東西。所以若是每當給微前端添加新功能時也要求後端更改,那麼由同一個團隊擁有的 BFF 就是一個很好的案例。web

該圖表顯示三對前端/後端。第一個後端只與本身的數據庫對話。其餘兩個後端與共享下游服務進行通訊。兩種方法都是有效的

圖 7:有不少不一樣的方式構建你的前/後端關係

其餘常見問題有,應該如何經過服務器對微前端應用的用戶進行身份驗證和受權?明顯咱們的用戶應該只須要認證一次,所以鑑權一般成爲屬於容器應用擁有的普遍關注的問題。容器可能有某種登陸形式,咱們經過它得到某種令牌。令牌由容器保存,能夠在初始化時注入到每一個微前端中。最終,微前端能夠在任何發送給服務器的請求中攜帶令牌,而後服務器就能夠執行任何須要的驗證。


測試

在測試方面,咱們認爲笨重前端和微前端之間沒有太大區別。一般來說,你用來測試笨重前端的任何策略均可以應用於每一個微前端。也就是說,每一個微前端都應該有本身全面的自動化測試套件來保證代碼的質量和正確性。

明顯的障礙是各類微前端與容器應用的集成測試。這個可使用你喜歡的功能/端對端測試工具(好比 Selenium 或 Cypress)來完成,可是不要過分使用。功能測試應該只涵蓋沒法在測試金字塔較低級別測試的方面。咱們的意思是,使用單元測試涵蓋低級別業務邏輯和渲染邏輯,功能測試只用來驗證頁面是否正確渲染。例如,你能夠在特定 URL 上加載徹底集成的應用程序,並斷言相應微前端的硬編碼標題出如今頁面上。

若是用戶的使用跨越微前端,那麼你能夠用功能測試來測試這些,但要保證功能測試專一於驗證前端的整合,而不是每一個微前端的內部業務邏輯,這應該已經被單元測試所涵蓋。正如剛纔提到的,用戶驅動的約定有助於直接指定微前端之間發生的交互,而不會出現集成環境和功能測試的瑕疵。


案例詳解

本文後面的大部份內容將詳細解釋咱們的示例應用程序實現的一種方式。咱們將重點關注容器應用和微前端如何使用 JavaScript 整合在一塊兒,這多是最有趣也最複雜的部分。你能夠在 demo.microfrontends.com 看到實時部署的最終結果,全部源代碼均可以在 Github 上看到。

整個微前端示例應用的首頁「概覽」截圖

圖 8:整個微前端示例應用的首頁「概覽」

該示例徹底使用 React 開發,有必要說明的是,React 沒有壟斷這個架構。可使用許多不一樣的工具或框架來實現微前端。這裏咱們使用 React 是由於它的受歡迎程度以及咱們對它的熟悉程度。

容器

咱們從容器開始,由於它是咱們用戶的入口。讓咱們從它的 package.json 中看看能夠發現什麼:

{
  "name": "@micro-frontends-demo/container",
  "description": "Entry point and container for a micro frontends demo",
  "scripts": {
    "start": "PORT=3000 react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test"
  },
  "dependencies": {
    "react": "^16.4.0",
    "react-dom": "^16.4.0",
    "react-router-dom": "^4.2.2",
    "react-scripts": "^2.1.8"
  },
  "devDependencies": {
    "enzyme": "^3.3.0",
    "enzyme-adapter-react-16": "^1.1.1",
    "jest-enzyme": "^6.0.2",
    "react-app-rewire-micro-frontends": "^0.0.1",
    "react-app-rewired": "^2.1.1"
  },
  "config-overrides-path": "node_modules/react-app-rewire-micro-frontends"
}
複製代碼

reactreact-scripts 依賴能夠看出它是經過 create-react-app 建立的 React 應用。更有趣的是那沒有的:任何說起咱們將要組成以造成咱們的最終應用程序的微前端。若是咱們在這裏將它們指定爲庫依賴項,那麼咱們將走向構建時集成的道路,那就會像以前提到的會致使在咱們的發佈週期中有問題的耦合。

react-scripts 1.x 版本能夠在單個頁面中擁有多個應用而不產生衝突,但在 2.x 版本使用一些 webpack 特性,當兩個以上應用在單個頁面渲染時會致使錯誤。基於這個緣由咱們使用 react-app-rewired 覆蓋一些 webpack 內部的 react-scripts 配置。它會修復這些錯誤,讓咱們繼續依靠 react-scripts 來管理咱們的構建工具。

爲了瞭解咱們如何選擇和展現微前端,咱們來看一下 App.js。咱們使用 React Router 將當前 URL 與預約義的路由列表進行匹配,而且渲染相應組件:

<Switch>
  <Route exact path="/" component={Browse} />
  <Route exact path="/restaurant/:id" component={Restaurant} />
  <Route exact path="/random" render={Random} />
</Switch>
複製代碼

Random 組件不那麼有趣 —— 它只是重定向到隨機選擇的餐廳 URL 對應的頁面。BrowseRestaurant 是這樣:

const Browse = ({ history }) => (
  <MicroFrontend history={history} name="Browse" host={browseHost} />
);
const Restaurant = ({ history }) => (
  <MicroFrontend history={history} name="Restaurant" host={restaurantHost} />
);
複製代碼

這兩種狀況,咱們渲染 MicroFrontend 組件。除了 history 對象(後面會變得重要),咱們指定應用的惟一名稱,以及 bundle 下載的主機地址。在本地運行時,這個配置驅動的 URL 相似於 http://localhost:3001,生產環境則相似 https://browse.demo.microfrontends.com

App.js 中選擇了一個微前端,如今咱們將在 MicroFrontend.js 渲染它,這只是另外一個 React 組件:

class MicroFrontend extends React.Component {
  render() {
    return <main id={`${this.props.name}-container`} />;
  }
}
複製代碼

這不是完整的類,咱們很快會看到它更多的方法。

渲染時,咱們要作的就是在頁面上放置帶有微前端惟一 ID 的容器元素。這是咱們告訴微前端渲染本身的地方。咱們使用 React 的 componentDidMount 做爲下載和渲染微前端的觸發器:

componentDidMount 是 React 組件的生命週期函數,它只會在組件實例首次在 DOM 中「渲染」時被框架調用。

MicroFrontend 類……

componentDidMount() {
    const { name, host } = this.props;
    const scriptId = `micro-frontend-script-${name}`;

    if (document.getElementById(scriptId)) {
      this.renderMicroFrontend();
      return;
    }

    fetch(`${host}/asset-manifest.json`)
      .then(res => res.json())
      .then(manifest => {
        const script = document.createElement('script');
        script.id = scriptId;
        script.src = `${host}${manifest['main.js']}`;
        script.onload = this.renderMicroFrontend;
        document.head.appendChild(script);
      });
  }
複製代碼

咱們必須從靜態清單文件中獲取腳本的 URL,由於 react-scripts 輸出的編譯後 JavaScript 文件名中包含便於緩存的哈希值。

首先咱們檢查有惟一 ID 的相關腳本是否已經下載,若是下載了,咱們能夠當即渲染它。若是沒有,咱們獲取從適當的主機獲取 asset-manifest.json 文件,以便查找主腳本資產的完整 URL。一旦咱們設置了腳本的 URL,剩下的就是將它附加到文檔中,使用 onload 處理程序渲染微前端:

MicroFrontend 類

renderMicroFrontend = () => {
    const { name, history } = this.props;

    window[`render${name}`](`${name}-container`, history);
    // E.g.: window.renderBrowse('browse-container, history');
  };
複製代碼

在上面的代碼中咱們調用了 window.renderBrowse 全局函數,它被咱們剛剛下載的腳本放在那裏。咱們給微前端應該渲染的 <main> 元素分配一個 ID 和 history 對象,咱們很快會解釋。這個全局函數的簽名是容器應用和微前端之間的關鍵約定。這是任何通信或集成應該發生的地方,所以保持它至關輕量級使其易於維護,並在將來添加新的微前端。每當咱們想要作一些須要更改此代碼的事情時,咱們應該仔細地思考它對於咱們的代碼庫的耦合以及約定的維護意味着什麼。

最後一件是處理清理工做。當咱們的 MicroFrontend 組件卸載時(從 DOM 中移除),咱們也想卸載相應的微前端。爲此,每一個微前端都定義了一個相應的全局函數,咱們在適當的 React 生命週期方法中調用它:

MicroFrontend 類……

componentWillUnmount() {
    const { name } = this.props;

    window[`unmount${name}`](`${name}-container`);
  }
複製代碼

就它自己的內容而言,容器直接渲染的全部內容是網站的頂層頭部和導航欄,由於這些在全部頁面中都是不變的。這些元素的 CSS 已通過仔細編寫,以確保它只對標題中的元素進行樣式化,因此它不該該與微前端內的任何樣式代碼衝突。

這就是容器應用的結尾!它至關初級,但這給了咱們一個 shell,能夠在運行時動態下載咱們的微前端,並將它們粘合在一塊兒造成一個單一頁面上的內容。這些微前端能夠單獨部署在生產上,無需改變任何其餘微前端或容器自己。

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

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


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

相關文章
相關標籤/搜索