微前端入門

最近在學習一些微前端的知識,轉載一片我的以爲比較好的。javascript

基本概念

微前端能夠理解爲,一個很大的前端應用,將此應用總體分解,分解成多個項目,每一個項目交給一個團隊,各團隊能夠獨立開發、測試、部署和維護,其實用什麼方法都沒有明確規定,最終的目的是要達到代碼隔離和團隊自治便可。html

微前端實現的一些方法

1. 後端模板集成

咱們用一個很是傳統的方式開始,將多個模板渲染到服務器上的HTML裏。咱們有一個index.html,其中包含全部常見的頁面元素,而後使用 include 來引入其餘模板:前端

<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>Feed me</title>
  </head>
  <body>
    <h1>🍽 Feed me</h1>
    <!--# include file="$PAGE.html" -->
  </body>
</html>

而後配置 nginxjava

server {
    listen 8080;
    server_name localhost;

    root /usr/share/nginx/html;
    index index.html;
    ssi on;

    # 將 / 重定向到 /browse
    rewrite ^/$ http://localhost:8080/browse redirect;

    # 根據路徑訪問 html 
    location /browse {
      set $PAGE 'browse';
    }
    location /order {
      set $PAGE 'order';
    }
    location /profile {
      set $PAGE 'profile'
    }

    # 全部其餘路徑都渲染 /index.html
    error_page 404 /index.html;
}

這是至關標準的服務器端應用。咱們之因此能夠稱其爲微前端,是由於咱們讓每一個頁面獨立,可由一個獨立的團隊交付。node

爲了得到更大的獨立性,能夠有一個單獨的服務器負責渲染和服務每一個微型前端,其中一個服務器位於前端,向其餘服務器發出請求。經過緩存,能夠把延遲降到最低。react

2. 使用package集成,即將小應用打包成npm包

有人會用到的一種方法是將每一個微前端發佈爲一個 node 包,並讓容器應用程序將全部微前端應用做爲依賴項。好比這個 package.json:webpack

{
  "name": "@feed-me/container",
  "version": "1.0.0",
  "description": "A food delivery web app",
  "dependencies": {
    "@feed-me/browse-restaurants": "^1.2.3",
    "@feed-me/order-food": "^4.5.6",
    "@feed-me/user-profile": "^7.8.9"
  }
}

乍看彷佛沒什麼問題,這種作法會產生一個可部署的包,咱們能夠輕鬆管理依賴項。
可是,這種方法意味着咱們必須從新編譯併發布每一個微前端應用,才能發佈咱們對某個應用做出的更改。咱們強烈不建議使用這種微前端方案。nginx

3. iframe集成

iframe 是集成的最簡單方式之一。本質上來講,iframe 裏的頁面是徹底獨立的,能夠輕鬆構建。並且 iframe 還提供了不少的隔離機制。git

<html>
  <head>
    <title>Feed me!</title>
  </head>
  <body>
    <h1>Welcome to Feed me!</h1>

    <iframe id="micro-frontend-container"></iframe>

    <script type="text/javascript">
      const microFrontendsByRoute = {
        '/': 'https://browse.example.com/index.html',
        '/order-food': 'https://order.example.com/index.html',
        '/user-profile': 'https://profile.example.com/index.html',
      };

      const iframe = document.getElementById('micro-frontend-container');
      iframe.src = microFrontendsByRoute[window.location.pathname];
    </script>
  </body>
</html>

iframe 並非一項新技術,因此上面代碼也許看起來並不那麼使人興奮。
可是,若是咱們從新審視先前列出的微前端的主要優點,只要咱們謹慎地劃分微應用和組建團隊的方式,iframe便很適合。
咱們常常看到不少人不肯意選擇iframe。由於 iframe有點使人討厭,但 iframe 實際上仍是有它的優勢的。上面提到的容易隔離確實會使iframe不夠靈活。它會使路由、歷史記錄和深層連接變得更加複雜,而且很難作成響應式頁面。github

4. 使用js集成

這種方式多是最靈活的一種,也是被採用頻率最高的一種方法。每一個微前端都對應一個 <script> 標籤,而且在加載時導出一個全局變量。而後,容器應用程序肯定應該安裝哪些微應用,並調用相關函數以告知微應用什麼時候以及在何處進行渲染。

