模块联邦

  • 什么是模块联邦?模块联邦是 Webpack 5 的新特性,它允许不同的应用或组件之间进行动态的模块共享。

  • 为什么要使用模块联邦?模块联邦可以解决多个应用或组件之间的依赖问题,避免重复安装依赖,提高开发效率。

  • Webpack Module Federation

项目地址

项目目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
├── 根目录
├── packages
├── host # host 项目
├── src
├── App.tsx
├── vite-env.d.ts
├── vite.config.ts
├── remote # remote 项目
├── src
├── components
├── CustomButton
├── index.tsx
├── index.ts
├── App.tsx
├── vite.config.ts
├── package.json
├── pnpm-workspace.yaml
├── README.md

项目搭建

配置 pnpm workspaces (也可以直接创建两个项目)

  • 在根目录下执行 pnpm init -y 初始化项目

  • 然后在 package.json 中配置 workspaces

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    "name": "root",
    "private": true,
    "workspaces": ["packages/*"],
    "scripts": {
    "preinstall": "npx only-allow pnpm",
    "remote": "pnpm -F remote build && pnpm -F remote run preview",
    "host": "pnpm -F host dev",
    "clean": "pnpm -r exec rm -rf node_modules && rimraf ./node_modules"
    },
    "dependencies": {},
    "devDependencies": {}
    }
  • 在根目录下创建 pnpm-workspace.yaml 并添加下面配置

    1
    2
    packages:
    - 'packages/*'
  • 然后在根目录下创建 packages 目录 (注意:名称要跟 package.jsonworkspaces 以及 pnpm-workspace.yamlpackages 一致)

创建 hostremote 应用

  • 依次执行下面命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 进入 packages 目录
    cd ./packages

    # 创建项目 host
    pnpm create vite

    # Project name
    host

    # Select a framework
    React

    # Select a variant
    TypeScript + SWC
  • packages/host/vite.config.ts 中配置 启动端口

    1
    2
    3
    4
    5
    6
    export default defineConfig({
    plugins: [react()],
    server: {
    port: 8000,
    },
    });
  • 依次执行下面命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 进入 packages 目录
    cd ./packages

    # 创建项目 remote
    pnpm create vite

    # Project name
    remote

    # Select a framework
    React

    # Select a variant
    TypeScript + SWC
  • packages/remote/vite.config.ts 中配置 启动端口

    1
    2
    3
    4
    5
    6
    export default defineConfig({
    plugins: [react()],
    server: {
    port: 8001,
    },
    });

安装依赖项

为两个项目添加 @originjs/vite-plugin-federation

1
2
3
4
5
6
7
8
# -r 为所有项目执行命令
pnpm -r i @originjs/vite-plugin-federation -D

# 为 host 项目添加 @originjs/vite-plugin-federation
# pnpm -F host i @originjs/vite-plugin-federation -D

# 为 remote 项目添加 @originjs/vite-plugin-federation
# pnpm -F remote i @originjs/vite-plugin-federation -D

配置 hostremote 应用

  • packages/host/src/App.tsx 文件中导出并使用 CustomButton 组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import './App.css';
    import React from 'react';

    const CustomButton = React.lazy(() => import('remote/CustomButton'));

    function App() {
    return (
    <>
    <CustomButton text="Hello Host" />
    </>
    );
    }

    export default App;
  • packages/host/src/vite-env.d.ts 文件中添加下面代码, 防止引入远程模块时 爆红

    1
    2
    3
    4
    5
    6
    7
    /// <reference types="vite/client" />

    declare module 'remote/*' {
    import { ComponentType } from 'react';
    const component: ComponentType<any>;
    export default component;
    }
  • packages/host/vite.config.ts 文件中配置 @originjs/vite-plugin-federation 插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import { defineConfig } from 'vite';
    import react from '@vitejs/plugin-react-swc';
    import federation from '@originjs/vite-plugin-federation';

    // https://vite.dev/config/
    export default defineConfig({
    plugins: [
    react(),
    federation({
    // 必填, 作为远程模块的模块名称
    name: 'host',
    remotes: {
    // 作为本地模块引用的远程模块入口文件, [远程模块名称]: [远程模块入口文件地址]
    remote: 'http://localhost:8001/assets/remoteEntry.js',
    },
    shared: ['react', 'react-dom'],
    }),
    ],
    server: {
    port: 8000,
    },
    });
  • packages/remote/src/components/CustomButton/index.tsx 中自定义 CustomButton 组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    interface Props {
    /** 按钮文案 */
    text?: string;
    /** 按钮点击事件 */
    onClick?: () => void;
    /** 按钮样式 */
    style?: React.CSSProperties;
    /** 按钮类名 */
    className?: string;
    }

    /** 自定义按钮 */
    const CustomButton = (props: Props) => {
    const { text, ...rest } = props;
    return <button {...rest}>{text}</button>;
    };

    export default CustomButton;
  • packages/remote/src/components/index.ts 中导出 CustomButton 组件

    1
    export { default as CustomButton } from './CustomButton/index.tsx';
  • packages/remote/src/App.tsx 文件使用 CustomButton 组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import './App.css';
    import { CustomButton } from './components';

    function App() {
    return (
    <>
    <CustomButton text="Hello Remote" />
    </>
    );
    }

    export default App;
  • packages/remote/vite.config.ts 文件中配置 @originjs/vite-plugin-federation 插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    import { defineConfig } from 'vite';
    import react from '@vitejs/plugin-react-swc';
    import federation from '@originjs/vite-plugin-federation';

    // https://vite.dev/config/
    // federation参数参考 https://github.com/originjs/vite-plugin-federation?tab=readme-ov-file#configuration
    export default defineConfig({
    plugins: [
    react(),
    federation({
    // 必填, 作为远程模块的模块名称
    name: 'remote',
    // 非必填, 作为远程模块的入口文件, 默认为 remoteEntry.js
    filename: 'remoteEntry.js',
    // 向公众公开的组件列表
    exposes: {
    './CustomButton': './src/components/CustomButton/index.tsx',
    },
    // 本地和远程模块共享的依赖项. 本地模块需要配置所有使用的远程模块的依赖; 远程模块需要配置外部提供的组件的依赖.
    shared: ['react', 'react-dom'],
    }),
    ],
    server: {
    port: 8001,
    },
    preview: {
    // 生产端口
    // 只有 Host 端支持 dev 模式,Remote 端要求使用 生成 RemoteEntry.js 包.
    // 详见: https://github.com/originjs/vite-plugin-federation?tab=readme-ov-file#vite-dev-mode
    port: 8001,
    },
    build: {
    // 处理报错: ERROR: await is not available in the configured target environmentTop-level
    // 详见: https://github.com/originjs/vite-plugin-federation?tab=readme-ov-file#error-top-level-await-is-not-available-in-the-configured-target-environment
    target: 'esnext',
    },
    });

启动项目

  • 先启动 remote 项目 (根目录下执行)

    1
    2
    3
    4
    5
    # 需要先打包项目, 生成 remoteEntry.js
    pnpm -F remote build

    # 启动项目
    pnpm -F remote run preview
  • 再启动 host 项目 (根目录下执行)

    1
    pnpm -F host dev