Webpack 5 Module Federation: A game-changer in JavaScript architecture
模塊聯合(Module federation)允許 JavaScript 應用在客戶端和服務器上動態運行來自另一個包或版本的代碼。
這是 JavaScript 捆綁器,等效於在 Apollo 中使用 GraphQL。
從沒有哪一種在獨立的應用程序之間共享代碼的可伸縮解決方案能夠如此便捷,而且在成規模時幾乎是不可能的做到的。我們所擁有的最接近的東西是 externals 或 DLLPlugin,不過這造成了對外部文件的集中式依賴。共享代碼很麻煩,各個應用程序並不是真正獨立的,並且通常只能共享有限數量的依賴項。此外,在單獨捆綁的應用程序之間共享實際的功能代碼或組件是不可行的、無效的並且是無益的。
對於那些想要更通俗版本的人,Jack Herrington 錄了一個視頻!
油管視頻:https://youtu.be/D3XYAx30CNc
我們需要一個可擴展的解決方案來共享 node 模塊和功能與應用程序代碼。它需要在運行時發生,以便具有適應性和動態性。Externals 並不能有效或靈活地完成工作;Import maps 無法解決規模問題。我並不是要單獨下載代碼並共享依賴項,而是需要一個業務編配層,該層能夠在運行時動態地共享模塊,並有後備功能。
什麼是模塊聯合(Module Federation)?
Module Federation 是我發明並原型化的一種 JavaScript 體系結構。然後,在我的聯合創始人和 Webpack 創始人的幫助下— —它變成了 Webpack 5 核心中最令人興奮的功能之一(裏面有一些很棒的東西,新的 API 確實功能強大且簡潔)。
我很自豪地向你介紹,JavaScript 應用架構中期待已久的飛躍。我們對開源社區的貢獻:Module Federation
模塊聯合(Module Federation) 允許 JavaScript 應用動態地從另一個應用中加載代碼,然後在過程中共享依賴項。如果使用模塊聯合的應用程序不具有聯合代碼所需的依賴項,則 Webpack 將從該聯合的生成源中下載缺少的依賴項。
可以共享代碼,但是每種情況都存在後備方案。聯合代碼始終可以加載其依賴關係,但在下載更多有效負載之前將嘗試使用使用者的依賴關係。這意味着像單片 Webpack 構建一樣,更少的代碼重複和依賴關係共享。雖然我發明了這個系統,但它是 Marais Rossouw(https://medium.com/u/e7046f61bad8) 和我(Zack Jackson (https://medium.com/u/9ef1379caffc))共同編寫的,並得到了 Tobias Koppers(https://medium.com/u/cccc522e775a 的大量指導和幫助。這些工程師在重寫和穩定 Webpack 5 核心中的模塊聯合部分發揮了關鍵作用。感謝他們一直以來的合作與支持。
術語
-
Module federation(模塊聯合):與 Apollo GraphQL 聯合有着相同的思想——但適用於 JavaScript 模塊,可用在瀏覽器和 node.js 中——通用模塊聯合
-
host(主機):一種 Webpack 構建,該構建在頁面加載期間首先初始化(觸發 onLoad 事件時)
-
remote(遠程主機):另一個 Webpack 構建,其中一部分被 “host” 所用
-
Bidirectional-hosts(雙向主機):當 bundle 或 Webpack 構建時可以作爲主機或作爲遠程主機使用。可在運行時使用其他應用程序或着被其他人使用
請注意,該系統的設計宗旨是使每個完全獨立的構建或應用都可以位於自己的存儲庫中,可以獨立部署,並能夠作爲自己的獨立 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 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 。
要注意這些是特殊的入口點 —— 它們只有幾KB的大小。包含可以與主機交互的特殊 Webpack 運行時,它不是標準入口點
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 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 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.js
聯合需要 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 - ScriptedAlchemy/webpack-external-import
(https://github.com/ScriptedAlchemy/webpack-external-import
原文鏈接
https://indepth.dev/webpack-5-module-federation-a-game-changer-in-javascript-architecture/