彻底搞懂 Module Federation(上):概述与实战

4 阅读6分钟

彻底搞懂 Module Federation(上):概述与实战

本文是《彻底搞懂 Module Federation》系列的第1篇,介绍 Module Federation 的基本概念和实战示例。

📚 系列文章

  • 第1篇(本文):概述与实战示例
  • 📖 第2篇:原理分析 - Webpack 异步加载流程
  • 📖 第3篇:原理分析 - MF 模块加载(上)
  • 📖 第4篇:原理分析 - MF 模块加载(下)
  • 📖 第5篇:原理分析 - Runtime API + 项目实操

1. 概述

1.1 基本概念

MF是在webpack5出来后提出来的新概念,解决模块级别复用问题。简单分为两种应用,

  • 生产者(Provider),通过Module federation构建插件设置exposes暴露内部模块给其他应用使用;一个remote仓库。
  • 消费者(Consumer),同样通过插件设置remotes消费其他生产者的模块。消费者同样可以作为生产者。

image.png

1.2 接入方案

1.2.1 介绍

老版本的mf无法在webpack低版本等不支持module federation的构建插件中消费远程模块,而且导出模块和消费模块都是纯构建行为,加载过程被构建工具插件封装,只需要在代码中引入远程模块进行消费即可。

对原本项目的构建模式要求比较高。所以Module Federation2.0提出了Federation Runtime方法。提供高级Api在代码中动态引入消费远程模块,不受构建框架限制。具体的共享依赖复用、远程模块加载等行为全都封装到Runtime中。

目前Module Federation提供两种注册模块和加载模块的方式:

  • 一种是在构建插件中声明(一般是在 module-federation.config.ts 文件中声明)

  • 另一种方式是直接通过 runtime 的 api 进行模块注册和加载。

运行时注册模块插件中注册模块
可脱离构建插件使用,在 webpack4 等项目中可直接使用纯运行时进行模块注册和加载构建插件需要是 webpack5 或以上
支持动态注册模块不支持动态注册模块
不支持 import 语法加载模块支持 import 同步语法加载模块
支持 loadRemote 加载模块支持 loadRemote 加载模块
设置 shared 必须提供具体版本和实例信息设置 shared 只需要配置规则即可,无须提供具体版本及实例信息
shared 依赖只能供外部使用,无法使用外部 shared 依赖shared 依赖按照特定规则双向共享
可以通过 runtime 的 plugin 机制影响加载流程目前不支持提供 plugin 影响加载流程
不支持远程类型提示支持远程类型提示

1.2.2 构建时接入

在构建工具对应的配置项中,增加module-federation插件配置

1.2.2.1 消费者配置
1.2.2.1.1 webpack构建配置
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = (env = {}) => ({
    mode: 'development',
    cache: false,
  ...
    plugins: [
        new ModuleFederationPlugin({
          name: 'layout',
          filename: 'remoteEntry.js',
          remotes: {
            home: 'home@http://localhost:3002/remoteEntry.js',
          },
          exposes: {},
          shared: {
            vue: {
              singleton: true,
            },
          },
        }),

     ],
  })
1.2.2.1.2 页面引入
const Content = defineAsyncComponent(() => import('home/Content'));
const Button = defineAsyncComponent(() => import('home/Button'));
1.2.2.2 生产者webpack配置

这里生产者通过配置exposes导出了Content和Button两个组件。

1.2.2.2.1 webpack构建配置
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');

const { ModuleFederationPlugin } = require('webpack').container;
module.exports = (env = {}) => ({
  mode: 'development',
  cache: false,

  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
    }),
    new ModuleFederationPlugin({
      name: 'home',
      filename: 'remoteEntry.js',
      remotes: {
        home: 'home@http://localhost:3002/remoteEntry.js',
      },
      exposes: {
        './Content': './src/components/Content',
        './Button': './src/components/Button',
      },
      shared: {
        vue: {
          singleton: true,
        },
      },
    }),
  ],

});

1.2.3 Runtime接入

对于消费者提供了js api进行模块注册和模块加载,可以脱离构建插件使用,在 webpack4 等项目中可直接使用纯运行时进行模块注册和加载。生产者还是用对应的构建插件进行配置,需要单独打包出remoteEntry.js入口文件

1.2.3.1 核心API
1.2.3.1.1 Init
  • 创建运行时实例,它可以重复调用,但只存在一个实例。若想动态注册远程模块或插件,请使用 registerPluginsregisterRemotes
// 可以只使用运行时加载模块,而不依赖于构建插件
// 当不使用构建插件时,共享的依赖项不能自动设置细节
import { init, loadRemote } from '@module-federation/enhanced/runtime';

init({
    name: '@demo/app-main',
    remotes: [
        {
            name: "@demo/app1",
            // mf-manifest.json 是在 Module federation 新版构建工具中生成的文件类型,对比 remoteEntry 提供了更丰富的功能
            // 预加载功能依赖于使用 mf-manifest.json 文件类型
            entry: "http://localhost:3005/mf-manifest.json",
            alias: "app1"
        },
        {
            name: "@demo/app2",
            entry: "http://localhost:3006/remoteEntry.js",
            alias: "app2"
        },
    ],
});

