它改變了 JavaScript 的體系結構——Webpack 5 Module Federation

原文:Zack Jackson

翻譯:瘋狂的技術宅javascript

https://indepth.dev/webpack-5...html

未經容許嚴禁轉前端

模塊聯合(Module federation)容許 JavaScript 應用在客戶端和服務器上動態運行來自另外一個包或版本的代碼。

這是 JavaScript 捆綁器,等效於在 Apollo 中使用 GraphQL。

從沒有哪種在獨立的應用程序之間共享代碼的可伸縮解決方案可以如此便捷,並且在成規模時幾乎是不可能的作到的。咱們所擁有的最接近的東西是 externals 或 DLLPlugin,不過這形成了對外部文件的集中式依賴。共享代碼很麻煩,各個應用程序並非真正獨立的,而且一般只能共享有限數量的依賴項。此外,在單獨捆綁的應用程序之間共享實際的功能代碼或組件是不可行的、無效的而且是無益的。

對於那些想要更通俗版本的人,Jack Herrington 錄了一個視頻!java

油管視頻:https://youtu.be/D3XYAx30CNcnode


咱們須要一個可擴展的解決方案來共享 node 模塊和功能與應用程序代碼。它須要在運行時發生,以便具備適應性和動態性。 Externals 並不能有效或靈活地完成工做;Import maps 沒法解決規模問題。我並非要單獨下載代碼並共享依賴項,而是須要一個業務編配層,該層可以在運行時動態地共享模塊,並有後備功能。react

image.png

什麼是模塊聯合(Module Federation)?

Module Federation 是我發明並原型化的一種 JavaScript 體系結構。而後,在個人聯合創始人和 Webpack 創始人的幫助下— —它變成了 Webpack 5 核心中最使人興奮的功能之一(裏面有一些很棒的東西,新的 API 確實功能強大且簡潔)。webpack

我很自豪地向你介紹,JavaScript 應用架構中期待已久的飛躍。咱們對開源社區的貢獻:Module Federation

模塊聯合(Module Federation) 容許 JavaScript 應用動態地從另外一個應用中加載代碼,而後在過程當中共享依賴項。若是使用模塊聯合的應用程序不具備聯合代碼所需的依賴項,則 Webpack 將從該聯合的生成源中下載缺乏的依賴項。git

能夠共享代碼,可是每種狀況都存在後備方案。聯合代碼始終能夠加載其依賴關係,但在下載更多有效負載以前將嘗試使用使用者的依賴關係。這意味着像單片 Webpack 構建同樣,更少的代碼重複和依賴關係共享。雖然我發明了這個系統,但它是 Marais Rossouw 和我(Zack Jackson)共同編寫的,並獲得了 Tobias Koppers 的大量指導和幫助。這些工程師在重寫和穩定 Webpack 5 核心中的模塊聯合部分發揮了關鍵做用。感謝他們一直以來的合做與支持。程序員

術語

  • Module federation(模塊聯合):與 Apollo GraphQL 聯合有着相同的思想——但適用於 JavaScript 模塊,可用在瀏覽器和 node.js 中——通用模塊聯合
  • host(主機):一種 Webpack 構建,該構建在頁面加載期間首先初始化(觸發 onLoad 事件時)
  • remote(遠程主機):另外一個 Webpack 構建,其中一部分被 「host」 所用
  • Bidirectional-hosts(雙向主機):當 bundle 或 Webpack 構建時能夠做爲主機或做爲遠程主機使用。可在運行時使用其餘應用程序或着被其餘人使用

image.png

請注意,該系統的設計宗旨是使每一個徹底獨立的構建或應用均可以位於本身的存儲庫中,能夠獨立部署,並可以做爲本身的獨立 SPA 運行。

這些應用都是雙向主機(bi-directional hosts)。 首先加載的任何應用都將會成爲主機**。當你修改路由並在應用程序中移動時,它將會以和動態導入相同的方式加載聯合模塊。可是若是你要刷新頁面,則首先在該負載上啓動的任何應用程序都將會成爲主機。github

image.png

假設網站的每一個頁面都是獨立部署和編譯的。我須要這種 micro-frontend 樣式的體系結構,可是咱們不但願在修改路由時從新加載頁面。我還但願在它們之間動態共享代碼和服務以使其高效,就好像它是一個大型的 Webpack 構建並進行了代碼拆分同樣。