<html>
  <head>
    <title>Feed me!</title>
  </head>
  <body>
    <h1>Welcome to Feed me!</h1>

    <!-- 這些腳本不會立刻渲染應用 -->
    <!-- 而是分別暴露全局變量 -->
    <script src="https://browse.example.com/bundle.js"></script>
    <script src="https://order.example.com/bundle.js"></script>
    <script src="https://profile.example.com/bundle.js"></script>

    <div id="micro-frontend-root"></div>

    <script type="text/javascript">
      // 這些全局函數是上面腳本暴露的
      const microFrontendsByRoute = {
        '/': window.renderBrowseRestaurants,
        '/order-food': window.renderOrderFood,
        '/user-profile': window.renderUserProfile,
      };
      const renderFunction = microFrontendsByRoute[window.location.pathname];

      // 渲染第一個微應用
      renderFunction('micro-frontend-root');
    </script>
  </body>
</html>

上面是一個很基本的例子,演示了 JS 集成的大致思路。
與 package 集成不一樣,咱們能夠用不一樣的bundle.js獨立部署每一個應用。
與 iframe 集成不一樣的是,咱們具備徹底的靈活性,你能夠用 JS 控制何時下載每一個應用,以及渲染應用時額外傳參數。
這種方法的靈活性和獨立性使其成爲最經常使用的方案。當咱們展現完整的示例時,會有更詳細的探討。

5. 經過 Web Component 集成

這是前一種方法的變體,每一個微應用對應一個 HTML 自定義元素,供容器實例化,而不是提供全局函數。

<html>
  <head>
    <title>Feed me!</title>
  </head>
  <body>
    <h1>Welcome to Feed me!</h1>

     <!-- 這些腳本不會立刻渲染應用 -->
    <!-- 而是分別提供自定義標籤 -->
    <script src="https://browse.example.com/bundle.js"></script>
    <script src="https://order.example.com/bundle.js"></script>
    <script src="https://profile.example.com/bundle.js"></script>

    <div id="micro-frontend-root"></div>

    <script type="text/javascript">
      // 這些標籤名是上面代碼定義的
      const webComponentsByRoute = {
        '/': 'micro-frontend-browse-restaurants',
        '/order-food': 'micro-frontend-order-food',
        '/user-profile': 'micro-frontend-user-profile',
      };
      const webComponentType = webComponentsByRoute[window.location.pathname];

      // 渲染第一個微應用(自定義標籤)
      const root = document.getElementById('micro-frontend-root');
      const webComponent = document.createElement(webComponentType);
      root.appendChild(webComponent);
    </script>
  </body>
</html>

主要區別在於使用 Web Component 代替全局變量。若是你喜歡 Web Component 規範,那麼這是一個不錯的選擇。若是你但願在容器應用程序和微應用之間定義本身的接口,那麼你可能更喜歡前面的示例。

可能遇到的問題

1. 樣式

CSS 沒有模塊系統、命名空間和封裝。就算有,也一般缺少瀏覽器支持。在微前端環境中,這些問題會更嚴重。

例如,若是一個團隊的微前端的樣式表爲 h2 { color: black; },而另外一個團隊的則爲 h2 { color: blue; },而這兩個選擇器都附加在同一頁面上,就會衝突!

這不是一個新問題,但因爲這些選擇器是由不一樣的團隊在不一樣的時間編寫的,而且代碼可能分散在不一樣的庫中,所以更難避免。

多年來,有許多方法可讓 CSS 變得更易於管理。有些人選擇使用嚴格的命名約定,例如 BEM,以確保選擇器的範圍是足夠小的。其餘一些人則使用預處理器,例如 SASS,其選擇器嵌套能夠用做命名空間。一種較新的方法是經過 CSS 模塊 或各類 CSS-in-JS 庫,以編程的方式寫 CSS。某些開發者還會使用 shadow DOM 來隔離樣式。

只要你選擇一種能確保開發人員的樣式互不影響的方案便可。

2. 共享組件庫

上面咱們提到,視覺一致性很重要,一種解決方法是應用間共享可重用的 UI 組件庫。

提及來容易,作起來難。建立這樣一個庫的主要好處是減小工做量。此外,你的組件庫能夠充當樣式指南,做爲開發人員和設計師之間進行協做的重要橋樑。

第一個容易出錯的點,就是過早地建立了太多組件。好比你試圖建立一個囊括全部常見 UI 組件的組件庫。可是,經驗告訴咱們,在實際使用組件以前,咱們很難猜想組件的 API 應該是什麼樣的,強行作組件會致使早期的混亂。所以,咱們寧願讓團隊根據需求建立本身的組件,即便這最初會致使某些重複。

讓 API 天然出現,一旦組件的 API 變得顯而易見,就能夠將重複的代碼整合到共享庫中。