// 使用别名加载
loadRemote<{add: (...args: Array<number>)=> number }>("app2/util").then((md)=>{
    md.add(1,2,3);
});
1.2.3.1.2 loadRemote
  • 用于加载初始化的远程模块,当与构建插件一起使用时,它可以通过原生的 import("remote name/expose")语法直接加载,并且构建插件会自动将其转换为loadRemote("remote name/expose")用法。
import { init, loadRemote } from '@module-federation/enhanced/runtime';

init({
  name: '@demo/main-app',
  remotes: [
    {
      name: '@demo/app2',
      alias: 'app2',
      entry: 'http://localhost:3006/remoteEntry.js',
    },
  ],
});


// remoteName + expose
loadRemote('@demo/app2/util').then((m) => m.add(1, 2, 3));

// alias + expose
loadRemote('app2/util').then((m) => m.add(1, 2, 3));
1.2.3.1.3 registerRemotes
  • 用于在初始化后注册远程模块.
function registerRemotes(remotes: Remote[], options?: { force?: boolean }) {}

type Remote = (RemoteWithEntry | RemoteWithVersion) & RemoteInfoCommon;

interface RemoteInfoCommon {
  alias?: string;
  shareScope?: string;
  type?: RemoteEntryType;
  entryGlobalName?: string;
}

interface RemoteWithEntry {
  name: string;
  entry: string;
}

interface RemoteWithVersion {
  name: string;
  version: string;
}
1.2.3.1.4 registerPlugins
  • 用于在初始化后注册远程插件.
import { registerPlugins } from '@module-federation/enhanced/runtime'
import runtimePlugin from 'custom-runtime-plugin.ts';

registerPlugins([runtimePlugin()]);

1.3 调试工具

1.3.1 安装Module Federation插件

chromewebstore.google.com/detail/modu…

image-1.png

1.3.2 插件提供了Devtools面板

image-2.png

1.3.3 查看远程依赖关系

image-3.png

2. 实战示例

介绍一下 简单的host-remote模式开发,及开发体验

官方发布的module-federation-examples,包含了很多不同构建框架之间结合使用的场景,

这里我们主要拿一个最简单的demo查看使用,依赖关系简单,方便对照分析后面的原理解析部分。

module-federation-examples/vue3-demo at master · module-federation/module-federation-examples

2.1 项目目录

项目可以拆分成两个文件夹

  • home: 生产者,暴露出来Content和Button组件给外部使用,同时配置了共享库vue,设置singleton: true
  • Layout: 消费者,也就是host,消费home项目暴露出来的Content和Button,也配置了vue共享库
vue3-demo/
├── home(remote)
│   ├── src
│   │   ├── App.vue         -- 入口组件 components: { Content: defineAsyncComponent(() => import('./components/Content')),
│   │   ├── components
│   │   │   ├── Button.js。  -- 业务组件Button
│   │   │   └── Content.vue  -- 业务组件Content
│   │   ├── index.js.      -- import('./main.js');
│   │   └── main.js        --  const app = createApp(App);app.mount('#app');
│   └── webpack.config.js   -- expose:{Content, Button}
│
├── layout(host)
│   ├── src
│   │   ├── Layout.vue     -- 使用业务组件Content,
│   │   ├── index.js       -- import('./main.js');
│   │   └── main.js        -- const Content = defineAsyncComponent(() => import('home/Content'));
│   └── webpack.config.js   -- remotes:{home: 'home@http://home.com/remoteEntry.js'}
└── package.json

2.2 核心代码块

2.2.1 Remote

2.2.1.1 src/index.js
// https://webpack.js.org/concepts/module-federation/#uncaught-error-shared-module-is-not-available-for-eager-consumption
import('./main.js');
2.2.1.2 src/main.js
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.mount('#app');
2.2.1.3 webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = (env = {}) => ({

  target: 'web',
  entry: path.resolve(__dirname, './src/index.js'),

  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
    }),
    new ModuleFederationPlugin({
      name: 'home',
      filename: 'remoteEntry.js',
      remotes: {
        home: 'home@http://localhost:3002/remoteEntry.js',
      },
      exposes: {
        './Content': './src/components/Content',
        './Button': './src/components/Button',
      },
      shared: {
        vue: {
          singleton: true,
        },
      },
    }),
  ],
   devServer: {
    port: 3002,
  },
});

2.2.2 Host

2.2.2.1 src/index.js
// https://webpack.js.org/concepts/module-federation/#uncaught-error-shared-module-is-not-available-for-eager-consumption
import('./main.js');
2.2.2.2 src/main.js
import { createApp, defineAsyncComponent } from 'vue';
import Layout from './Layout.vue';

const Content = defineAsyncComponent(() => import('home/Content'));
const Button = defineAsyncComponent(() => import('home/Button'));

const app = createApp(Layout);

app.component('content-element', Content);
app.component('button-element', Button);

app.mount('#app');
2.2.2.3 wepback.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = (env = {}) => ({
  entry: path.resolve(__dirname, './src/index.js'),


  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
    }),
    new ModuleFederationPlugin({
      name: 'layout',
      filename: 'remoteEntry.js',
      remotes: {
        home: 'home@http://localhost:3002/remoteEntry.js',
      },
      exposes: {},
      shared: {
        vue: {
          singleton: true,
        },
      },
    }),
  ],
  devServer: {
    port: 3001,
  },
});

2.3 示例截图

image-4.png


👉 下一篇彻底搞懂 Module Federation(中上):Webpack 异步加载流程