WebpackModule Federation微前端前端工程化代码共享

一看就懂的 Module Federation(微前端进阶必读)

谢国度(心伦)··原文链接
收录于 2026/5/16 17:08:17

一文看懂 Module Federation

作者: 谢国度(心伦) | 发布时间: 2022-05-09 来源: 阿里巴巴前端技术


前言

一直在听说 Webpack5 的新特性 Module Federation(简称 MF)可以很好解决代码共享的问题,但实际在这两年并没有在团队中使用起来——现有项目都不是 Webpack5,小范围落地又有局限性,而团队在微前端探索中已经有了跨子应用代码共享的解决方案。

目前为了探索 MF 与微前端方案结合的可能性,决定深入了解其底层原理。


核心概念

什么是 Module Federation

Webpack 官网描述:

Multiple separate builds should form a single application. These separate builds should not have dependencies between each other, so they can be developed and deployed individually. This is often known as Micro-Frontends, but is not limited to that.

简译: 一个应用可以由多个独立的构建组成,这些独立构建之间没有依赖关系,可以独立开发、部署——这就是常说的微前端,但不仅限于此。

MF 和微前端解决的问题类似:把一个应用拆分成多个应用,每个可独立开发、独立部署,一个应用可以动态加载并运行另一个应用的代码,并实现应用间的依赖共享

MF 在设计上提出了三个核心概念:


1. Container

一个被 ModuleFederationPlugin 打包出来的模块被称为 Container

通俗讲:使用了 ModuleFederationPlugin 构建的应用就是一个 Container,它可以加载其他的 Container,也可以被其他的 Container 所加载。


2. Host & Remote

从消费者和生产者的角度看 Container,Container 又可被称作 HostRemote

角色含义
Host消费方,动态加载并运行其他 Container 的代码
Remote提供方,暴露属性(如组件、方法等)供 Host 使用

注意:Host 和 Remote 是相对的——一个 Container 既可以作为 Host,也可以作为 Remote。


3. Shared

一个 Container 可以 Shared 它的依赖(如 react、react-dom)给其他 Container 使用,即共享依赖


使用实践

效果演示

两个应用 app1app2app2 共享它的 Hello 组件给 app1 使用,它们共享一份 react 和 react-dom 依赖。

app1/src/app.js

import React from 'react';
import App2Hello from 'app2/Hello';

const RootComponent = () => {
  return (
    <div>
      <div>app1</div>
      <App2Hello />
    </div>
  );
};

export default RootComponent;

app1/src/bootstrap.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';

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

app1/src/index.js

import('./bootstrap');

app2/src/Hello.js

import React from 'react';

const Hello = () => {
  return (
    <div>app2 hello</div>
  )
};

export default Hello;

效果: app1 引用了 app2 的 Hello 组件,在渲染时异步下载了 app2 的远程模块入口代码和 Hello 组件代码,并且只下载了 app1 的 react 和 react-dom 代码,app2 直接使用 app1 提供的依赖——实现了应用动态加载并运行另一应用的代码,同时实现应用间依赖共享。


如何配置插件

实现跨应用代码共享,主要借助了 Webpack5 提供的 ModuleFederationPlugin

app2 作为提供方(Remote)的配置:

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

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app2',
      filename: 'app2RemoteEntry.js',
      exposes: {
        './Hello': './src/Hello',
      },
      shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
    }),
  ]
}

app1 作为消费方(Host)的配置:

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

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      filename: 'app1RemoteEntry.js',
      remotes: {
        'app2': 'app2@http://127.0.0.1:8002/app2RemoteEntry.js',
      },
      shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
    })
  ]
}

核心字段详解

name

当前应用的别名。当应用作为 Remote 给 Host 使用时,作为引用前缀:

import xx from 'name/expose'

filename

当前应用作为 Remote 时,提供的远程模块入口文件名,如 app2RemoteEntry.js

exposes

当前应用作为 Remote 时,可提供哪些属性(如组件、方法,甚至一个值)可消费:

exposes: {
  './Hello': './src/Hello',
}
  • key:在 Host 使用时的相对路径
  • value:当前应用暴露的属性的相对路径

同步引用:import App2Hello from 'app2/Hello'; 异步引用:const App2Hello = React.lazy(() => import('app2/App1Hello'));

