JS `Module Federation` (Webpack):实现真正意义上的运行时模块共享

各位观众老爷们,大家好!我是今天的主讲人,咱们今天聊点儿硬核的——Webpack 的 Module Federation,这玩意儿能让你实现真正意义上的运行时模块共享,说白了,就是让你的代码像乐高积木一样,随便拼,随便搭,妈妈再也不用担心我的代码重复了!

开场白:Module Federation 是个啥玩意儿?

在传统的 Webpack 打包方式中,如果你有多个应用,它们之间想要共享一些组件或者模块,通常的做法是:

  1. 发布到 npm 上: 把共享的代码打成包,发布到 npm,然后在每个应用中安装。这听起来很合理,但实际上维护起来很痛苦,每次更新都要重新发布,重新安装,简直烦死个人。
  2. 使用共享的组件库: 搞一个单独的组件库项目,然后各个应用引用。这比 npm 稍微好一点,但仍然需要构建、发布、更新,流程依然繁琐。
  3. 拷贝代码: 最原始的办法,直接把代码复制粘贴到各个项目里。这简直是噩梦,一旦需要修改,就要改好几个地方,稍不留神就出问题,代码一致性根本无法保证。

Module Federation 就像一个救星一样出现了,它允许你在运行时动态地从其他应用中加载模块,而不需要重新构建或者重新部署。这意味着你可以把你的应用拆分成更小的、可独立部署的模块,然后让它们在运行时相互协作。

Module Federation 的核心概念

要理解 Module Federation,你需要先了解几个核心概念:

  • Host (宿主): 宿主应用,它会加载和使用其他应用的模块。
  • Remote (远程): 提供可共享模块的应用。
  • Shared Modules (共享模块): 在 Host 和 Remote 之间共享的模块,例如 React、Vue、Lodash 等。

简单来说,Host 就像一个舞台,Remote 就像是演员,Shared Modules 就像是舞台上的道具。Host 负责搭建舞台,Remote 负责表演节目,Shared Modules 则是大家共用的道具。

Module Federation 的配置

Module Federation 的配置主要通过 Webpack 的 ModuleFederationPlugin 插件来实现。下面是一个简单的配置示例:

Host 应用 (App1) 的 webpack.config.js:

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

