JS `Module Federation` `Shared Scopes` 与 `Singleton` 共享机制

各位观众,各位老铁,大家好!我是今天的主讲人,咱们今天聊聊Module Federation里头有点意思的玩意儿:共享作用域(Shared Scopes)和单例(Singleton)共享机制。这俩哥们儿是解决模块联邦里依赖重复加载,版本冲突问题的利器。准备好了吗?咱们开始!

开场白:模块联邦的烦恼

设想一下,你开发了一个电商网站,用了Module Federation把商品展示模块、购物车模块、用户中心模块都拆分成了独立的微前端应用。每个模块都依赖了React。如果没有特殊的处理,每个模块都把自己那份React打进去,最后用户访问你的网站,吭哧吭哧下载了三份React!这不仅浪费带宽,还可能导致各种运行时的冲突,比如React Context用起来不正常了。

这时候,Shared Scopes和Singleton就该闪亮登场了。

第一幕:Shared Scopes – “共享单车”模式

Shared Scopes,翻译过来就是“共享作用域”。它的核心思想是,把一些公共的依赖,比如React,React-DOM,放到一个“共享池”里。每个模块先去这个池子里找,如果已经有了,就直接用,没有再加载。这就像共享单车,谁需要谁骑走,用完放回去,大家一起用,避免重复购买。

怎么配置 Shared Scopes?

webpack.config.js 里,通过 ModuleFederationPluginshared 字段来配置。

const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  // ... 其他配置
  plugins: [
    new ModuleFederationPlugin({
      name: 'host', // 主应用的名字
      remotes: { // 远程模块的配置
        'remote_app': 'remote_app@http://localhost:3001/remoteEntry.js',
      },
      shared: { // 共享依赖配置
        react: {
          singleton: true, // 强制单例
          requiredVersion: '^17.0.0', // 必须满足的版本
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^17.0.0',
        },
      },
    }),
  ],
};

解释一下几个关键的参数:

  • name: 这个模块的名字,随便起,但是要唯一。
  • remotes: 远程模块的配置,告诉Webpack去哪里找远程模块。这里remote_app是远程模块的名字,remote_app@http://localhost:3001/remoteEntry.js 是远程模块的入口文件地址。
  • shared: 共享依赖的配置,这是重点!
    • react: 要共享的依赖的名字,这里是React。
    • singleton: 是否强制单例,true 表示强制单例,false 表示不强制单例。后面我们会详细讨论单例模式。
    • requiredVersion: 要求的版本范围,只有满足这个版本范围的依赖才能被共享。

远程模块的配置:

远程模块也需要配置 shared,这样才能把自己的依赖暴露出来,供其他模块使用。

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

module.exports = {
  // ... 其他配置
  plugins: [
    new ModuleFederationPlugin({
      name: 'remote_app', // 远程模块的名字
      exposes: { // 暴露的模块
        './RemoteComponent': './src/RemoteComponent',
      },
      shared: { // 共享依赖配置
        react: {
          singleton: true,
          requiredVersion: '^17.0.0',
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^17.0.0',
        },
      },
    }),
  ],
};

注意,远程模块需要配置 exposes 来暴露自己的模块,这样主应用才能加载它。

工作流程:

  1. 主应用或者远程模块在加载依赖的时候,首先会检查 shared 作用域里有没有已经加载过的相同依赖。
  2. 如果有,并且版本符合 requiredVersion 的要求,就直接使用 shared 作用域里的依赖。
  3. 如果没有,或者版本不符合要求,就加载自己的依赖。

Shared Scopes 的优点:

  • 减少重复加载: 避免了重复加载相同的依赖,减少了打包体积,提高了加载速度。
  • 版本兼容性: requiredVersion 可以保证不同模块使用的依赖版本是兼容的。

第二幕:Singleton – “独生子女”模式

Singleton,翻译过来就是“单例”。顾名思义,它保证某个依赖在整个应用中只有一个实例。这就像独生子女,家里只有一个孩子,大家都得围着他转。

为什么要用 Singleton?

有些依赖,比如React Context,Redux store,如果存在多个实例,就会导致各种问题。比如,不同的组件拿到的Context是不同的,Redux store的状态不同步。

Singleton 和 Shared Scopes 的关系:

Singleton 往往和 Shared Scopes 结合使用。通过在 shared 配置里设置 singleton: true,可以强制把某个依赖变成单例。

shared: {
  react: {
    singleton: true, // 强制单例
    requiredVersion: '^17.0.0',
  },
}

Singleton 的注意事项:

  • 版本一致性: 如果多个模块都声明了同一个依赖是单例,但是版本不一致,Webpack会发出警告,甚至报错。这时候,你需要手动解决版本冲突,保证所有模块使用的都是同一个版本的依赖。
  • 初始化时机: 单例依赖的初始化时机很重要。一般来说,应该在主应用里初始化单例依赖,然后共享给其他模块。这样可以保证所有模块使用的都是同一个实例。

第三幕:版本冲突的解决之道

