模块联邦:更快的微前端方式!

什么是模块联邦

在前端项目中,不同团队之间的业务模块可能有耦合,比如 A 团队的页面里有一个富文本模块(组件),而 B 团队 的页面恰好也需要使用这个富文本模块。

传统模式下,B 团队只能去抄 A 团队的代码,把这个组件放到自己的项目了。

image.png

为了解决不同服务之间组件共享的问题,Webpack 的模块联邦功能应用而生。借助模块联邦,可以在 B 服务运行时,动态加载 A 服务「暴露的模块」。

Vite 通过  vite-plugin-federation 插件也可以实现

模块联邦的核心优势

结合模块联邦的原理与功能,它具有以下显著优势:

  • 「独立部署」:各个应用可独立开发、构建和部署,互不依赖。
  • 「运行时共享」:模块在运行时动态加载,无需在构建阶段打包进主应用。
  • 「版本控制」:支持为共享模块指定版本,便于多项目之间的依赖管理。
  • 「减少重复代码」:多个应用可共享功能模块,避免重复实现。

相比 qiankun 等微前端方案,模块联邦在灵活性上更具优势。qiankun 侧重于加载整个子应用,而模块联邦支持按需加载特定模块,粒度更细,使用更灵活。

如何实现

以 Vite+Vue 为例。

假设我们有 Vite 应用:

  • ui-library:组件提供者,暴露一个按钮组件  SharedButton.vue
  • main-app:主应用,运行时动态加载  ui-library  中的按钮组件来使用。
shixiaoshi-demo/
│
├── ui-library/     # 远程模块:暴露组件
│   ├── src/SharedButton.vue
│   ├── vite.config.js
│
└── main-app/       # 主项目:加载远程组件
    ├── src/App.vue
    ├── vite.config.js

1.安装模块联邦插件

我们需要使用  vite-plugin-federation 插件。

npm install vite-plugin-federation --save-dev

2.暴露组件

  • ui-library/src/SharedButton.vue
<template>
  <button class="shared-btn">我是共享按钮</button>
</template>

<script setup></script>

<style scoped>
  .shared-btn {
    padding: 10px;
    background-color: teal;
    color: white;
    border: none;
    border-radius: 5px;
  }
</style>

ui-library/vite.config.js

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import federation from "@originjs/vite-plugin-federation";

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: "ui_library",
      filename: "remoteEntry.js",
      exposes: {
        "./SharedButton": "./src/SharedButton.vue",
      },
      shared: ["vue"],
    }),
  ],
  build: {
    target: "esnext",
    minify: false,
    cssCodeSplit: false,
  },
});

「启动服务」

npm run dev -- --port=5001

暴露地址:http://localhost:5001/assets/remoteEntry.js

3.加载远程组件

  • main-app/vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import federation from "@originjs/vite-plugin-federation";

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: "main_app",
      remotes: {
        ui_library: "http://localhost:5001/assets/remoteEntry.js",
      },
      shared: ["vue"],
    }),
  ],
});
  • main-app/src/App.vue
<template>
  <div>
    <h1>主应用</h1>
    <RemoteButton />
  </div>
</template>

<script setup>
  import { defineAsyncComponent } from "vue";
  // 从 ui-library 中动态加载共享组件
  const RemoteButton = defineAsyncComponent(() =>
    import("ui_library/SharedButton")
  );
</script>
  • 启动主应用
npm run dev

访问  http://localhost:5173,你会看到来自  ui-library  的按钮组件成功渲染!

主应用与子组件关系图

main-app (主应用)
   |
   | -- 在运行时访问 -->
   |    http://localhost:5001/assets/remoteEntry.js
   |
   | -- 加载 -->
   |    ui-library 中暴露的 SharedButton.vue
   |
   | -- 使用 -->
   |    作为主应用的本地组件一样渲染

缺陷

在实际使用中,模块联邦也存在一些问题。

以我们项目开发中遇到的问题为例,当应用 A 加载应用 B 中的某个组件时,B 的代码执行环境会落在 A 的作用域下。这会带来一些潜在影响:

  • 「依赖冲突问题」:如果 B 服务依赖特定版本的 Vue,而 A 服务的 Vue 版本不同,实际运行时组件会使用 A 的 Vue 实例,可能导致行为异常,特别是在响应式系统、生命周期等细节方面。
  • 「状态失效问题」:如果 B 中的组件依赖 Vuex 或 Pinia 等状态管理工具,运行时由于上下文切换,可能无法正确获取或响应状态,导致 store 在远程加载后失效。
  • 「构建配置差异问题」:例如,B 使用了自动引入插件(如 unplugin-auto-import),可以在代码中直接使用  refcomputed  等 Vue API 而无需显式导入;但若 A 没有启用相同插件,加载 B 的组件时会因缺少这些 API 导致运行错误,除非显式引入相关函数。

因此,在使用模块联邦时,建议主应用与远程模块在框架版本、构建工具、插件配置等方面尽可能保持一致,或通过适配包装进行隔离,以减少因上下文差异带来的问题。