分析 `Webpack Module Federation` 在微前端架构中实现模块共享和版本兼容的复杂机制。

各位前端的英雄好汉,大家好!我是今天的主讲人,咱们今天聊聊微前端架构里的大杀器——Webpack Module Federation。这玩意儿,说白了,就是解决微前端之间代码共享和版本冲突的难题的。

开场白:微前端的那些事儿

话说,微前端这概念,大家应该都不陌生。它就像把一个巨大的单体应用拆成一个个小的、独立的“微应用”。这些微应用可以由不同的团队开发、独立部署,最后拼装在一起,给用户提供完整的体验。

这样做的好处嘛,那可多了去了:

  • 技术栈自由: 每个微应用可以选择自己喜欢的技术栈,不用被统一的技术栈绑死。
  • 独立部署: 每个微应用可以独立发布,互不影响,大大提升了开发效率。
  • 团队自治: 每个团队可以负责自己的微应用,职责清晰,更容易管理。

但是!问题也来了:

  • 代码重复: 不同的微应用可能需要用到相同的组件或者工具函数,如果没有一个好的共享机制,就会出现大量的代码重复。
  • 版本冲突: 不同的微应用可能依赖同一个第三方库的不同版本,如果没有一个好的版本管理机制,就会出现版本冲突,导致应用崩溃。

这时候,Webpack Module Federation 就该闪亮登场了!

Module Federation:共享的魔法

Module Federation,顾名思义,就是模块联邦。它可以让不同的 Webpack 构建的应用之间共享模块,就像联邦国家一样,每个州(微应用)可以共享资源,但又保持独立性。

Module Federation 的基本概念

在深入代码之前,我们需要先了解几个关键的概念:

  • Host(宿主): 消费其他微应用暴露的模块的应用。
  • Remote(远程): 暴露模块给其他微应用消费的应用。
  • Shared Modules(共享模块): 可以被 Host 和 Remote 共享的模块,比如 React、Vue、Lodash 等等。

Module Federation 的工作原理

Module Federation 的核心思想是:在构建时,将 Remote 应用暴露的模块的信息(比如模块的名称、版本、入口地址)写入一个 manifest 文件中。Host 应用在运行时,会根据 manifest 文件去加载 Remote 应用暴露的模块。

代码实战:手把手教你用 Module Federation

光说不练假把式,接下来咱们就用代码来演示一下 Module Federation 的用法。

场景: 假设我们有两个微应用:app1app2app1 是 Host,app2 是 Remote。app2 暴露一个名为 Button 的 React 组件给 app1 使用。

1. 创建两个项目

首先,创建两个空的 React 项目:

npx create-react-app app1
npx create-react-app app2

2. 配置 app2 (Remote)

进入 app2 目录,安装 webpackwebpack-cli

cd app2
npm install webpack webpack-cli --save-dev

app2 目录下创建一个 webpack.config.js 文件,配置如下:

const path = require('path');
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;

module.exports = {
  entry: './src/index',
  mode: 'development',
  devtool: 'source-map',
  devServer: {
    static: {
      directory: path.join(__dirname, 'dist'),
    },
    port: 3002,
  },
  output: {
    publicPath: 'auto',
  },
  module: {
    rules: [
      {
        test: /.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['@babel/preset-react'],
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'app2',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/Button',
      },
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        'react-dom': {
          singleton: true,
          requiredVersion: deps['react-dom'],
        },
      },
    }),
  ],
  resolve: {
    extensions: ['.js', '.jsx'],
  },
};
  • name: Remote 应用的名称,必须唯一。
  • filename: 生成的 manifest 文件的名称。
  • exposes: 暴露的模块,./Button 是模块的别名,./src/Button 是模块的实际路径。
  • shared: 共享的模块,这里我们共享了 reactreact-domsingleton: true 表示只加载一个版本的 reactreact-domrequiredVersion: deps.react 表示必须满足 package.json 中定义的 react 版本。

创建一个 src/Button.jsx 文件:

import React from 'react';

const Button = ({ children }) => {
  return <button style={{ backgroundColor: 'lightblue' }}>{children}</button>;
};

export default Button;

修改 app2package.json 文件,添加一个 build 命令:

{
  "scripts": {
    "start": "react-scripts start",
    "build": "webpack --mode production",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  }
}

运行 npm run build 构建 app2

3. 配置 app1 (Host)

进入 app1 目录,安装 webpackwebpack-cli

cd ../app1
npm install webpack webpack-cli --save-dev

app1 目录下创建一个 webpack.config.js 文件,配置如下:

const path = require('path');
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;

module.exports = {
  entry: './src/index',
  mode: 'development',
  devtool: 'source-map',
  devServer: {
    static: {
      directory: path.join(__dirname, 'dist'),
    },
    port: 3001,
  },
  output: {
    publicPath: 'auto',
  },
  module: {
    rules: [
      {
        test: /.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['@babel/preset-react'],
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      remotes: {
        app2: 'app2@http://localhost:3002/remoteEntry.js',
      },
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        'react-dom': {
          singleton: true,
          requiredVersion: deps['react-dom'],
        },
      },
    }),
  ],
  resolve: {
    extensions: ['.js', '.jsx'],
  },
};
  • remotes: 指定 Remote 应用的信息,app2 是 Remote 应用的名称,http://localhost:3002/remoteEntry.js 是 Remote 应用的 manifest 文件的地址。