Module Federation里,版本冲突是个常见的问题。假设主应用依赖 React 17,远程模块依赖 React 18,如果没有处理,就会导致运行时错误。

Webpack 提供的策略:

Webpack 提供了一些策略来解决版本冲突:

  • strictVersion: 严格模式,要求所有模块必须使用完全相同的版本。如果版本不一致,Webpack会报错。
  • requiredVersion: 指定要求的版本范围。只有满足这个版本范围的依赖才能被共享。
  • singleton: 强制单例,可以避免多个实例之间的冲突。
  • eager: 立即加载共享模块,而不是延迟加载。这可以解决一些初始化顺序的问题。

实战演练:解决 React 版本冲突

假设主应用(Host)依赖 [email protected],远程模块(Remote)依赖 [email protected]

1. 统一版本:

最简单的办法就是把所有模块的 React 版本都升级到 18.0.0,或者都降级到 17.0.0。这是最彻底的解决方案,但是可能需要修改代码。

2. 使用 requiredVersionsingleton

在主应用和远程模块的 webpack.config.js 里,配置 shared 字段:

// 主应用 webpack.config.js
shared: {
  react: {
    singleton: true,
    requiredVersion: '^17.0.0', // 允许的版本范围
  },
  'react-dom': {
    singleton: true,
    requiredVersion: '^17.0.0',
  },
}

// 远程模块 webpack.config.js
shared: {
  react: {
    singleton: true,
    requiredVersion: '^17.0.0', // 允许的版本范围
  },
  'react-dom': {
    singleton: true,
    requiredVersion: '^17.0.0',
  },
}

这样配置之后,如果主应用先加载,那么远程模块就会使用主应用的 React 17。如果远程模块先加载,那么主应用就会使用远程模块的 React 18,但是因为主应用声明了 requiredVersion: '^17.0.0',所以Webpack会发出警告。

3. 使用版本别名 (Version Aliasing):

可以使用Webpack的 alias 配置来将远程模块的 React 18 别名到 React 17。但是这种方法比较 tricky,不推荐使用。

// webpack.config.js
resolve: {
  alias: {
    'react': path.resolve(__dirname, 'node_modules/react'), // 指向主应用的React
    'react-dom': path.resolve(__dirname, 'node_modules/react-dom'), // 指向主应用的React-DOM
  }
}

总结:

解决版本冲突的关键在于统一版本,或者使用 requiredVersionsingleton 来限制版本范围。选择哪种方案取决于你的具体情况。

第四幕:高级技巧和注意事项

  • 动态 Shared Scope: 有时候,你可能需要在运行时动态地添加 Shared Scope。比如,根据用户的配置加载不同的主题。可以使用 __webpack_init_sharing____webpack_share_scopes__ API 来实现动态 Shared Scope。

    // 初始化共享作用域
    __webpack_init_sharing__('default');
    
    // 获取默认的共享作用域
    const shareScope = __webpack_share_scopes__.default;
    
    // 动态添加共享依赖
    shareScope.moment = {
      get: () => require('moment'),
      version: '2.29.1',
    };
  • eager 参数: eager: true 可以立即加载共享模块,而不是延迟加载。这可以解决一些初始化顺序的问题。但是,eager 会增加初始加载时间,所以要谨慎使用。

    shared: {
      react: {
        singleton: true,
        requiredVersion: '^17.0.0',
        eager: true, // 立即加载
      },
    }
  • 处理循环依赖: Module Federation 对循环依赖的支持有限。如果你的模块之间存在循环依赖,可能会导致加载错误。可以尝试使用 splitChunks 来拆分模块,或者重构代码来避免循环依赖。

  • 监控和调试: Module Federation 的配置比较复杂,容易出错。可以使用 Webpack 的 stats 功能来监控模块的加载情况,或者使用 Chrome DevTools 来调试。

表格总结:关键参数对比

参数 作用
name 模块的名字,必须唯一。
remotes 远程模块的配置,告诉 Webpack 去哪里找远程模块。
exposes 暴露的模块,远程模块需要配置 exposes 来暴露自己的模块,这样主应用才能加载它。
shared 共享依赖的配置,这是核心!
singleton 是否强制单例,true 表示强制单例,false 表示不强制单例。
requiredVersion 要求的版本范围,只有满足这个版本范围的依赖才能被共享。
strictVersion 严格模式,要求所有模块必须使用完全相同的版本。
eager 立即加载共享模块,而不是延迟加载。

结尾:Module Federation 的未来

Module Federation 是一个强大的工具,可以帮助我们构建可扩展、可维护的微前端应用。虽然配置比较复杂,但是只要掌握了 Shared Scopes 和 Singleton 这些核心概念,就能轻松应对各种挑战。

未来,Module Federation 还会继续发展,会涌现出更多的工具和最佳实践。让我们一起期待 Module Federation 的未来吧!

好了,今天的分享就到这里。感谢大家的观看,希望对大家有所帮助!有问题可以在评论区留言,我会尽力解答。再见!

发表回复

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