remotes

当前应用作为 Host 时,需要消费哪些 Remote 应用:

remotes: {
  'app2': 'app2@http://127.0.0.1:8002/app2RemoteEntry.js',
}

使用 Remote 应用的格式为 import * from {name}{path}

注意:这里的 name 是引用别名,可以跟 Remote 应用定义的 name 不一致。

shared

当前应用无论是作为 Host 还是 Remote,可以共享的三方库依赖有哪些:

shared: { react: { singleton: true }, 'react-dom': { singleton: true } }
配置项含义
singleton: true开启单例模式,共享依赖只加载一次(优先取版本高的)
requiredVersion指定共享依赖的版本

版本冲突处理:

  • app1 的 react 版本为 16.13.0,app2 的 react 版本为 16.14.0
  • 双方配置 singleton: true → 共同使用 16.14.0(app2 提供)
  • app1 配置 requiredVersion: 16.13.0 → app1 用 16.13.0,app2 用 16.14.0,各自下载自己的

工作原理

构建上的差异

使用 MF 之后,打包文件中新增了四种 chunk:

Chunk 类型生成原因
remoteEntry-chunk配置了 ModuleFederationPlugin 后自动生成
shared-chunk开启 shared 功能后,共享依赖被分离出来
expose-chunk配置了 exposes 后,暴露的属性生成的独立 chunk
async-chunk手动代码分割产生的异步 chunk

为什么需要 bootstrap.js 异步加载

这是实现 MF 功能的关键限制。入口代码必须放到 bootstrap.jsindex.js 使用 import('./bootstrap') 异步加载。

原因:

如果 bootstrap.js 不是异步加载,而是直接打包在 main.js 里,那么:

import App2Hello from 'app2/Hello';

这行语句会立刻执行,此时 app2 的资源根本没有被下载,会报错。同样,import React from 'react' 同步执行也会报错,因为共享依赖还未初始化。

正确顺序:

  1. 先加载 main.js
  2. 异步加载 src_bootstrap_tsx.js
  3. 前置加载好远程应用的资源 + 初始化共享依赖
  4. 最后执行 bootstrap.js 模块

远程模块加载机制

当 app1 执行 import App2Hello from 'app2/Hello' 时,背后发生了什么?

