Webpack 5 Module Federation: A game-changer in JavaScript architecturejavascript
從沒有哪種在獨立的應用程序之間共享代碼的可伸縮解決方案可以如此便捷,並且在成規模時幾乎是不可能的作到的。咱們所擁有的最接近的東西是 externals 或 DLLPlugin,不過這形成了對外部文件的集中式依賴。共享代碼很麻煩,各個應用程序並非真正獨立的,而且一般只能共享有限數量的依賴項。此外,在單獨捆綁的應用程序之間共享實際的功能代碼或組件是不可行的、無效的而且是無益的。
油管視頻:https://youtu.be/D3XYAx30CNchtml
咱們須要一個可擴展的解決方案來共享 node 模塊和功能與應用程序代碼。它須要在運行時發生,以便具備適應性和動態性。Externals 並不能有效或靈活地完成工做;Import maps 沒法解決規模問題。我並非要單獨下載代碼並共享依賴項,而是須要一個業務編配層,該層可以在運行時動態地共享模塊,並有後備功能。前端
Module Federation 是我發明並原型化的一種 JavaScript 體系結構。而後,在個人聯合創始人和 Webpack 創始人的幫助下— —它變成了 Webpack 5 核心中最使人興奮的功能之一(裏面有一些很棒的東西,新的 API 確實功能強大且簡潔)。java
我很自豪地向你介紹,JavaScript 應用架構中期待已久的飛躍。咱們對開源社區的貢獻:Module Federation
模塊聯合(Module Federation) 容許 JavaScript 應用動態地從另外一個應用中加載代碼,而後在過程當中共享依賴項。若是使用模塊聯合的應用程序不具備聯合代碼所需的依賴項,則 Webpack 將從該聯合的生成源中下載缺乏的依賴項。node
能夠共享代碼,可是每種狀況都存在後備方案。聯合代碼始終能夠加載其依賴關係,但在下載更多有效負載以前將嘗試使用使用者的依賴關係。這意味着像單片 Webpack 構建同樣,更少的代碼重複和依賴關係共享。雖然我發明了這個系統,但它是 Marais Rossouw(https://medium.com/u/e7046f61bad8) 和我(Zack Jackson (https://medium.com/u/9ef1379caffc))共同編寫的,並獲得了 Tobias Koppers(https://medium.com/u/cccc522e775a 的大量指導和幫助。這些工程師在重寫和穩定 Webpack 5 核心中的模塊聯合部分發揮了關鍵做用。感謝他們一直以來的合做與支持。react
Module federation(模塊聯合):與 Apollo GraphQL 聯合有着相同的思想——但適用於 JavaScript 模塊,可用在瀏覽器和 node.js 中——通用模塊聯合webpack
host(主機):一種 Webpack 構建,該構建在頁面加載期間首先初始化(觸發 onLoad 事件時) git
remote(遠程主機):另外一個 Webpack 構建,其中一部分被 「host」 所用github
Bidirectional-hosts(雙向主機):當 bundle 或 Webpack 構建時能夠做爲主機或做爲遠程主機使用。可在運行時使用其餘應用程序或着被其餘人使用web
請注意,該系統的設計宗旨是使每一個徹底獨立的構建或應用均可以位於本身的存儲庫中,能夠獨立部署,並可以做爲本身的獨立 SPA 運行。
這些應用都是*雙向主機(bi-directional hosts)。首先加載的任何應用都將會成爲主機*。當你修改路由並在應用程序中移動時,它將會以和動態導入相同的方式加載聯合模塊。可是若是你要刷新頁面,則首先在該負載上啓動的任何應用程序都將會成爲主機。
假設網站的每一個頁面都是獨立部署和編譯的。我須要這種 micro-frontend 樣式的體系結構,可是咱們不但願在修改路由時從新加載頁面。我還但願在它們之間動態共享代碼和服務以使其高效,就好像它是一個大型的 Webpack 構建並進行了代碼拆分同樣。
登錄主頁應用程序將使 「主頁」 頁面成爲「主機」。若是瀏覽到 「about」 頁面,則主機(主頁 spa)其實是從另外一個獨立的應用程序( about 頁面 spa)動態導入模塊,它不會加載主入口點和整個應用程序:僅僅幾千字節的代碼。若是我在 「about」 頁面上並刷新瀏覽器,「about」 頁面會成爲「主機」,而再次瀏覽回到主頁將是 「about」 頁面 「主機」 的一種狀況,即從 「遠程」 頁面(即主頁)中獲取運行時的一部分。
全部應用程序都是遠程和主機,被調用者以及系統中任何其餘聯合模塊的使用者。
你能夠在 GitHub 上閱讀更多有關技術方面的信息:
https://github.com/webpack/webpack/issues/10352
讓咱們從三個獨立的應用程序開始。
配置:
我將使用 App 1 中的應用容器 App。其餘應用程序將會使用它。爲此我將其 App 公開爲 AppContainer。
App 1 還將使用來自另外兩個聯合應用的組件。爲此,我指定了remotes 配置項:
1const HtmlWebpackPlugin = require("html-webpack-plugin"); 2const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); 3 4module.exports = { 5 // other webpack configs... 6 plugins: [ 7 new ModuleFederationPlugin({ 8 name: "app_one_remote", 9 remotes: { 10 app_two: "app_two_remote", 11 app_three: "app_three_remote" 12 }, 13 exposes: { 14 'AppContainer':'./src/App' 15 }, 16 shared: ["react", "react-dom","react-router-dom"] 17 }), 18 new HtmlWebpackPlugin({ 19 template: "./public/index.html", 20 chunks: ["main"] 21 }) 22 ] 23}
設置構建流程:
在我應用程序的開頭加載了 app_one_remote.js。這樣能夠把你鏈接到其餘 Webpack 運行時,並在運行時預配業務編配層。這是專門設計的 Webpack 運行時和入口點。它不是普通的應用程序入口點,只有幾個 KB 。
1<head> 2 <script src="http://localhost:3002/app_one_remote.js"></script> 3 <script src="http://localhost:3003/app_two_remote.js"></script> 4</head> 5<body> 6 <div id="root"></div> 7</body>
從遠程主機使用代碼
App1 的頁面使用了來自App 2 的對話框組件。
1const Dialog = React.lazy(() => import("app_two_remote/Dialog")); 2 3const Page1 = () => { 4 return ( 5 <div> 6 <h1>Page 1</h1> 7 <React.Suspense fallback="Loading Material UI Dialog..."> 8 <Dialog /> 9 </React.Suspense> 10 </div> 11 ); 12} 13 14export default Page1; 15
路由看起來很標準:
1import { Route, Switch } from "react-router-dom"; 2 3import Page1 from "./pages/page1"; 4import Page2 from "./pages/page2"; 5import React from "react"; 6 7const Routes = () => ( 8 <Switch> 9 <Route path="/page1"> 10 <Page1 /> 11 </Route> 12 <Route path="/page2"> 13 <Page2 /> 14 </Route> 15 </Switch> 16); 17 18export default Routes;
配置:
App 2 將公開對話框,使 App 1 可以使用它。App 2 也會使用 App 1 的 App,所以咱們指定 app_one 爲遠端-展現雙向主機:
1const HtmlWebpackPlugin = require("html-webpack-plugin"); 2const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); 3module.exports = { 4 plugins: [ 5 new ModuleFederationPlugin({ 6 name: "app_two_remote", 7 filename: "remoteEntry.js", 8 exposes: { 9 Dialog: "./src/Dialog" 10 }, 11 remotes: { 12 app_one: "app_one_remote", 13 }, 14 shared: ["react", "react-dom","react-router-dom"] 15 }), 16 new HtmlWebpackPlugin({ 17 template: "./public/index.html", 18 chunks: ["main"] 19 }) 20 ] 21};
使用:
根 App 以下所示:
1import React from "react"; 2import Routes from './Routes' 3const AppContainer = React.lazy(() => import("app_one_remote/AppContainer")); 4 5const App = () => { 6 return ( 7 <div> 8 <React.Suspense fallback="Loading App Container from Host"> 9 <AppContainer routes={Routes}/> 10 </React.Suspense> 11 </div> 12 ); 13} 14 15export default App;
使用 Dialog 的默認頁面以下所示:
1import React from 'react' 2import {ThemeProvider} from "@material-ui/core"; 3import {theme} from "./theme"; 4import Dialog from "./Dialog"; 5 6 7function MainPage() { 8 return ( 9 <ThemeProvider theme={theme}> 10 <div> 11 <h1>Material UI App</h1> 12 <Dialog /> 13 </div> 14 </ThemeProvider> 15 ); 16} 17 18export default MainPage
不出所料,App 3 看上去相似。可是它不會使用 App 1 中的App,它能夠做爲獨立的自運行組件(沒有導航或側邊欄)工做。因此它不指定任何 remote:
1new ModuleFederationPlugin({ 2 name: "app_three_remote", 3 library: { type: "var", name: "app_three_remote" }, 4 filename: "remoteEntry.js", 5 exposes: { 6 Button: "./src/Button" 7 }, 8 shared: ["react", "react-dom"] 9}),
請密切注意瀏覽器中 network 標籤。該代碼將在三個不一樣的服務器之間進行聯合:三個不一樣的 bundle。一般狀況下,除非你用了 *** 或漸進式加載,不然不要聯合整個應用程序容器。可是這個概念很是強大。
查看推文中的視頻:
https://twitter.com/ScriptedAlchemy/status/1234383702433468416
幾乎沒有依賴項重複。經過 shared 選項 —— 遠程將依賴於主機依賴關係,若是主機沒有依賴關係,則 remote 將下載本身的依賴關係。沒有代碼重複,可是內置冗餘。
手動將供應商或其餘模塊添加到 shared 並不理想。能夠用自定義編寫的函數或補充性的 Webpack 插件輕鬆地將其自動化。咱們確實打算髮布 AutomaticModuleFederationPlugin 並從 Webpack 核心外部對其進行維護。既然咱們已經在 Webpack 中內置了一流的代碼聯合支持,那麼擴展其功能就變得微不足道了。
如今有一個大問題 —— *** 能夠勝任這項工做嗎?
咱們將其設計爲通用的。模塊聯合可在任何環境中使用。在服務器端渲染聯合代碼是徹底可能的。只需讓服務器構建使用 commonjs 庫目標便可。有多種實現聯合 *** 的方法:S3流、ESI、自動執行 npm 發佈以使用服務器變體。我計劃用公共共享文件卷或異步 S3 流在整個文件系統中流式傳輸文件,使服務器可以像在瀏覽器中同樣請求聯合代碼,並用 fs 而不是 http 來加載聯合代碼。
1module.exports = { 2 plugins: [ 3 new ModuleFederationPlugin({ 4 name: "container", 5 library: { type: "commonjs-module" }, 6 filename: "container.js", 7 remotes: { 8 containerB: "../1-container-full/container.js" 9 }, 10 shared: ["react"] 11 }) 12 ] 13};
「模塊聯合也能夠與 target:"node" 一塊兒使用。做爲代替指向其餘微前端的 URL,在這裏用指向其餘微前端的文件路徑。這樣你可使用相同的代碼庫和不一樣的 webpack 配置進行 ***,以構建 node.js。對於 node.js 中的 Module Federation,相同的屬性仍然適用:e.g. 單獨構建,單獨部署」 —— Tobias Koppers
聯合須要 Webpack 5 —— Next 還沒有正式支持。可是,我確實設法 fork 並升級了 Next.js 以使其與 Webpack 5 兼容!這項工做仍在進行中。一些開發模式的中間件須要完成。生產模式目前能夠工做,一些其餘加載器仍須要從新測試。
在Twitter上查看:
https://twitter.com/ScriptedAlchemy/status/1234240375818076160/photo/1
我但願有機會分享更多有關這項技術的信息。若是你想使用 Module Federation 或 Federated 體系結構,咱們很想聽聽你對當前體系結構的經驗和改進。咱們也但願有機會在播客、聚會或公司中談論它。經過 Twitter 與我聯繫: https://twitter.com/ScriptedAlchemy
你也能夠成爲個人共同創做者。請關注咱們,並獲取有關模塊聯合、FOSA(獨立應用程序聯盟)體系結構以及咱們正在建立的其餘工具的最新更新,這些工具被用於聯合應用程序
社區對此反應熱烈!個人共同創做者以及我本身的時間都花費在編寫到 Webpack 5 中。咱們但願最終完成其他功能並編寫一些文檔的同時,一些代碼示例會對你有所幫助:
https://twitter.com/codervandal
Webpack 5 and Module Federation - A Microfrontend Revolution
https://dev.to/marais/webpack-5-and-module-federation-4j1i
因爲有足夠的帶寬,咱們將會建立 *** 示例和更全面的演示。若是有人想構建可用做演示的東西,咱們將很樂意接受將請求並 pull 到 webpack-external-import 中。
module-federation/module-federation-examplesExamples
(https://github.com/module-federation/module-federation-examples)
module-federation/next-webpack-5
(https://github.com/module-federation/next-webpack-5
ScriptedAlchemy/mfe-webpack-demo
(https://github.com/ScriptedAlchemy/mfe-webpack-demo
https://indepth.dev/webpack-5-module-federation-a-game-changer-in-javascript-architecture/