[譯] 微前端詳解

最近接觸了一些DevOps、微服務等概念,想要找一篇文章大體瞭解一下,看到個微前端標題比較新穎因而選擇翻譯這篇文章。原文https://martinfowler.com/articles/micro-frontends.htmljavascript


近年來,微服務已經爆紅,許多組織使用這種架構來避免大型、單一後端的限制。雖然已經有不少關於這種構建服務器端軟件的風格的文章,可是許多公司仍然在與統一的前端代碼庫做鬥爭。html

也許您想要構建一個漸進的或響應性的web應用程序,可是找不到一個容易的地方將這些特性集成到現有代碼中。也許您想開始使用新的JavaScript語言特性(或者能夠編譯成JavaScript的衆多語言之一),可是沒法將必要的構建工具放入現有的構建過程當中。或者,您可能只是想擴展您的開發,以便多個團隊能夠同時處理單個產品,可是現有總體中的耦合和複雜性意味着每一個人都在踩對方的腳。這些都是真實存在的問題,它們都會對你有效地爲客戶提供高質量體驗的能力產生負面影響。 最近,咱們看到愈來愈多的人關注複雜的現代web開發所必需的整體架構和組織架構。特別是,咱們看到了將前端總體分解成更小、更簡單的塊模式,這些塊能夠獨立開發、測試和部署,同時仍然做爲一個單一的內聚產品出如今客戶面前。咱們稱這種技術爲微前端。前端

An architectural style where independently deliverable frontend applications are composed into a greater wholejava

在2016年11月出版的ThoughtWorks technology radar上,咱們將微前沿技術列爲組織應該評估的一項技術。咱們後來將其推廣到試用版,並最終推廣到Adopt版,這意味着咱們將其視爲一種驗證過的方法,您應該在有意義時使用它。nginx

例子

想象一個客戶點餐網站,表面上這是一個web

  • 須要一個登錄界面,用戶能夠瀏覽和搜索餐廳。而且能夠篩選和查詢對應餐廳的價格、菜品以及哪些是以前有人點過的。
  • 每個餐廳須要有本身的頁面,能展現菜單,容許用戶選擇他們想要的菜以及價格、數量和特殊需求。
  • 客戶我的頁面展現我的歷史點菜記錄、訂單詳情和支付選項

每一個頁面都有足夠的複雜性,咱們能夠很容易地爲每一個頁面指定一個專門的團隊,而且每一個團隊都應該可以獨立於全部其餘團隊在本身的頁面上工做。他們應該可以開發、測試、部署和維護他們的代碼,而不用擔憂與其餘團隊的衝突或協調。然而,咱們的客戶仍然應該看到一個單一的、無縫的網站。json

集成方法

鑑於上述至關鬆散的定義,有許多方法能夠合理地稱爲微前端。在本節中,咱們將展現一些例子並討論它們之間的權衡。有一個至關天然的架構出如今全部的方法:一般有一個微前端的每一個頁面應用程序,有一個單一的容器應用程序,其中:後端

  • 渲染通用的頁面元素例如header和footer
  • 解決諸如身份驗證和導航之類的橫切關注點
  • 將各類微前端整合到頁面上,並告訴每一個微前端什麼時候何地呈現本身

服務器端模版組合

咱們從一個明顯不新穎的前端開發方法開始,在服務器上使用多個模版或片斷呈現HTML。咱們有一個index.html,它爆紅任何常見的頁面元素,而後使用服務器端包含從HTML文件中插入特定到頁面內容:瀏覽器

<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>
複製代碼

咱們使用Nginx提供這個文件,配置$PAGE變量經過匹配被請求的URL:緩存

server {
    listen 8080;
    server_name localhost;

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

    # Redirect / to /browse
    rewrite ^/$ http://localhost:8080/browse redirect;

    # Decide which HTML fragment to insert based on the URL
    location /browse {
      set $PAGE 'browse';
    }
    location /order {
      set $PAGE 'order';
    }
    location /profile {
      set $PAGE 'profile'
    }

    # All locations should render through index.html
    error_page 404 /index.html;
}
複製代碼

這是至關標準的服務器端組合。咱們能夠合理地將此微前端稱爲微前端的緣由是,咱們將代碼分割成這樣的一種方式,即每一個部分都表示一個獨立團隊能夠支付的自包含領域概念。這裏沒有顯示的是這些不一樣的HTML文件是如何在web服務器上結束的,可是假設它們都有本身的部署管道,這容許咱們將更改部署到一個頁面,而不影響或考慮任何其餘頁面。 爲了得到更大的獨立性,能夠有一個單獨的服務器負責呈現和服務於每一個微前端,其中一個服務器位於前端,向其餘服務器發出請求。經過仔細緩存響應,這個能夠在不影響延遲的狀況下完成。