修改 app1src/App.js 文件,引入 app2 暴露的 Button 组件:

import React, { Suspense, lazy } from 'react';

const RemoteButton = lazy(() => import('app2/Button'));

function App() {
  return (
    <div>
      <h1>App1</h1>
      <Suspense fallback="Loading Button...">
        <RemoteButton>Hello from App2!</RemoteButton>
      </Suspense>
    </div>
  );
}

export default App;
  • 这里使用了 React.lazySuspense 来实现动态加载 Remote 组件。

修改 app1package.json 文件,添加一个 build 命令:

{
  "scripts": {
    "start": "react-scripts start",
    "build": "webpack --mode production",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  }
}

运行 npm run build 构建 app1

4. 运行项目

分别运行 app1app2

# 在 app1 目录下
npm start

# 在 app2 目录下
npm start

打开浏览器,访问 http://localhost:3001,你就能看到 app1 成功加载了 app2 暴露的 Button 组件啦!

Module Federation 的高级用法

上面的例子只是 Module Federation 的一个简单应用,它还有很多高级用法,比如:

  • 动态 Remote: 可以根据不同的环境加载不同的 Remote 应用。
  • 版本控制: 可以指定 Remote 应用的版本,避免版本冲突。
  • 共享模块的高级配置: 可以更灵活地配置共享模块,比如指定加载策略、版本范围等等。

1. 动态 Remote

假设我们想要根据环境变量来决定加载哪个 Remote 应用。

修改 app1webpack.config.js 文件:

const path = require('path');
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;

const remoteUrl = process.env.REMOTE_URL || 'http://localhost:3002/remoteEntry.js';

module.exports = {
  // ... 其他配置
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      remotes: {
        app2: `app2@${remoteUrl}`,
      },
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        'react-dom': {
          singleton: true,
          requiredVersion: deps['react-dom'],
        },
      },
    }),
  ],
  // ... 其他配置
};

在运行 app1 时,可以通过设置 REMOTE_URL 环境变量来指定 Remote 应用的地址:

REMOTE_URL=http://localhost:3003/remoteEntry.js npm start

2. 版本控制

假设 app2 有两个版本:1.0.02.0.0。我们想要 app1 使用 app21.0.0 版本。

首先,修改 app2package.json 文件,将版本设置为 1.0.0

{
  "name": "app2",
  "version": "1.0.0",
  // ... 其他配置
}

构建 app2

然后,修改 app1webpack.config.js 文件:

const path = require('path');
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;

module.exports = {
  // ... 其他配置
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      remotes: {
        app2: 'app2@http://localhost:3002/remoteEntry.js',
      },
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        'react-dom': {
          singleton: true,
          requiredVersion: deps['react-dom'],
        },
      },
    }),
  ],
  // ... 其他配置
};

shared 中,我们可以指定共享模块的版本范围:

shared: {
  react: {
    singleton: true,
    requiredVersion: {
      // 指定 react 的版本范围
      "^16.0.0": "16.x",
      "^17.0.0": "17.x",
    }[deps.react] || deps.react, // 默认使用 package.json 中定义的版本
  },
  "react-dom": {
    singleton: true,
    requiredVersion: {
      "^16.0.0": "16.x",
      "^17.0.0": "17.x",
    }[deps["react-dom"]] || deps["react-dom"], // 默认使用 package.json 中定义的版本
  },
},

Module Federation 的优缺点

优点:

  • 代码共享: 避免了代码重复,减少了应用体积。
  • 版本管理: 解决了版本冲突的问题,保证了应用的稳定性。
  • 技术栈自由: 不同的微应用可以使用不同的技术栈。
  • 独立部署: 每个微应用可以独立发布,互不影响。

缺点:

  • 配置复杂: 需要配置 Webpack,有一定的学习成本。
  • 运行时依赖: Host 应用需要在运行时加载 Remote 应用的模块,可能会影响性能。
  • 调试困难: 跨应用的调试可能会比较困难。

Module Federation 的应用场景

Module Federation 适用于以下场景:

  • 微前端架构: 这是 Module Federation 最典型的应用场景。
  • 插件系统: 可以使用 Module Federation 来实现插件的动态加载。
  • 大型单体应用: 可以使用 Module Federation 将单体应用拆分成多个模块,提高开发效率。

总结

Module Federation 是一个强大的工具,它可以帮助我们构建更加灵活、可维护的微前端架构。虽然它有一定的学习成本,但是一旦掌握了它,你就能体会到它的强大之处。

Module Federation 的配置参数详解

为了让大家更深入地了解 Module Federation,下面我们来详细地讲解一下它的配置参数。

ModuleFederationPlugin 的配置参数

参数名 类型 描述

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注