The Own Lab The Own Lab

Module Federation 入門

深入理解 Module Federation 的 Host / Remote / Container 概念,以及 Webpack 5 的設定與 shared dependencies 機制

Overview##

上一篇中,我們比較了 Micro-Frontend 的三種整合模式。其中 Module Federation 是效能最好、開發體驗最接近 monolith 的方案。

Module Federation 是 Webpack 5 內建的功能,核心能力是:讓多個獨立 build 的應用在 runtime 動態載入彼此的模組,就像它們原本就在同一個 bundle 裡一樣。

這篇會深入它的運作機制、設定方式,以及最容易踩坑的 shared dependencies 管理。

Architecture##

三個核心角色###

Module Federation 的架構由三個角色組成:

graph TD
  H[Host App] -->|loads at runtime| RE1[remoteEntry.js]
  H -->|loads at runtime| RE2[remoteEntry.js]
  RE1 --> R1[Remote A: Product]
  RE2 --> R2[Remote B: Cart]
  R1 -->|exposes| M1[ProductCard]
  R1 -->|exposes| M2[ProductList]
  R2 -->|exposes| M3[CartWidget]
角色說明類比
Host主應用,負責載入其他應用的模組插座(接收插頭)
Remote被載入的應用,對外暴露特定模組插頭(提供功能)
ContainerRemote 的入口檔案(remoteEntry.js),提供 get()init() 方法插頭上的規格標籤

Note

同一個應用可以同時是 Host 和 Remote。例如,Product App 可以 expose 自己的元件給 Shell,同時 consume Cart App 的購物車 Widget。這讓架構非常靈活。

載入流程###

當 Host 需要一個 Remote 的模組時,實際發生的事情是:

sequenceDiagram
  participant H as Host
  participant C as Container (remoteEntry.js)
  participant R as Remote Chunk

  H->>C: 1. Load remoteEntry.js
  H->>C: 2. init(sharedScope)
  H->>C: 3. get('./ProductCard')
  C->>R: 4. Load module chunk
  R-->>H: 5. Return module
  1. Host 載入 Remote 的 remoteEntry.js(Container)
  2. Host 呼叫 init(),傳入 shared scope(共享依賴的版本資訊)
  3. Host 呼叫 get('./ProductCard'),請求特定模組
  4. Container 非同步載入對應的 chunk
  5. 模組回傳給 Host 使用

Tip

整個過程是非同步的。這就是為什麼在 React 中使用 Remote 模組時,一定要用 React.lazy() + <Suspense> — 模組是在 runtime 才下載的。

Shared Dependencies 機制###

Module Federation 最強大的功能之一是 dependency sharing — 多個應用共用同一份 React,而不是各自打包一份。

graph TD
  subgraph Without Sharing
    A1[Host: React 18.2]
    B1[Remote A: React 18.2]
    C1[Remote B: React 18.2]
  end

  subgraph With Sharing
    S[Shared: React 18.2]
    A2[Host] --> S
    B2[Remote A] --> S
    C2[Remote B] --> S
  end

沒有 sharing 時,3 個應用各打包一份 React(~140KB x 3 = 420KB)。有 sharing 時,只載入一份(~140KB)。

但 sharing 不只是省 bundle size — 對 React 來說,singleton 是必要的。如果同時存在兩個 React instance,hooks 會壞掉。

Setup##

Prerequisites###

  • Node.js 18+
  • Webpack 5 或 Rspack

Remote 端設定(被載入的應用)###

// apps/product/webpack.config.ts
import { readFileSync } from 'node:fs';
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');

const deps = JSON.parse(
  readFileSync(new URL('./package.json', import.meta.url), 'utf-8'),
).dependencies;