登錄主頁應用程序將使 「主頁」 頁面成爲「主機」。若是瀏覽到 「about」 頁面,則主機(主頁 spa)其實是從另外一個獨立的應用程序( about 頁面 spa)動態導入模塊,它不會加載主入口點和整個應用程序:僅僅幾千字節的代碼。若是我在 「about」 頁面上並刷新瀏覽器,「about」 頁面會成爲「主機」,而再次瀏覽回到主頁將是 「about」 頁面 「主機」 的一種狀況,即從 「遠程」 頁面(即主頁)中獲取運行時的一部分。

全部應用程序都是遠程和主機,被調用者以及系統中任何其餘聯合模塊的使用者。

你能夠在 GitHub 上閱讀更多有關技術方面的信息:

https://github.com/webpack/we...

怎樣構建聯合應用程序

讓咱們從三個獨立的應用程序開始。

App 1

配置:

我將使用 App 1 中的應用容器 App。其餘應用程序將會使用它。爲此我將其 App 公開爲 AppContainer

App 1 還將使用來自另外兩個聯合應用的組件。爲此,我指定了remotes 配置項:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  // other webpack configs...
  plugins: [
    new ModuleFederationPlugin({
      name: "app_one_remote",
      remotes: {
        app_two: "app_two_remote",
        app_three: "app_three_remote"
      },
      exposes: {
        'AppContainer':'./src/App'
      },
      shared: ["react", "react-dom","react-router-dom"]
    }),
    new HtmlWebpackPlugin({
      template: "./public/index.html",
      chunks: ["main"]
    })
  ]
}

設置構建流程:

在我應用程序的開頭加載了 app_one_remote.js。這樣能夠把你鏈接到其餘 Webpack 運行時,並在運行時預配業務編配層。這是專門設計的 Webpack 運行時和入口點。 它不是普通的應用程序入口點,只有幾個 KB

要注意這些是特殊的入口點 —— 它們只有幾KB的大小。包含能夠與主機交互的特殊 Webpack 運行時,它不是標準入口點

<head>
  <script src="http://localhost:3002/app_one_remote.js"></script>
  <script src="http://localhost:3003/app_two_remote.js"></script>
</head>
<body>
  <div id="root"></div>
</body>

從遠程主機使用代碼

App1 的頁面使用了來自App 2 的對話框組件。

const Dialog = React.lazy(() => import("app_two_remote/Dialog"));

const Page1 = () => {
    return (
        <div>
            <h1>Page 1</h1>
            <React.Suspense fallback="Loading Material UI Dialog...">
                <Dialog />
            </React.Suspense>
        </div>
    );
}

export default Page1;

路由看起來很標準:

import { Route, Switch } from "react-router-dom";

import Page1 from "./pages/page1";
import Page2 from "./pages/page2";
import React from "react";

const Routes = () => (
  <Switch>
    <Route path="/page1">
      <Page1 />
    </Route>
    <Route path="/page2">
      <Page2 />
    </Route>
  </Switch>
);

export default Routes;

App 2

配置:

App 2 將公開對話框,使 App 1 可以使用它。App 2 也會使用 App 1 的 App,所以咱們指定 app_one 爲遠端-展現雙向主機:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "app_two_remote",
      filename: "remoteEntry.js",
      exposes: {
        Dialog: "./src/Dialog"
      },
      remotes: {
        app_one: "app_one_remote",
      },
      shared: ["react", "react-dom","react-router-dom"]
    }),
    new HtmlWebpackPlugin({
      template: "./public/index.html",
      chunks: ["main"]
    })
  ]
};

使用:

根 App 以下所示:

import React from "react";
import Routes from './Routes'
const AppContainer = React.lazy(() => import("app_one_remote/AppContainer"));

const App = () => {
    return (
        <div>
            <React.Suspense fallback="Loading App Container from Host">
                <AppContainer routes={Routes}/>
            </React.Suspense>
        </div>
    );
}

export default App;

使用 Dialog 的默認頁面以下所示:

import React from 'react'
import {ThemeProvider} from "@material-ui/core";
import {theme} from "./theme";
import Dialog from "./Dialog";


function MainPage() {
    return (
        <ThemeProvider theme={theme}>
            <div>
                <h1>Material UI App</h1>
                <Dialog />
            </div>
        </ThemeProvider>
    );
}

export default MainPage

App 3