與任何共享內部庫同樣,庫的全部權和治理權很難分配。一種人認爲,全部開發成員都擁有庫的全部權,實際上這意味着沒有人擁有庫的全部權。若是沒有明確的約定或技術遠見,共享組件庫很快就會成爲不一致代碼的大雜燴。若是取另外一個極端,即徹底集中式的開發共享庫,後果就是建立組件的人與使用這些組件的人之間將存在很大的脫節。

咱們見過的最好的合做方式是,任何人均可覺得庫貢獻代碼,可是有一個 託管者(一我的或一個團隊)負責確保這些代碼的質量、一致性和有效性。

維護共享庫的人須要技術很強,同時溝通能力也要很強。

3. 跨微應用通訊

關於微前端的最多見問題之一是如何讓應用彼此通訊。咱們建議應該儘量少地通訊,由於這一般會引入沒必要要的耦合。

不過跨應用通訊的需求仍是存在的。

  1. 使用自定義事件通訊,是下降耦合的一種好方法。不過這樣作會使微應用之間的接口變得模糊。
  2. 能夠考慮 React 應用中常見的機制:自上而下傳遞迴調和數據。
  3. 第三種選擇是使用地址欄做爲通訊橋樑,咱們將在後面詳細探討 。

若是你使用的是 Redux,那麼一般你會爲整個應用建立一個全局狀態。但若是每一個微應用是獨立的,那麼每一個微應用就都應該有本身的 Redux 和全局狀態。

不管選擇哪一種方法,咱們都但願咱們的微應用經過消息或事件進行通訊,並避免任何共享狀態,以免耦合。

你還應該考慮如何自動驗證集成沒有中斷。功能測試是解法之一,可是因爲實現和維護成本,咱們傾向於只作一部分功能測試。或者,你能夠實施消費者驅動接口,讓每一個微應用指定它對其餘微應用的要求,這樣你就不用實際將它們所有集成在一塊兒並在瀏覽器中測試。

4. 後端通信

若是咱們有獨立的團隊獨立處理前端應用,那麼後端開發又是怎樣的呢?

咱們堅信全棧團隊的價值,從界面代碼一直到後臺 API 開發,再到數據庫和網站架構。

咱們推薦的模式是 Backends For Frontends 模式,其中每一個前端應用程序都有一個相應的後端,後端的目的僅僅是爲了知足該前端的需求。BFF模式起初的粒度多是每一個前端平臺(PC頁面、手機頁面等)對應一個後端應用,但最終會變爲每一個微應用對應一個後端應用。

這裏要說明一下,一個後端應用可能有獨立業務邏輯和數據庫的,也可能只是下游服務的聚合器。 若是微前端應用只有一個與之通訊的API,而且該API至關穩定,那麼爲它單獨構建一個後臺可能根本沒有太大價值。指導原則是:構建微前端應用的團隊沒必要等待其餘團隊爲其構建什麼事物。

所以,若是一個微前端用到的新功能須要後端接口的變動,那麼這一前一後兩個地方就應該交給一個團隊來開發。

另外一個常見的問題是,如何作身份驗證和鑑權?

顯然用戶只須要進行一次身份驗證,所以身份驗證應該放在容器應用裏。容器可能具備某種登陸形式,經過該登陸形式咱們能夠得到某種令牌。該令牌將歸容器全部,並能夠在初始化時注入到每一個微前端中。最後,微前端能夠將令牌發送到服務器,而後服務器進行驗證。

5. 共享內容

雖然咱們但願每一個團隊和微應用盡量獨立,可是有些事情仍是會共享的。

上面提過共享組件庫,可是對於這個小型應用而言,組件庫會顯得過大。所以,咱們有一個小 的公共內容庫,其中包括圖像、JSON數據和CSS,這些內容被全部其餘微應用共享。

還有一個重要的東西須要共享:依賴庫。重複的依賴項是微前端的一個常見缺點。即便在應用程序之間共享這些依賴也很是困難,咱們來討論如何實現依賴庫的共享。

第一步是選擇要共享的依賴項。對咱們已編譯代碼的分析代表,大約50%的代碼是由 react 和 react-dom 貢獻。這兩個庫是咱們最核心的依賴項,所以若是把這兩個庫單獨提取出來做爲共享庫,會很是有效。最後,它們是很是穩定和成熟的庫,升級也很慎重,因此升級工做應該不會太困難。

至於如何提取,咱們須要作的就是在 webpack 配置中將庫標記爲外部庫(externals):

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

而後,用 script 向每一個index.html 文件添加幾個標籤,以從共享內容服務器中獲取這兩個庫:

<body>
  <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>

做者:方應杭
連接:https://juejin.im/post/5d8adb... 來源:掘金

相關文章
相關標籤/搜索