webpack5新特性:模块联邦简单入门

1,214 阅读9分钟

模块联邦

模块联邦(Module Federation)是一种现代前端开发技术,旨在解决前端微服务架构中的模块共享和代码复用问题。它允许不同的前端应用程序或模块在运行时动态地共享代码和资源,而不需要在构建时静态地捆绑这些代码。这个概念的核心思想是将前端应用程序拆分为更小的模块,然后在运行时根据需要加载这些模块,从而实现更好的代码复用和分布式开发。

  1. 模块共享:模块联邦允许不同的前端应用程序之间共享模块(例如 React 组件、工具函数等),这些模块可以在运行时动态加载。
  2. 独立构建:每个前端应用程序可以独立构建和部署,而不需要将所有代码打包到一个巨大的捆绑文件中。这有助于减小应用程序的构建大小。
  3. 动态加载:模块联邦使前端应用程序能够在运行时动态地加载其他应用程序的模块,以实现按需加载和延迟加载。
  4. 版本管理:模块联邦支持在不同应用程序之间共享不同版本的模块,这有助于解决依赖关系版本冲突的问题。
  5. 独立部署:每个前端应用程序可以独立部署,这使得微前端架构更加灵活和可维护。
  6. 减少重复:模块联邦可以减少重复的模块代码,提高了代码的复用性,减小了维护成本。
  7. 增强开发速度:开发团队可以并行开发不同的前端应用程序,而不会互相干扰,从而提高了开发速度。

概念介绍

分享端Remote:通过打包,暴露出远程模块入口文件的webpack构建端

消费端Host:引入使用远程模块文件的webpack构建端

一个应用可以是Host,也可以是Remote,也可以同时是Host和Remote.

关于runtime

当一个应用程序(比如 app1)消费另一个应用程序(比如 app2)的模块时,通常涉及构建时和运行时两个阶段:

  1. 构建时:在构建时,你会使用 Webpack 的 ModuleFederationPlugin 插件来配置应用程序之间的模块联邦关系。这个插件的配置是在构建时定义的,它告诉 Webpack 如何组织和打包应用程序之间的模块。这个阶段包括:

    • 配置 remotes:在 app1 的 Webpack 配置中,通过 remotes 配置来告诉 Webpack 如何远程引入 app2 的模块。这个配置是在构建时进行的。
    • 配置 exposes:在 app2 的 Webpack 配置中,通过 exposes 配置来指定要共享的模块。这个配置也是在构建时进行的。
  2. 运行时:在运行时,当 app1 运行时,它可以动态地加载和使用 app2 的模块。这是通过在代码中使用 Webpack 的动态模块加载机制来实现的,通常使用 import() 函数来进行动态加载。这个阶段包括:

    • 动态加载远程模块:在 app1 的代码中,通过使用 import() 函数来动态加载 app2 的模块。这个操作是在应用程序运行时进行的。
    • 处理远程模块的加载和错误:由于动态加载是异步操作,因此需要适当地处理模块加载和可能的错误。

总结来说,模块联邦的配置是在构建时进行的,但远程模块的加载是在运行时进行的。这使得不同的应用程序可以在运行时动态地共享和加载模块,实现了模块联邦的功能。

入门

分享端Remote

一个分享端应该有以下代码组成:

// in app2

// Button.js
import React from 'react';

const Button = () => <button>App 2 Button</button>;
export default Button;

// utils/index.js
export const add = (a, b) => a + b;
function subtract(a, b) {
  return a - b;
}
export default subtract;

// webpack.config.js 截取部分代码
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
    plugins: [
        new ModuleFederationPlugin({
        // name(字符串,必需):用于标识当前应用程序的名称。这个名称在联邦配置中唯一标识了你的应用。
          name: 'app2',
          // filename(字符串,可选):生成的远程入口文件的文件名。默认是 remoteEntry.js。
          filename: 'remoteEntry.js',
          // library(对象,可选):`type`(字符串,可选):定义用于导出和共享模块的库类型。可以是 `'var'`、`'module'`、`'assign'`、`'this'`、`'window'`、`'self'`、`'global'` 等。通常使用 `'var'`。
          library: { type: 'var', name: 'app2' },
          // exposes(对象,可选):用于指定哪些本地模块可以被远程模块引用。这个配置可以将本地模块暴露给其他远程模块使用。
          exposes: {
             // `./` 开头以表示当前模块的相对路径。这是因为模块联邦会将这些相对路径映射到远程模块的路径上。
            './Button': './src/Button',
            './Utils': '/src/utils/index',
          }
        })
      ]
 }

只要配置完以上配置,通过webpack5的打包就会在输出生成remoteEntry.js文件和其他暴露出去的一些异步模块,比如Button模块就是611.js image.png remoteEntry.js 就是一张网,将你的文件名和路径关联在一起,引用使用到的时候再去异步加载进行按需下载

消费端Host

一个分享端应该有以下代码组成:

// in app1

// webpack.config.js 截取部分代码
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
 plugins: [
    new ModuleFederationPlugin({
      // name(字符串,必需):用于标识当前应用程序的名称。这个名称在联邦配置中唯一标识了你的应用。
      name: 'app1',
      // remotes(对象,可选):用于配置要引入的远程模块。这个配置告诉当前应用程序从哪里加载远程模块。
      remotes: {
        app2: `app2@//localhost:3002/remoteEntry.js`,
      }
    })
  ]
}
 // APP.js