不出所料,App 3 看上去相似。可是它不會使用 App 1 中的App,它能夠做爲獨立的自運行組件(沒有導航或側邊欄)工做。因此它不指定任何 remote:

new ModuleFederationPlugin({
  name: "app_three_remote",
  library: { type: "var", name: "app_three_remote" },
  filename: "remoteEntry.js",
  exposes: {
    Button: "./src/Button"
  },
  shared: ["react", "react-dom"]
}),

瀏覽器中的最終結果

請密切注意瀏覽器中 network 標籤。該代碼將在三個不一樣的服務器之間進行聯合:三個不一樣的 bundle。一般狀況下,除非你用了 SSR 或漸進式加載,不然不要聯合整個應用程序容器。可是這個概念很是強大。

image.png

查看推文中的視頻:https://twitter.com/ScriptedA...

代碼重複

幾乎沒有依賴項重複。經過 shared 選項 —— 遠程將依賴於主機依賴關係,若是主機沒有依賴關係,則 remote 將下載本身的依賴關係。沒有代碼重複,可是內置冗餘。

image.png

手動將供應商或其餘模塊添加到 shared 並不理想。能夠用自定義編寫的函數或補充性的 Webpack 插件輕鬆地將其自動化。咱們確實打算髮布 AutomaticModuleFederationPlugin 並從 Webpack 核心外部對其進行維護。既然咱們已經在 Webpack 中內置了一流的代碼聯合支持,那麼擴展其功能就變得微不足道了。

如今有一個大問題 —— SSR 能夠勝任這項工做嗎?

3oz8xwNlejeJDQREic.gif

服務器端渲染

咱們將其設計爲通用的。模塊聯合可在任何環境中使用。在服務器端渲染聯合代碼是徹底可能的。只需讓服務器構建使用 commonjs 庫目標便可。有多種實現聯合 SSR 的方法:S3流、ESI、自動執行 npm 發佈以使用服務器變體。我計劃用公共共享文件卷或異步 S3 流在整個文件系統中流式傳輸文件,使服務器可以像在瀏覽器中同樣請求聯合代碼,並用 fs 而不是 http 來加載聯合代碼。

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "container",
      library: { type: "commonjs-module" },
      filename: "container.js",
      remotes: {
        containerB: "../1-container-full/container.js"
      },
      shared: ["react"]
    })
  ]
};
「模塊聯合也能夠與 target:"node" 一塊兒使用。做爲代替指向其餘微前端的 URL,在這裏用指向其餘微前端的文件路徑。這樣你可使用相同的代碼庫和不一樣的 webpack 配置進行 SSR,以構建 node.js。對於 node.js 中的 Module Federation,相同的屬性仍然適用:e.g. 單獨構建,單獨部署」 —— Tobias Koppers

在 Webpack 5 上聯合 Next.js

聯合須要 Webpack 5 —— Next 還沒有正式支持。可是,我確實設法 fork 並升級了 Next.js 以使其與 Webpack 5 兼容!這項工做仍在進行中。一些開發模式的中間件須要完成。生產模式目前能夠工做,一些其餘加載器仍須要從新測試。

image.png

在Twitter上查看

談話,播客或反饋

我但願有機會分享更多有關這項技術的信息。若是你想使用 Module Federation 或 Federated 體系結構,咱們很想聽聽你對當前體系結構的經驗和改進。咱們也但願有機會在播客、聚會或公司中談論它。經過 Twitter 與我聯繫https://twitter.com/ScriptedA...

你也能夠成爲個人共同創做者。請關注咱們,並獲取有關模塊聯合、FOSA(獨立應用程序聯盟)體系結構以及咱們正在建立的其餘工具的最新更新,這些工具被用於聯合應用程序

模塊聯合的示例

社區對此反應熱烈!個人共同創做者以及我本身的時間都花費在編寫到 Webpack 5 中。咱們但願最終完成其他功能並編寫一些文檔的同時,一些代碼示例會對你有所幫助:https://twitter.com/codervandal

Webpack 5 and Module Federation - A Microfrontend Revolution

因爲有足夠的帶寬,咱們將會建立 SSR 示例和更全面的演示。若是有人想構建可用做演示的東西,咱們將很樂意接受將請求並 pull 到 webpack-external-import 中。


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎繼續閱讀本專欄其它高贊文章:


相關文章
相關標籤/搜索