export default {
  mode: 'production',
  output: {
    publicPath: 'https://product.example.com/',
  },
  plugins: [
    new ModuleFederationPlugin({
      // unique name for this remote
      name: 'productApp',

      // container entry filename
      filename: 'remoteEntry.js',

      // modules to expose to consumers
      exposes: {
        './ProductCard': './src/components/ProductCard',
        './ProductList': './src/components/ProductList',
      },

      // shared dependencies
      shared: {
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        'react-dom': {
          singleton: true,
          requiredVersion: deps['react-dom'],
        },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

Host 端設定(主應用)###

// apps/shell/webpack.config.ts
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',

      // declare remotes and their entry points
      remotes: {
        productApp: 'productApp@https://product.example.com/remoteEntry.js',
        cartApp: 'cartApp@https://cart.example.com/remoteEntry.js',
      },

      shared: {
        react: {
          singleton: true,
          requiredVersion: '^18.0.0',
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.0.0',
        },
      },
    }),
  ],
};

Usage##

Basic Usage###

Remote 端 — 暴露元件:

// apps/product/src/components/ProductCard.tsx
interface ProductCardProps {
  productId: string;
  onAddToCart?: (id: string) => void;
}

export default function ProductCard({ productId, onAddToCart }: ProductCardProps) {
  return (
    <div className="product-card">
      <h3>Product {productId}</h3>
      <button onClick={() => onAddToCart?.(productId)}>Add to Cart</button>
    </div>
  );
}

Host 端 — 消費 Remote 元件:

// apps/shell/src/App.tsx
import React, { Suspense } from 'react';

// Dynamic import from remote — this is resolved at runtime
const ProductCard = React.lazy(() => import('productApp/ProductCard'));

export default function App() {
  const handleAddToCart = (id: string) => {
    console.log(`Added product ${id} to cart`);
  };

  return (
    <div>
      <h1>My Store</h1>
      <Suspense fallback={<div>Loading product...</div>}>
        <ProductCard productId="42" onAddToCart={handleAddToCart} />
      </Suspense>
    </div>
  );
}

TypeScript 型別支援:

Host 端不知道 Remote 的型別。需要手動宣告:

// apps/shell/src/types/remotes.d.ts
declare module 'productApp/ProductCard' {
  import { ComponentType } from 'react';

  interface ProductCardProps {
    productId: string;
    onAddToCart?: (id: string) => void;
  }

  const ProductCard: ComponentType<ProductCardProps>;
  export default ProductCard;
}

Warning

型別宣告需要手動同步。當 Remote 的 props 改變時,Host 的 .d.ts 不會自動更新。Module Federation 2.0 引入了 dynamic type hinting 來解決這個問題,但 Webpack 5 內建版本仍需要手動管理。

Advanced Usage — Shared Dependencies 深入###

Shared dependencies 的設定看似簡單,但版本衝突是 Module Federation 最常見的問題來源。

// Understanding shared configuration options
new ModuleFederationPlugin({
  shared: {
    react: {
      singleton: true, // only one instance allowed globally
      requiredVersion: '^18.0.0', // acceptable version range
      strictVersion: true, // throw error if version mismatch (not just warning)
      eager: true, // include in initial chunk, not async loaded
      version: '18.3.1', // advertise this version to other containers
    },
  },
});

各選項的影響:

選項預設說明何時使用
singletonfalse全域只允許一個 instanceReact、Vue 等有 global state 的 library
requiredVersionpackage.json 推斷可接受的 semver 範圍指定相容版本
strictVersionfalse版本不符時 throw error(否則只 warning)production 環境防止隱性 bug
eagerfalse放入 initial chunk 而非 async 載入避免 async boundary 或用於 SSR
versionpackage.json 推斷對外宣告的版本號覆寫自動推斷的版本

常見的版本衝突場景:

Host:     react@18.2.0 (singleton: true, requiredVersion: "^18.0.0")
Remote A: react@18.3.1 (singleton: true, requiredVersion: "^18.0.0")
Remote B: react@17.0.2 (singleton: true, requiredVersion: "^17.0.0")

Host 和 Remote A 相容(都是 18.x),但 Remote B 要求 17.x。Module Federation 的行為:

  • strictVersion: false(預設) → 使用已載入的 18.x 版本,console 印出 warning
  • strictVersion: true → 直接 throw error,應用崩潰

Warning

Singleton 衝突是最危險的問題。 如果 React 同時存在兩個 instance,所有 hooks 都會壞掉(Invalid hook call error)。確保所有 Host 和 Remote 的 React 版本在同一個 major version。

錯誤處理###

Remote 可能因為網路問題或部署時間差而載入失敗。必須做好 fallback:

// apps/shell/src/components/RemoteWrapper.tsx
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

interface RemoteWrapperProps {
  children: React.ReactNode;
  fallback?: React.ReactNode;
}

function ErrorFallback({ error }: { error: Error }) {
  return (
    <div role="alert">
      <p>This section is temporarily unavailable.</p>
      <pre>{error.message}</pre>
    </div>
  );
}

export function RemoteWrapper({ children, fallback }: RemoteWrapperProps) {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <Suspense fallback={fallback ?? <div>Loading...</div>}>{children}</Suspense>
    </ErrorBoundary>
  );
}
// Usage
<RemoteWrapper fallback={<ProductCardSkeleton />}>
  <ProductCard productId="42" />
</RemoteWrapper>

Configuration Reference##

ModuleFederationPlugin 主要選項###

選項型別說明
namestringContainer 的唯一名稱,不可重複
filenamestringContainer entry 的檔名,通常是 remoteEntry.js
exposesRecord<string, string>對外暴露的模組路徑映射
remotesRecord<string, string>要消費的 Remote 應用列表
sharedArray | Record共享依賴的設定
library{ type, name }Container 的 library 輸出格式
shareScopestring共享作用域的名稱,預設 default

Remote 字串格式###

remotes: {
  // format: "<internalName>": "<containerName>@<remoteEntryUrl>"
  productApp: "productApp@https://cdn.example.com/product/remoteEntry.js",
}

安裝###

指令用途
pnpm add webpack@5 -D安裝 webpack 5(已內建 Module Federation)
pnpm add @module-federation/enhanced -D安裝 Module Federation 2.0 enhanced 版本

Quiz##

Single Choice

Module Federation 的三個核心角色中,remoteEntry.js 屬於哪個角色?

Single Choice

為什麼 React 在 Module Federation 的 shared dependencies 中必須設為 singleton: true

Single Choice

在 Host 端使用 Remote 元件時,以下哪些是必要的?

Code Challengepython

Shared dependencies 使用 semver 來判斷版本相容性。寫一個函式 is_compatible(required, actual),判斷 actual 版本是否符合 required 的 ^ 語意(相同 major version 且 actual >= required)。

Ctrl+Enter to run

Summary##

  • Module Federation 讓多個獨立 build 在 runtime 共享模組。 Host 載入 Remote 的 remoteEntry.js,透過 init() + get() 取得模組。
  • 三個核心角色:Host(消費者)、Remote(提供者)、Container(入口)。 同一個應用可以同時扮演 Host 和 Remote。
  • Shared dependencies 是最重要也最容易出錯的設定。 React 等 library 務必設為 singleton: true,並確保所有應用的 major version 一致。
  • 永遠要做錯誤處理。 Remote 可能載入失敗,用 ErrorBoundary + Suspense 確保 Host 不會因此崩潰。
  • 型別需要手動同步。 Webpack 5 內建的 Module Federation 沒有自動型別推導,考慮升級到 Module Federation 2.0 或手動維護 .d.ts

留言 (0)

登入後即可留言