module.exports = {
  entry: './src/index.js',
  mode: 'development',
  devServer: {
    port: 3000,
  },
  output: {
    publicPath: 'http://localhost:3000/',
  },
  module: {
    rules: [
      {
        test: /.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react', '@babel/preset-env'],
          },
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'App1', // 必须唯一,作为模块的名称
      remotes: {
        App2: 'App2@http://localhost:3001/remoteEntry.js', // 远程应用及其入口文件
      },
      shared: {
        react: { singleton: true, requiredVersion: '>=16.0.0' },
        'react-dom': { singleton: true, requiredVersion: '>=16.0.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

Remote 应用 (App2) 的 webpack.config.js:

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

module.exports = {
  entry: './src/index.js',
  mode: 'development',
  devServer: {
    port: 3001,
  },
  output: {
    publicPath: 'http://localhost:3001/',
  },
  module: {
    rules: [
      {
        test: /.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react', '@babel/preset-env'],
          },
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'App2', // 必须唯一,作为模块的名称
      filename: 'remoteEntry.js', // 暴露的入口文件名称
      exposes: {
        './Button': './src/Button.jsx', // 暴露的模块及其路径
      },
      shared: {
        react: { singleton: true, requiredVersion: '>=16.0.0' },
        'react-dom': { singleton: true, requiredVersion: '>=16.0.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

配置详解:

配置项 描述
name 模块的名称,必须唯一。
remotes (Host) 指定需要加载的远程模块。键是模块的名称,值是远程模块的 URL 和入口文件 (通常是 remoteEntry.js)。
filename (Remote) 指定暴露的入口文件的名称。
exposes (Remote) 指定需要暴露的模块。键是模块的名称,值是模块的路径。
shared 指定需要在 Host 和 Remote 之间共享的模块。singleton: true 表示只使用一个实例,requiredVersion 指定需要的版本范围。如果 Host 和 Remote 使用了不同版本的共享模块,Webpack 会自动处理版本冲突,但这可能会导致一些问题,所以最好保持版本一致。
publicPath 非常重要!它告诉 Webpack 在运行时在哪里查找你的 chunk 文件。在开发环境中,通常设置为 http://localhost:<port>/。在生产环境中,你需要根据你的部署环境进行配置。 如果不设置或者设置错误,可能会导致模块加载失败,出现 "Loading chunk failed" 错误。

代码示例:

App2 (Remote) 的 src/Button.jsx:

import React from 'react';

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

export default Button;

App1 (Host) 的 src/index.js:

import React from 'react';
import ReactDOM from 'react-dom';

// 动态导入远程模块
import('App2/Button').then((ButtonModule) => {
  const Button = ButtonModule.default;

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

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

运行步骤:

  1. 分别启动 App1 (端口 3000) 和 App2 (端口 3001)。
  2. 打开 http://localhost:3000,你就可以看到 App1 的页面,上面有一个按钮,这个按钮实际上是来自 App2 的。

注意事项:

  • 异步加载: Module Federation 使用的是异步加载,所以你需要使用 import() 动态导入模块。
  • Shared Modules 的版本: 尽量保持 Host 和 Remote 之间 Shared Modules 的版本一致,避免版本冲突。
  • Public Path: publicPath 配置非常重要,它决定了 Webpack 在运行时在哪里查找你的 chunk 文件。
  • 错误处理: 在动态导入模块时,一定要进行错误处理,避免因为远程模块加载失败导致整个应用崩溃。

高级用法:

  • 多层嵌套: Module Federation 可以实现多层嵌套,例如 A 应用加载 B 应用的模块,B 应用又加载 C 应用的模块。
  • 动态 Remotes: 你可以根据环境或者配置动态地加载不同的 Remote 应用。
  • 自定义加载器: 你可以自定义加载器,实现更复杂的模块加载逻辑。
  • 版本控制: Module Federation 支持版本控制,你可以指定加载特定版本的 Remote 应用。
  • Typescript 支持: Module Federation 对 Typescript 的支持也很好,可以实现类型安全的模块共享。

Module Federation 的优势:

  • 代码共享: 实现真正意义上的运行时代码共享,避免代码重复。
  • 独立部署: 每个应用都可以独立部署,互不影响。
  • 弹性扩展: 可以根据需要动态地加载和卸载模块,实现弹性扩展。
  • 微前端架构: 非常适合构建微前端架构,将大型应用拆分成更小的、可独立维护的模块。
  • 降低耦合性: 降低应用之间的耦合性,使得各个应用可以独立开发和演进。

Module Federation 的劣势:

  • 配置复杂: Module Federation 的配置比较复杂,需要仔细理解各个配置项的含义。
  • 调试困难: 由于模块是动态加载的,调试起来可能会比较困难。
  • 性能损耗: 动态加载模块会带来一定的性能损耗,需要权衡利弊。
  • 版本管理: Shared Modules 的版本管理比较复杂,需要仔细规划。
  • 依赖管理: 需要仔细管理各个应用之间的依赖关系,避免循环依赖。

实战案例:

假设我们有一个电商网站,它由以下几个模块组成:

  • 商品列表模块: 展示商品列表。
  • 购物车模块: 管理购物车。
  • 用户中心模块: 管理用户个人信息。

我们可以使用 Module Federation 将这三个模块拆分成独立的 Remote 应用,然后让一个 Host 应用加载它们。这样,每个模块都可以独立开发和部署,互不影响。

代码示例 (简化版):

商品列表模块 (ProductList) 的 webpack.config.js:

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

module.exports = {
  entry: './src/index.js',
  mode: 'development',
  devServer: {
    port: 3002,
  },
  output: {
    publicPath: 'http://localhost:3002/',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'ProductList',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductList': './src/ProductList.jsx',
      },
      shared: {
        react: { singleton: true, requiredVersion: '>=16.0.0' },
        'react-dom': { singleton: true, requiredVersion: '>=16.0.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

购物车模块 (Cart) 的 webpack.config.js:

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

module.exports = {
  entry: './src/index.js',
  mode: 'development',
  devServer: {
    port: 3003,
  },
  output: {
    publicPath: 'http://localhost:3003/',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'Cart',
      filename: 'remoteEntry.js',
      exposes: {
        './Cart': './src/Cart.jsx',
      },
      shared: {
        react: { singleton: true, requiredVersion: '>=16.0.0' },
        'react-dom': { singleton: true, requiredVersion: '>=16.0.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

用户中心模块 (UserCenter) 的 webpack.config.js:

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

module.exports = {
  entry: './src/index.js',
  mode: 'development',
  devServer: {
    port: 3004,
  },
  output: {
    publicPath: 'http://localhost:3004/',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'UserCenter',
      filename: 'remoteEntry.js',
      exposes: {
        './UserCenter': './src/UserCenter.jsx',
      },
      shared: {
        react: { singleton: true, requiredVersion: '>=16.0.0' },
        'react-dom': { singleton: true, requiredVersion: '>=16.0.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

Host 应用 (MainApp) 的 webpack.config.js:

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

module.exports = {
  entry: './src/index.js',
  mode: 'development',
  devServer: {
    port: 3000,
  },
  output: {
    publicPath: 'http://localhost:3000/',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'MainApp',
      remotes: {
        ProductList: 'ProductList@http://localhost:3002/remoteEntry.js',
        Cart: 'Cart@http://localhost:3003/remoteEntry.js',
        UserCenter: 'UserCenter@http://localhost:3004/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '>=16.0.0' },
        'react-dom': { singleton: true, requiredVersion: '>=16.0.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

Host 应用 (MainApp) 的 src/index.js:

import React from 'react';
import ReactDOM from 'react-dom';

const App = () => {
  return (
    <div>
      <h1>Main App</h1>
      <React.Suspense fallback="Loading Product List...">
        <ProductList />
      </React.Suspense>
      <React.Suspense fallback="Loading Cart...">
        <Cart />
      </React.Suspense>
      <React.Suspense fallback="Loading User Center...">
        <UserCenter />
      </React.Suspense>
    </div>
  );
};

const ProductList = React.lazy(() => import('ProductList/ProductList'));
const Cart = React.lazy(() => import('Cart/Cart'));
const UserCenter = React.lazy(() => import('UserCenter/UserCenter'));

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

总结:

Module Federation 是一个非常强大的工具,它可以让你实现真正意义上的运行时模块共享,从而构建更灵活、更可维护的应用。虽然配置比较复杂,但只要你理解了它的核心概念,掌握了配置方法,就可以充分利用它的优势,提升你的开发效率,改善你的代码质量。

最后,希望今天的讲座对大家有所帮助,谢谢大家!记住,代码的世界,就是要大胆尝试,不断学习,才能成为真正的编程大师!

发表回复

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