JS `Module Federation` (Webpack):微前端架构下的模块共享

各位观众老爷们,大家好!今天咱们就来聊聊前端界炙手可热的“Module Federation”,这玩意儿听起来玄乎,其实就是Webpack为了解决微前端架构下模块共享问题而生的一把利器。说白了,就是让不同的应用可以像搭积木一样,互相“借用”对方的模块。

开场白:微前端这出戏,Module Federation 来唱戏

在单体应用时代,咱们的代码都挤在一个“大房子”里,日子过得倒也舒坦。但随着业务越来越复杂,这个“大房子”变得臃肿不堪,每次修改都牵一发动全身,搞得大家苦不堪言。于是,微前端应运而生,它把一个大型应用拆分成若干个小的、自治的应用(也就是“小房子”),每个小房子可以独立开发、独立部署,团队之间互不干扰,是不是有点“分家单过”的意思?

但是问题来了,不同的“小房子”之间,难免会有一些公共的模块需要共享,比如常用的UI组件、工具函数等等。如果每个小房子都自己维护一份,不仅浪费资源,而且难以保证一致性,维护起来也是噩梦。这时候,Module Federation 就该闪亮登场了!它就像一个“共享仓库”,让各个小房子可以从这里拿取所需的模块,实现真正的模块共享。

第一幕:Module Federation 的基本概念

Module Federation 的核心思想是:让一个Webpack构建的应用,既可以作为“host”(宿主),提供模块给其他应用使用,也可以作为“remote”(远程),消费其他应用提供的模块。

  • Host (宿主): 一个Webpack应用,它消费其他应用的模块,并把它们集成到自己的应用中。
  • Remote (远程): 一个Webpack应用,它暴露自己的模块给其他应用使用。
  • Shared Modules (共享模块): 可以在多个应用之间共享的模块,例如React、ReactDOM、Lodash等。

简单来说,host就像一个“伸手党”,remote就像一个“大方的人”。 host从remote那里“借”模块来用,remote则把自己的模块“分享”出去。

第二幕:Module Federation 的配置

要让Module Federation发挥作用,我们需要在Webpack配置文件中进行一些设置。咱们先来看一个简单的例子:

假设我们有两个应用: app1 (Host) 和 app2 (Remote)。

app2 (Remote) 的 Webpack 配置 (webpack.config.js):

const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

