最近在學習一些微前端的知識,轉載一片我的以爲比較好的。javascript
微前端能夠理解爲,一個很大的前端應用,將此應用總體分解,分解成多個項目,每一個項目交給一個團隊,各團隊能夠獨立開發、測試、部署和維護,其實用什麼方法都沒有明確規定,最終的目的是要達到代碼隔離和團隊自治便可。html
咱們用一個很是傳統的方式開始,將多個模板渲染到服務器上的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
有人會用到的一種方法是將每一個微前端發佈爲一個 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
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
這種方式多是最靈活的一種,也是被採用頻率最高的一種方法。每一個微前端都對應一個 <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 控制何時下載每一個應用,以及渲染應用時額外傳參數。
這種方法的靈活性和獨立性使其成爲最經常使用的方案。當咱們展現完整的示例時,會有更詳細的探討。
這是前一種方法的變體,每一個微應用對應一個 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 規範,那麼這是一個不錯的選擇。若是你但願在容器應用程序和微應用之間定義本身的接口,那麼你可能更喜歡前面的示例。
CSS 沒有模塊系統、命名空間和封裝。就算有,也一般缺少瀏覽器支持。在微前端環境中,這些問題會更嚴重。
例如,若是一個團隊的微前端的樣式表爲 h2 { color: black; },而另外一個團隊的則爲 h2 { color: blue; },而這兩個選擇器都附加在同一頁面上,就會衝突!
這不是一個新問題,但因爲這些選擇器是由不一樣的團隊在不一樣的時間編寫的,而且代碼可能分散在不一樣的庫中,所以更難避免。
多年來,有許多方法可讓 CSS 變得更易於管理。有些人選擇使用嚴格的命名約定,例如 BEM,以確保選擇器的範圍是足夠小的。其餘一些人則使用預處理器,例如 SASS,其選擇器嵌套能夠用做命名空間。一種較新的方法是經過 CSS 模塊 或各類 CSS-in-JS 庫,以編程的方式寫 CSS。某些開發者還會使用 shadow DOM 來隔離樣式。
只要你選擇一種能確保開發人員的樣式互不影響的方案便可。
上面咱們提到,視覺一致性很重要,一種解決方法是應用間共享可重用的 UI 組件庫。
提及來容易,作起來難。建立這樣一個庫的主要好處是減小工做量。此外,你的組件庫能夠充當樣式指南,做爲開發人員和設計師之間進行協做的重要橋樑。
第一個容易出錯的點,就是過早地建立了太多組件。好比你試圖建立一個囊括全部常見 UI 組件的組件庫。可是,經驗告訴咱們,在實際使用組件以前,咱們很難猜想組件的 API 應該是什麼樣的,強行作組件會致使早期的混亂。所以,咱們寧願讓團隊根據需求建立本身的組件,即便這最初會致使某些重複。
讓 API 天然出現,一旦組件的 API 變得顯而易見,就能夠將重複的代碼整合到共享庫中。
與任何共享內部庫同樣,庫的全部權和治理權很難分配。一種人認爲,全部開發成員都擁有庫的全部權,實際上這意味着沒有人擁有庫的全部權。若是沒有明確的約定或技術遠見,共享組件庫很快就會成爲不一致代碼的大雜燴。若是取另外一個極端,即徹底集中式的開發共享庫,後果就是建立組件的人與使用這些組件的人之間將存在很大的脫節。
咱們見過的最好的合做方式是,任何人均可覺得庫貢獻代碼,可是有一個 託管者(一我的或一個團隊)負責確保這些代碼的質量、一致性和有效性。
維護共享庫的人須要技術很強,同時溝通能力也要很強。
關於微前端的最多見問題之一是如何讓應用彼此通訊。咱們建議應該儘量少地通訊,由於這一般會引入沒必要要的耦合。
不過跨應用通訊的需求仍是存在的。
若是你使用的是 Redux,那麼一般你會爲整個應用建立一個全局狀態。但若是每一個微應用是獨立的,那麼每一個微應用就都應該有本身的 Redux 和全局狀態。
不管選擇哪一種方法,咱們都但願咱們的微應用經過消息或事件進行通訊,並避免任何共享狀態,以免耦合。
你還應該考慮如何自動驗證集成沒有中斷。功能測試是解法之一,可是因爲實現和維護成本,咱們傾向於只作一部分功能測試。或者,你能夠實施消費者驅動接口,讓每一個微應用指定它對其餘微應用的要求,這樣你就不用實際將它們所有集成在一塊兒並在瀏覽器中測試。
若是咱們有獨立的團隊獨立處理前端應用,那麼後端開發又是怎樣的呢?
咱們堅信全棧團隊的價值,從界面代碼一直到後臺 API 開發,再到數據庫和網站架構。
咱們推薦的模式是 Backends For Frontends 模式,其中每一個前端應用程序都有一個相應的後端,後端的目的僅僅是爲了知足該前端的需求。BFF模式起初的粒度多是每一個前端平臺(PC頁面、手機頁面等)對應一個後端應用,但最終會變爲每一個微應用對應一個後端應用。
這裏要說明一下,一個後端應用可能有獨立業務邏輯和數據庫的,也可能只是下游服務的聚合器。 若是微前端應用只有一個與之通訊的API,而且該API至關穩定,那麼爲它單獨構建一個後臺可能根本沒有太大價值。指導原則是:構建微前端應用的團隊沒必要等待其餘團隊爲其構建什麼事物。
所以,若是一個微前端用到的新功能須要後端接口的變動,那麼這一前一後兩個地方就應該交給一個團隊來開發。
另外一個常見的問題是,如何作身份驗證和鑑權?
顯然用戶只須要進行一次身份驗證,所以身份驗證應該放在容器應用裏。容器可能具備某種登陸形式,經過該登陸形式咱們能夠得到某種令牌。該令牌將歸容器全部,並能夠在初始化時注入到每一個微前端中。最後,微前端能夠將令牌發送到服務器,而後服務器進行驗證。
雖然咱們但願每一個團隊和微應用盡量獨立,可是有些事情仍是會共享的。
上面提過共享組件庫,可是對於這個小型應用而言,組件庫會顯得過大。所以,咱們有一個小 的公共內容庫,其中包括圖像、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... 來源:掘金