這個例子說明了微前端不必定是一種新技術,也不必定是複雜的。只要咱們注意咱們的設計決策如何影響咱們的代碼庫。

構建時集成

咱們有時看到的一種方法是將每一個微前端發佈爲一個包,並讓容器應用程序將它們都包含爲庫依賴項。這是咱們應用中容器的package.json可能的樣子:

{
    "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"
    }
}
複製代碼

起初,這彷佛是有道理的。它像往常同樣生成一個可部署的JavaScript包,容許咱們從各類應用程序中刪除重複的公共依賴項。然而,這種方法意味着咱們必需從新編譯併發布每一個單獨的微前端,以便發佈對產品任何單獨部分的更改。正如微服務同樣,咱們已經看到了這種同步發佈過程所帶來的痛苦,所以咱們強烈建議不要使用這種方法來處理微前端。 在經歷了將應用程序劃分爲能夠獨立開發和測試的離散代碼庫的全部麻煩以後,讓咱們不要在發佈階段從新引入全部耦合。咱們應該找到一種在運行時集成微前端的方法,而不是在構建時。

運行時經過iframe集成

在瀏覽器中組合應用程序最簡單的方法之一是用iframe。從本質上講,iframe使用獨立的子頁面構建頁面變得很容易。它們還在樣式和全局變量互不干擾方面提供了良好的隔離程度。

<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.come/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>
複製代碼

就像服務端集成選項同樣,使用iframes構建頁面並非新技術,並且看起來也不那麼使人興奮。可是,若是咱們回顧一下前面列出的微前端主要優勢,iframe基本上符合要求,只要咱們仔細考慮如何分割應用程序和構建團隊。 咱們常常看到不少人不肯意選擇iframe。雖然有些不情願彷佛是由一種iframe有點「使人討厭」的直覺驅動的,但仍是有一些很好的理由讓人們避免使用它們。上面提到的簡單隔離確實使它們比其餘選項更不靈活。在應用程序的不一樣部分之間構建集成可能比較困難,所以它們會使用路由、歷史記錄和深度連接變得更加複雜,而且在使頁面徹底響應方面還會帶來一些額外的挑戰。

經過Javascript運行時集成

接下來咱們將要描述一個最靈活而且使用最多的方法。每一個微前端都使用標記script包含在頁面中,而且在加載時公開一個全局函數做爲其入口點。而後容器應用程序肯定應該掛載哪一個微前端,並調用相關函數來告訴微前端什麼時候何地呈現本身。

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

    <!-- These scripts don't render anything immediately --> <!-- Instead they attach entry-point functions to `window` --> <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"> // These global functions are attached to window by the above scripts const microFrontendsByRoute = { '/': window.renderBrowseRestaurants, '/order-food': window.renderOrderFood, '/user-profile': window.renderUserProfile, }; const renderFunction = microFrontendsByRoute[window.location.pathname]; // Having determined the entry-point function, we now call it, // giving it the ID of the element where it should render itself renderFunction('micro-frontend-root'); </script> </body> </html> 複製代碼

上面的例子顯然是一個原始的例子,可是它演示了基本的技術。與構建時集成不一樣,咱們能夠獨立部署每一個bundle.js文件。與iframe不一樣的是,咱們擁有充分的靈活性,能夠在咱們喜歡的任何微前端之間構建集成。咱們能夠經過多種方式擴展上面的代碼,例如只根據須要下載每一個JavaScript包,或者在呈現微前端時傳遞數據。

這種方法的靈活性,加上獨立的可部署性,使其成爲咱們的默認選擇,也是咱們在野外最多見的選擇。當咱們看到完整的示例時,咱們將更詳細地研究它。

經過WebComponents運行時集成

前一種方法的一個變體是,每一個微前端都定義一個HTML自定義元素來實例化容器,而不是定義一個全局函數來調用容器。

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

    <!-- These scripts don't render anything immediately --> <!-- Instead they each define a custom element type --> <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"> // These element types are defined by the above scripts const webComponentsByRoute = { '/': 'micro-frontend-browse-restaurants', '/order-food': 'micro-frontend-order-food', '/user-profile': 'micro-frontend-user-profile', }; const webComponentType = webComponentsByRoute[window.location.pathname]; // Having determined the right web component custom element type, // we now create an instance of it and attach it to the document const root = document.getElementById('micro-frontend-root'); const webComponent = document.createElement(webComponentType); root.appendChild(webComponent); </script> </body> </html> 複製代碼

這裏的最終結果與前面的示例很是類似,主要的區別在於您選擇以「web組件方式」進行操做。若是您喜歡web組件規範,而且喜歡使用瀏覽器提供的功能,那麼這是一個不錯的選擇。若是您喜歡在容器應用程序和微前端之間定義本身的接口,那麼您可能更喜歡前面的示例。

樣式

共享組件庫

跨應用通訊

後端通訊

測試

相關文章
相關標籤/搜索