JS `Module Federation` (Webpack):实现跨应用模块共享与微前端

各位同学,晚上好!我是今天的主讲人,咱们今天来聊聊一个前端圈里挺火的技术——Webpack 的 Module Federation。这玩意儿,说白了,就是让你在不同的应用之间共享代码,搞微前端,让你的前端架构更灵活、更高效。准备好了吗?咱们开讲!

一、什么是 Module Federation?

Module Federation,中文可以翻译成“模块联邦”,听起来就挺厉害的。它允许你将 Webpack 打包的应用拆分成更小的、独立的模块,这些模块可以被其他应用动态地加载和使用。 想象一下,你有一个组件库,包含了各种按钮、输入框、表格等等。以前,每个应用都要复制一份这个组件库,费时费力。现在有了 Module Federation,你可以把这个组件库打包成一个联邦模块,其他应用直接从这个联邦模块里引用组件,省时省力,还保证了组件的统一性。

二、为什么要用 Module Federation?

这问题问得好!Module Federation 解决了很多痛点:

  • 代码共享: 避免重复造轮子,提高代码复用率。
  • 独立部署: 每个应用都可以独立部署,互不影响。
  • 动态更新: 联邦模块更新后,引用它的应用会自动更新,无需重新部署。
  • 技术栈无关: 联邦模块可以被不同技术栈的应用引用,例如 React 应用可以引用 Vue 应用的组件。

简而言之,它提高了开发效率,降低了维护成本,让你的前端架构更加现代化。

三、Module Federation 的核心概念

在深入代码之前,先了解几个核心概念:

  • Host (宿主): 消费其他远程模块的应用。
  • Remote (远程): 提供可被其他应用消费的模块的应用。
  • Shared Modules (共享模块): Host 和 Remote 之间共享的依赖项。

你可以把 Host 理解为顾客,Remote 理解为商家,Shared Modules 理解为他们共同使用的原材料。顾客从商家那里购买商品,他们都用到了相同的原材料。

四、Module Federation 的配置

配置 Module Federation 主要涉及到 webpack.config.js 文件。我们需要配置 ModuleFederationPlugin 插件。 下面是一个简单的例子:

1. Remote 应用 (提供组件):

// webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');

module.exports = {
  mode: 'development', //或者production
  devtool: 'source-map',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    publicPath: 'http://localhost:3001/', // 必须以/结尾
    filename: 'remoteEntry.js',
    libraryTarget: 'umd',
  },
  devServer: {
    port: 3001,
    hot: true,
    headers: {
      'Access-Control-Allow-Origin': '*', // 允许跨域
    },
  },
  module: {
    rules: [
      {
        test: /.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react'],
          },
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'remoteApp', // 必须唯一
      filename: 'remoteEntry.js', // 暴露的文件名
      exposes: {
        './Button': './src/Button', // 暴露的模块
        './utils': './src/utils'
      },
      shared: {
        react: { singleton: true, requiredVersion: false },
        'react-dom': { singleton: true, requiredVersion: false },
      },
    }),
  ],
};

说明:

  • name: Remote 应用的名称,必须唯一。
  • filename: 暴露的文件名,一般是 remoteEntry.js
  • exposes: 定义要暴露的模块,key 是暴露的模块名,value 是模块的路径。 这里暴露了 ./src/Button 组件,模块名为 ./Button。 也暴露了 ./src/utils 文件,模块名为 ./utils
  • shared: 定义共享的依赖项。singleton: true 表示只加载一个版本的依赖项。 requiredVersion: false 表示不强制要求版本一致。

2. Host 应用 (消费组件):

// webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');

module.exports = {
  mode: 'development', //或者production
  devtool: 'source-map',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    publicPath: 'http://localhost:3000/', // 必须以/结尾
    filename: 'bundle.js',
  },
  devServer: {
    port: 3000,
    hot: true,
  },
  module: {
    rules: [
      {
        test: /.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react'],
          },
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'hostApp', // 必须唯一
      remotes: {
        remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js', // 远程应用
      },
      shared: {
        react: { singleton: true, requiredVersion: false },
        'react-dom': { singleton: true, requiredVersion: false },
      },
    }),
  ],
};

说明:

  • name: Host 应用的名称,必须唯一。
  • remotes: 定义远程应用。key 是远程应用的别名,value 是远程应用的名称和 remoteEntry.js 的 URL。 这里定义了一个远程应用 remoteApp,它的 URL 是 http://localhost:3001/remoteEntry.js