module.exports = {
  mode: 'development',
  devtool: 'inline-source-map',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    publicPath: 'http://localhost:3002/', // 注意:这里的 publicPath 非常重要!
    filename: 'bundle.js'
  },
  devServer: {
    port: 3002,
    hot: true,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
      "Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization"
    }
  },
  module: {
    rules: [
      {
        test: /.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react']
          }
        }
      }
    ]
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'app2', // 必须唯一,作为模块的标识
      filename: 'remoteEntry.js', // 远程模块的入口文件
      exposes: {
        './Button': './src/Button.jsx', // 暴露 Button 组件
      },
      shared: {
        react: { singleton: true, requiredVersion: '>=16.0.0' },
        'react-dom': { singleton: true, requiredVersion: '>=16.0.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

app1 (Host) 的 Webpack 配置 (webpack.config.js):

const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

module.exports = {
  mode: 'development',
  devtool: 'inline-source-map',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    publicPath: 'http://localhost:3001/', // 注意:这里的 publicPath 非常重要!
    filename: 'bundle.js'
  },
  devServer: {
    port: 3001,
    hot: true,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
      "Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization"
    }
  },
  module: {
    rules: [
      {
        test: /.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react']
          }
        }
      }
    ]
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1', // 必须唯一,作为模块的标识
      remotes: {
        app2: 'app2@http://localhost:3002/remoteEntry.js', // 引用 app2 的地址
      },
      shared: {
        react: { singleton: true, requiredVersion: '>=16.0.0' },
        'react-dom': { singleton: true, requiredVersion: '>=16.0.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

关键配置项解释:

配置项 作用
name 每个应用必须设置一个唯一的 name,用于在其他应用中引用。
filename remoteEntry.js 是远程模块的入口文件,它包含了所有暴露的模块的信息。
exposes exposes 字段定义了哪些模块需要暴露给其他应用使用。例如,'./Button': './src/Button.jsx' 表示将 ./src/Button.jsx 模块暴露为 ./Button
remotes remotes 字段定义了需要从哪些远程应用获取模块。例如,app2: 'app2@http://localhost:3002/remoteEntry.js' 表示从 app2 应用获取模块,app2 是应用的 namehttp://localhost:3002/remoteEntry.jsremoteEntry.js 的地址。
shared shared 字段定义了哪些模块需要在应用之间共享。singleton: true 表示只加载一次共享模块,避免重复加载。requiredVersion 指定了共享模块的版本要求。
publicPath 这个非常重要!它告诉Webpack从哪个路径加载模块。如果你的应用部署在子目录,需要正确设置 publicPath,否则可能会导致模块加载失败。

第三幕:代码示例

app2 (Remote) 的 src/Button.jsx 组件:

import React from 'react';

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

export default Button;

app1 (Host) 的 src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import Button from 'app2/Button'; // 引入 app2 的 Button 组件

const App = () => {
  return (
    <div>
      <h1>App1 (Host)</h1>
      <Button>Click me!</Button>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById('root'));

运行结果:

启动 app1app2 的开发服务器后,app1 的页面会显示 app2 提供的 Button 组件,并且按钮的文本会带有 "(From App2)" 的标识。

第四幕:共享模块的策略

shared 配置项非常重要,它决定了如何处理应用之间的共享模块。Webpack 提供了多种共享策略:

  1. singleton: true: 只加载一次共享模块,适用于像React、ReactDOM这种全局唯一的模块。

  2. requiredVersion: '>=16.0.0': 指定共享模块的版本要求。如果host和remote使用的版本不兼容,Webpack会发出警告。

  3. strictVersion: true: 强制使用完全匹配的版本。如果host和remote使用的版本不一致,Webpack会报错。

  4. eager: true: 立即加载共享模块,而不是按需加载。这可以避免一些潜在的加载问题。

选择合适的共享策略非常重要,它可以避免版本冲突、重复加载等问题。

第五幕:高级用法

除了基本的模块共享,Module Federation 还有一些高级用法,可以满足更复杂的需求:

  • 动态 Remote: 可以根据运行时环境动态加载 Remote 应用。这对于插件系统、A/B 测试等场景非常有用。
  • Remote Container: 可以将多个 Remote 应用打包到一个容器中,方便管理和部署。
  • 代码分割: Module Federation 支持代码分割,可以按需加载 Remote 应用的模块,提高性能。
  • Typescript 支持: Module Federation 也支持 Typescript, 你只需要配置相应的 loader 就行。

第六幕:踩坑指南

在使用 Module Federation 的过程中,可能会遇到一些坑,这里给大家总结一下:

  • publicPath 配置错误: 这是最常见的错误,一定要确保 publicPath 配置正确,否则会导致模块加载失败。
  • 版本冲突: host和remote使用的共享模块版本不兼容,会导致运行时错误。一定要 carefully 地管理 shared 配置。
  • 循环依赖: host和remote之间存在循环依赖,会导致加载死循环。尽量避免循环依赖。
  • CORS 问题: 如果host和remote不在同一个域名下,可能会遇到CORS问题。需要在remote的服务器上配置CORS头。
  • Typescript 类型问题: 如果你使用了Typescript, 可能会遇到类型定义的问题。 确保正确配置Typescript和相关的loader。

第七幕:Module Federation 的优缺点

优点:

  • 真正的代码共享: 避免了重复开发和维护,提高了代码复用率。
  • 独立部署: 每个应用可以独立部署,降低了部署风险。
  • 技术栈无关: 不同的应用可以使用不同的技术栈,提高了灵活性。
  • 增量升级: 可以逐步升级应用,降低了升级成本。
  • 团队自治: 每个团队可以独立开发和维护自己的应用,提高了开发效率。

缺点:

  • 配置复杂: Module Federation 的配置比较复杂,需要一定的学习成本。
  • 运行时依赖: host应用在运行时依赖于remote应用,增加了系统的复杂性。
  • 版本管理: 需要 carefully 管理共享模块的版本,避免版本冲突。
  • 调试困难: 跨应用的调试比较困难。

第八幕:Module Federation 的适用场景

Module Federation 适用于以下场景:

  • 大型企业应用: 可以将大型应用拆分成多个小的、自治的应用,方便管理和维护。
  • 微前端架构: 是微前端架构的核心技术之一,可以实现真正的模块共享。
  • 插件系统: 可以将插件作为remote应用加载到host应用中,实现插件的动态扩展。
  • A/B 测试: 可以动态加载不同的remote应用,实现A/B测试。

结尾:总结与展望

Module Federation 是一种强大的模块共享技术,它可以帮助我们构建更加灵活、可维护的微前端应用。虽然它有一些缺点,但只要 carefully 配置和管理,就可以发挥出巨大的威力。

未来,Module Federation 可能会朝着更加智能化、自动化的方向发展,例如自动版本管理、自动依赖分析等。相信它会在前端领域发挥越来越重要的作用。

好了,今天的讲座就到这里,希望大家有所收获!如果有什么问题,欢迎随时提问。谢谢大家!

发表回复

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