完整流程:

  1. app1 加载 src_bootstrap_tsx 模块,发现它依赖 webpack/container/remote/app2/Hello
  2. 先去下载 webpack/container/reference/app2(即 app2RemoteEntry.js
  3. 返回 app2 全局变量
  4. 执行 app2.get('./Hello') 异步获取 Hello 组件
  5. 远程资源 + src_bootstrap_tsx 资源全部下载完成
  6. 最后执行 src_bootstrap_tsx 模块

核心代码在 __webpack_require__.f.remotes

__webpack_require__.f.remotes = (chunkId, promises) => {
  if (__webpack_require__.o(chunkMapping, chunkId)) {
    chunkMapping[chunkId].forEach((id) => {
      var data = idToExternalAndNameMapping[id];
      // ...
      handleFunction(__webpack_require__, data[2], 0, 0, onExternal, 1);
      // data[2] = 'webpack/container/reference/app2'
    });
  }
}

app2RemoteEntry.js 暴露了 getinit 方法:

var get = (module, getScope) => {
  __webpack_require__.o(moduleMap, module)
    ? moduleMap[module]()
    : Promise.reject(new Error('Module does not exist in container.'));
};

var init = (shareScope, initScope) => {
  __webpack_require__.S[name] = shareScope;  // 合并共享作用域
  return __webpack_require__.I(name, initScope);
};

依赖共享原理

关键:__webpack_require__.S — 共享作用域(sharedScope)

在 Host 和 Remote 应用之间建立一个可共享的 sharedScope,包含所有可共享的依赖。

初始化流程

  1. app1 调用 __webpack_require__.I 初始化 sharedScope
  2. app1 注册自己的共享依赖版本(react 16.14.0、react-dom 16.14.0)
  3. app2 也调用 __webpack_require__.I,注册自己的共享依赖
  4. app2 的 init 方法将 app1 的 __webpack_require__.S 作为自己的 __webpack_require__.S
  5. 两者共用同一个 sharedScope

版本选择规则

singleton 模式(取最高版本):

var getSingletonVersion = (scope, scopeName, key, requiredVersion) => {
  var version = findSingletonVersionKey(scope, key);  // 找最高版本
  if (!satisfy(requiredVersion, version)) {
    console.warn('版本不兼容警告');
  }
  return get(scope[key][version]);
};

requiredVersion 模式(精确匹配):

var loadStrictVersionCheckFallback = (scopeName, scope, key, version, fallback) => {
  var entry = findValidVersion(scope, key, version);  // 找精确版本
  return entry ? get(entry) : fallback();
};

最终数据结构

__webpack_require__.S = {
  default: {                    // sharedScope 名称
    react: {
      '16.14.0': { get: [Function], from: 'app1', eager: false },
      '16.13.0': { get: [Function], from: 'app2', eager: false },
    },
    'react-dom': {
      '16.14.0': { get: [Function], from: 'app1', eager: false }
    }
  }
}

应用场景

场景一:代码共享

传统方案对比:

方案问题
直接复制代码维护困难
发布到 NPM效率低,版本管理复杂
微前端异步加载子应用优雅但不成标准

MF 方案:

只需配置 exposes,使用方配置 remotes 即可引用:

// 同步引用
import ServiceInfo from 'optimus/ServiceInfo';

// 异步引用
const ServiceInfo = React.lazy(() => import('optimus/ServiceInfo'));

业务组件库新玩法

不需要 babel-plugin-import 就能实现按需加载+懒加载:

// webpack.config.js
new ModuleFederationPlugin({
  name: 'tracks',
  filename: 'tracksRemoteEntry.js',
  exposes: {
    './PageHeader': './src/components/PageHeader',
    './Address': './src/components/Address',
    './Empty': './src/components/Empty',
  },
  shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),

存在的两个问题

  1. 缺乏类型提示:即使 Remote 生成了类型文件,Host 引用时也获取不到
  2. 缺乏开发工具:没有官方工具支持多个应用同时启动、同时开发

场景二:公共依赖

传统方案对比:

方案弊端
webpack externals全量加载,依赖顺序需人工保证,多版本共存无法支持
微前端方案模块管理不成标准,无法与社区方案融合

MF 方案:

所有公共依赖均可作为一个应用,依赖关系交给 Webpack 处理:

// react16 应用
new ModuleFederationPlugin({
  name: 'react16',
  filename: 'react16RemoteEntry.js',
  exposes: {
    './index': './src/index',
  },
}),

使用方:

import React from 'react16/index';

存在的问题:

  1. 依赖别名问题:使用方需写 react16/index,体验不友好
  2. 性能问题:每个公共依赖一个 remoteEntry.js,启动时异步下载资源过多

折中方案: 建一个库应用存放所有公共依赖,合并为只有一个 remoteEntry.js(但无法解决多版本并存问题)。


优缺点总结

优点

  1. 框架无关:提供了一种拆分巨石应用的快速方式
  2. 代码共享:一个应用可以很方便共享模块给其他应用使用
  3. 依赖共享机制完善:支持多版本依赖共存
  4. 学习/改造成本低:基于 Webpack 生态,实施成本低

缺点

  1. 性能影响:资源需要各种异步加载,可能对页面性能造成负面影响
  2. 版本控制问题:远程应用的资源路径需显式配置,存在和 NPM 包管理一样的问题
  3. 缺乏类型提示:引用远程模块时没有类型提示,存在代码质量问题
  4. 缺乏官方开发工具:不支持多个应用一起启动、一起开发

思考

MF 极致地发挥了模块动态加载与依赖自动管理的优势,使得应用拆分和代码复用有了新思路。

与微前端的关系:互补而非替代。

  • MF 专注于应用间代码共享和依赖共享,从原生构建层面解决模块依赖关系
  • 微前端更专注于宏观角度:应用动态加载、生命周期管理、沙箱管理、版本管理、研发平台

当前 MF 仍处于相对不稳定、不完善的阶段,但值得长期关注。某些缺陷(如开发工具、多应用同时启动)的问题都是可以自行解决的——要不要在 MF 基础上投入解决,取决于投入产出比的权衡。

无论怎样,MF 绝对值得长期关注并投入时间去探索,它将与微前端很好地结合起来。