五、使用 Remote 组件

在 Host 应用中,你可以像使用本地组件一样使用 Remote 组件:

// src/App.js
import React, { Suspense } from 'react';

const RemoteButton = React.lazy(() => import('remoteApp/Button')); // 使用remoteApp暴露出来的Button
const RemoteUtils = React.lazy(() => import('remoteApp/utils'));

const App = () => {
  return (
    <div>
      <h1>Host App</h1>
      <Suspense fallback={'Loading Button...'}>
        <RemoteButton />
      </Suspense>
      <Suspense fallback={'Loading Utils...'}>
        <RemoteUtils />
      </Suspense>
    </div>
  );
};

export default App;

说明:

  • 使用 React.lazy 动态加载 Remote 组件。
  • remoteApp/Button 表示加载 remoteApp 应用中暴露的 Button 模块。

六、Shared Modules 的重要性

Shared Modules 是 Module Federation 的关键。它允许 Host 和 Remote 共享依赖项,避免重复加载和版本冲突。

1. 解决依赖冲突

如果没有 Shared Modules,Host 和 Remote 可能会加载不同版本的 React,导致运行时错误。

2. 优化加载性能

通过共享依赖项,可以减少重复加载,提高加载性能。

3. 配置 Shared Modules 的策略

  • 精确匹配版本: 强制要求 Host 和 Remote 使用相同的版本。
  • 范围匹配版本: 允许 Host 和 Remote 使用兼容的版本范围。
  • 忽略版本: 不强制要求版本一致,但可能会导致运行时错误。

webpack.config.js 中,你可以使用以下配置:

shared: {
  react: { singleton: true, requiredVersion: '17.0.0' }, // 精确匹配版本
  'react-dom': { singleton: true, requiredVersion: '^17.0.0' }, // 范围匹配版本
  lodash: { singleton: true, requiredVersion: false }, // 忽略版本
},

七、高级用法

Module Federation 还有很多高级用法,可以满足更复杂的需求:

  • 动态 Remote: 根据环境动态加载 Remote 应用。
  • Fallback Modules: 在 Remote 应用加载失败时,使用 Fallback 模块。
  • Custom Module Resolvers: 自定义模块解析器。

1. 动态 Remote

你可以根据环境变量或用户配置,动态加载 Remote 应用。

// webpack.config.js
remotes: {
  remoteApp: `remoteApp@${process.env.REMOTE_URL}/remoteEntry.js`,
},

2. Fallback Modules

在 Remote 应用加载失败时,可以使用 Fallback 模块。

// webpack.config.js
remotes: {
  remoteApp: {
    entry: 'remoteApp@http://localhost:3001/remoteEntry.js',
    fallback: './src/FallbackModule',
  },
},

3. Custom Module Resolvers

你可以自定义模块解析器,实现更灵活的模块加载策略。

八、最佳实践

在使用 Module Federation 时,有一些最佳实践可以帮助你避免一些常见问题:

  • 统一技术栈: 尽量使用相同的技术栈,减少依赖冲突。
  • 合理划分模块: 将应用拆分成更小的、独立的模块,提高代码复用率。
  • 谨慎使用 Shared Modules: 只共享必要的依赖项,避免过度共享。
  • 监控加载性能: 监控 Remote 应用的加载性能,及时优化。

九、Module Federation 的优缺点

优点:

优点 描述
代码共享 避免重复造轮子,提高代码复用率。
独立部署 每个应用都可以独立部署,互不影响。
动态更新 联邦模块更新后,引用它的应用会自动更新,无需重新部署。
技术栈无关 联邦模块可以被不同技术栈的应用引用,例如 React 应用可以引用 Vue 应用的组件。

缺点:

缺点 描述
配置复杂 需要配置 Webpack,有一定的学习成本。
依赖管理 需要仔细管理 Shared Modules,避免依赖冲突。
加载性能 Remote 应用的加载可能会影响应用的性能。
安全问题 需要注意 Remote 应用的安全问题,避免恶意代码注入。

十、总结

Module Federation 是一个强大的工具,可以帮助你构建更灵活、更高效的前端架构。 但是,它也有一定的复杂性,需要仔细配置和管理。希望今天的讲座能帮助你更好地理解 Module Federation,并在实际项目中应用它。

最后,祝大家编程愉快!如果有什么问题,欢迎随时提问。 下课!

发表回复

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