import React from 'react';
import subtract, { add } from 'app2/utils';

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

const App = () => {
  console.log(add(1,1)) // 2
  console.log(subtract(1,1)) // 0
  
  return (
    <div>
      <h1>Host Application - React Version {React.version}</h1>
      <h2>App 1</h2>
      <React.Suspense fallback="Loading Button">
        <RemoteButton />
      </React.Suspense>
    </div>
  );
};
export default App;

通过获取react版本API React.version 得知:app1和app2的react版本相互独立,相当于是一个页面使用了两个react,缺点有:

  1. 内存问题:如果同时加载多个版本的 React,可能会浪费内存,因为每个版本都会占用一定的内存空间。这可能会导致应用程序占用更多的内存,降低性能。
  2. 不一致的用户体验:如果应用程序的不同部分使用了不同版本的 React,用户可能会遇到不一致的用户体验,因为每个版本可能有不同的界面和行为。
  3. 性能问题:不同版本的 React 可能会具有不同的性能特性和优化。如果共享模块使用的是一个版本的 React,而消费模块使用的是另一个版本的 React,可能会导致性能瓶颈或性能下降。
  4. 版本冲突:不同版本的 React 可能具有不同的 API、行为和内部实现,这可能导致共享模块和消费模块之间存在不一致的行为。某些组件或功能可能在一个版本中可用,但在另一个版本中不可用,导致应用程序的不稳定性。

进阶

如果希望是统一两个项目的依赖版本的话也是可以做到的

分享端Remote

in app2
// webpack.config.js  截取部分代码
const { ModuleFederationPlugin } = require('webpack').container;

plugins: [
    new ModuleFederationPlugin({
      name: 'app2',
      library: { type: 'var', name: 'app2' },
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button'
      },
      shared: {
        'react-dom': {
          import: 'react-dom', // 从本地引入模块
          shareKey: 'newReactDom', // 共享的键
          // 默认作用域(default) :如果没有指定 `shareScope`,则共享模块会默认放置在一个名为 "default" 的作用域中。这意味着任何应用程序都可以访问这些共享模块,因为它们属于默认作用域。
          shareScope: 'default', 
          // 为true时,模块联邦将阻止应用程序自动获取共享模块的最新版本。它会强制应用程序使用第一个加载的版本,并忽略后续加载的版本。
          singleton: true, 
        },
        react: {
          import: 'react', // 从本地引入模块
          shareKey: 'newReact', // 共享的键
          // 默认作用域(default) :如果没有指定 `shareScope`,则共享模块会默认放置在一个名为 "default" 的作用域中。这意味着任何应用程序都可以访问这些共享模块,因为它们属于默认作用域。
          shareScope: 'default', 
          // 为true时,模块联邦将阻止应用程序自动获取共享模块的最新版本。它会强制应用程序使用第一个加载的版本,并忽略后续加载的版本。
          singleton: true, 
        }
      }
    })
  ]

消费端Host

in app1
// webpack.config.js  截取部分代码
const { ModuleFederationPlugin } = require('webpack').container;

  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      library: { type: 'var', name: 'app1' },
      remotes: {
        app2: 'app2',
      },
      shared: {
        'react-dom': {
          import: 'react-dom', // 从本地引入模块
          shareKey: 'newReactDom', // 共享的键如果和app1一致,则会放入到一个对象中
          // 默认作用域(default) :如果没有指定 `shareScope`,则共享模块会默认放置在一个名为 "default" 的作用域中。这意味着任何应用程序都可以访问这些共享模块,因为它们属于默认作用域。
          shareScope: 'default',// Scopes对象中的命名空间,可以通过__webpack_share_scopes__访问 
          // 为true时,模块联邦将阻止应用程序自动获取共享模块的最新版本。它会强制应用程序使用第一个加载的版本,并忽略后续加载的版本。
          singleton: true, 
        },
        react: {
          import: 'react', // 从本地引入模块
          shareKey: 'newReact', // 共享的键如果和app1一致,则会放入到一个对象中
          // 默认作用域(default) :如果没有指定 `shareScope`,则共享模块会默认放置在一个名为 "default" 的作用域中。这意味着任何应用程序都可以访问这些共享模块,因为它们属于默认作用域。
          shareScope: 'default', // Scopes对象中的命名空间,可以通过__webpack_share_scopes__访问
          // 为true时,模块联邦将阻止应用程序自动获取共享模块的最新版本。它会强制应用程序使用第一个加载的版本,并忽略后续加载的版本。
          singleton: true,
        }
      }
    })
    new HtmlWebpackPlugin({
      template: './public/index.html',
      app2RemoteEntry: '//localhost:3002/remoteEntry.js',  // 这也是一种方法
    })
  ]

如果分享端和消费端shareKey一致:

image.png 同一个Scope下,同一个key下有多个版本,消费端singleton为true时,默认会取版本最高的react版本

另外的Remote引用方式

在HtmlWebpackPlugin插件中: app2RemoteEntry 是一个自定义属性,用于指定远程入口文件的 URL 地址,以便在构建时将它嵌入到生成的 HTML 文件中,以供应用程序在运行时加载远程模块。

image.png

需要注意

react不同版本API也不一致,特别需要注意兼容老项目。