用模块联邦(Module Federation)优雅共享组件


Vue 3 微前端实战:用模块联邦(Module Federation)优雅共享组件 🚀

摘要:还在为跨项目共享组件而烦恼吗?每次组件更新都要发版 npm,再到各个项目里更新依赖,简直是"沟通两小时,发布一下午"。今天,我们来聊聊微前端的利器------模块联邦(Module Federation),让你丝滑地实现组件实时共享!

🤔 前言:我们遇到了什么问题?

在现代前端开发中,组件化思想已深入人心。但当我们的应用变得越来越庞大,甚至拆分成多个独立项目时,一个新的问题浮出水面:

  • 组件复用难 :一个设计精美的 Button 或一个功能复杂的 DataGrid 组件,如何在多个项目中复用?

  • 传统方式的痛点

    1. 复制粘贴:最原始的方式,但也是最糟糕的。一旦组件有 Bug 或需求变更,你需要去每个项目里都改一遍,简直是噩梦。
    2. 发布为 NPM 包:这是标准的解决方案。但它也带来了流程上的繁琐:修改组件 -> 打包 -> 发布 npm -> 其他项目更新依赖 -> 重新部署。一个小小的文案修改,可能都要走完这一整套流程,效率极低。

有没有一种方式,可以让 A 项目的组件,直接、实时地被 B 项目使用,就像使用内部组件一样简单?

答案是肯定的!模块联邦 (Module Federation) 就是为此而生的。

✨ 什么是模块联邦?

模块联邦是 Webpack 5 提出的一个革命性功能(现在 Vite 也有了强大的社区插件支持),它允许一个 JavaScript 应用在运行时动态地加载另一个应用的代码。

听起来有点抽象?我们把它具象化:

  • Remote (提供方) :一个独立的应用,它将自己的某些模块(比如组件、函数)"暴露"出去,供其他应用使用。
  • Host (消费方) :另一个独立的应用,它可以"引用"并渲染 Remote 应用暴露出来的模块。

这种方式的最大优势在于:Remote 应用的更新可以无需 Host 应用重新部署,只要 Remote 重新部署,Host 刷新页面就能获取到最新的组件!

话不多说,让我们撸起袖子,直接开干!

场景设定

为了方便演示,我们创建两个 Vue 3 项目:

  1. packages-center (Remote - 提供方)

    • 职责:存放和暴露公共组件。
    • 运行在:http://localhost:3001
    • 我们将从中导出一个名为 CoolButton.vue 的组件。
  2. main-app (Host - 消费方)

    • 职责:主应用,需要使用 packages-center 的组件。
    • 运行在:http://localhost:3000

本文将优先介绍目前更流行的 Vite 方案,之后会补充 Webpack 的配置方法。


🚀 实战演练:Vite 篇 (推荐)

Vite 凭借其极速的开发体验,在 Vue 3 社区中备受欢迎。我们可以使用 @originjs/vite-plugin-federation 插件来轻松实现模块联邦。

第一步:配置组件提供方 (packages-center)

1. 安装插件

Bash

bash 复制代码
# 进入 packages-center 项目目录
npm install @originjs/vite-plugin-federation --save-dev

2. 创建要共享的组件

src/components/ 目录下创建一个 CoolButton.vue

代码段

xml 复制代码
// src/components/CoolButton.vue
<template>
  <button class="cool-button">
    <slot>✨ 一个来自远方的酷按钮</slot>
  </button>
</template>

<script setup>
// 你的组件可以有复杂的逻辑
console.log('CoolButton 已加载!');
</script>

<style scoped>
.cool-button {
  padding: 12px 24px;
  border-radius: 8px;
  border: none;
  background: linear-gradient(45deg, #FF6B6B, #FFD93D);
  color: white;
  font-size: 16px;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.3s ease;
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.cool-button:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
}
</style>

3. 修改 vite.config.js

这是最核心的配置!我们需要告诉 Vite 要暴露哪些模块。

JavaScript

javascript 复制代码
// packages-center/vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import federation from '@originjs/vite-plugin-federation'

export default defineConfig({
  // 确保开发服务器端口是我们设定的 3001
  server: {
    port: 3001,
  },
  plugins: [
    vue(),
    federation({
      // 模块联邦的全局唯一名称
      name: 'packages-center', 
      // 导出的入口文件名
      filename: 'remoteEntry.js', 
      
      // exposes: 定义需要暴露的模块
      exposes: {
        // './CoolButton' 是外部访问的别名
        // './src/components/CoolButton.vue' 是组件的实际路径
        './CoolButton': './src/components/CoolButton.vue',
      },
      
      // shared: 定义需要与 Host 共享的依赖
      // 这可以避免重复加载,减小包体积,并防止因版本不一致导致的问题
      shared: ['vue'] 
    })
  ],
  build: {
    target: 'esnext' // 浏览器兼容性目标
  }
})

OK!启动 packages-center 项目 (npm run dev),我们的组件就已经成功"广播"出去了!

第二步:配置组件消费方 (main-app)

现在,我们来 main-app 项目里接收并使用这个按钮。

1. 安装插件 (同上)

Bash

bash 复制代码
# 进入 main-app 项目目录
npm install @originjs/vite-plugin-federation --save-dev

2. 修改 vite.config.js

JavaScript

php 复制代码
// 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({
  server: {
    port: 3000,
  },
  plugins: [
    vue(),
    federation({
      name: 'main-app',
      
      // remotes: 声明需要引用的远程模块
      remotes: {
        // 'packages_center' 是我们在本地使用的别名
        // 'packages-center@http://localhost:3001/assets/remoteEntry.js' 是远程模块的地址
        'packages_center': 'packages-center@http://localhost:3001/assets/remoteEntry.js',
      },
      
      // 共享的依赖,应该和 remote 方保持一致
      shared: ['vue']
    })
  ],
})

注意 :Vite Dev Server 启动时,remoteEntry.js 通常位于 /assets/ 目录下。

3. 在页面中使用远程组件

最佳实践是使用 Vue 的 defineAsyncComponent 进行异步加载,这样只有在需要时才会去下载远程组件的代码,并且可以优雅地处理加载状态。

修改你的 App.vue

代码段

xml 复制代码
// main-app/src/App.vue
<template>
  <div class="main-container">
    <h1>欢迎来到主应用 (Host)</h1>
    <p>下面这个超酷的按钮,就是从 `packages-center` 实时加载的!</p>
    
    <Suspense>
      <template #default>
        <RemoteCoolButton @click="handleRemoteClick" />
      </template>
      <template #fallback>
        <div>正在努力加载远程按钮...</div>
      </template>
    </Suspense>
  </div>
</template>

<script setup>
import { defineAsyncComponent } from 'vue';

// 异步加载远程组件
// 'packages_center' 对应 vite.config.js 中 remotes 的 key
// '/CoolButton' 对应 remote 应用 exposes 的 key
const RemoteCoolButton = defineAsyncComponent(() => import('packages_center/CoolButton'));

const handleRemoteClick = () => {
  alert('你点击了来自 `packages-center` 的按钮!');
}
</script>

<style>
.main-container {
  text-align: center;
  margin-top: 60px;
}
</style>

现在,同时启动 packages-centermain-app 两个项目 。访问 http://localhost:3000,你就能看到那个酷炫的按钮了!

不信?去 packages-center 项目里修改一下 CoolButton.vue 的文案或样式,保存后,回到 main-app 刷新页面------你会发现按钮已经神奇地同步更新了!是不是很酷?


📦 Webpack 篇

如果你仍在使用 Vue CLI (基于 Webpack),配置同样简单。

1. 提供方 (packages-center) 配置

vue.config.js 中配置 ModuleFederationPlugin

JavaScript

css 复制代码
// packages-center/vue.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  publicPath: 'auto',
  devServer: {
    port: 3001,
  },
  configureWebpack: {
    plugins: [
      new ModuleFederationPlugin({
        name: 'packages_center',
        filename: 'remoteEntry.js',
        exposes: {
          './CoolButton': './src/components/CoolButton.vue',
        },
        shared: {
          vue: {
            singleton: true, // 保证 vue 实例唯一
            requiredVersion: require('./package.json').dependencies.vue,
          },
        },
      }),
    ],
  },
};

2. 消费方 (main-app) 配置

JavaScript

css 复制代码
// main-app/vue.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  publicPath: 'auto',
  devServer: {
    port: 3000,
  },
  configureWebpack: {
    plugins: [
      new ModuleFederationPlugin({
        name: 'main_app',
        remotes: {
          'packages_center': 'packages_center@http://localhost:3001/remoteEntry.js',
        },
        shared: {
          vue: {
            singleton: true,
            requiredVersion: require('./package.json').dependencies.vue,
          },
        },
      }),
    ],
  },
};

组件的使用方式与 Vite 方案完全相同,依然是通过 defineAsyncComponent 异步加载。


📌 核心要点与避坑指南

  1. shared 是关键 :务必共享 vuevue-routerpinia 等核心库。否则,每个应用都会打包一份,不仅增大体积,还可能因为实例不唯一导致各种奇怪的 Bug。
  2. 异步加载与 Suspense :远程组件加载需要网络请求,使用 defineAsyncComponent<Suspense> 能极大提升用户体验,避免页面白屏。
  3. CSS 样式隔离 :在编写共享组件时,尽量使用 scoped CSS 来避免样式污染。如果需要共享全局样式,需要团队约定好加载顺序和命名规范。
  4. TypeScript 支持 :跨应用共享组件时,TS 类型定义会丢失。你需要为远程模块手动编写 .d.ts 声明文件,或者使用一些社区工具来自动生成。
  5. 版本控制 :虽然可以实时更新,但也带来了版本不一致的风险。重要更新建议做好通知,避免 breaking change 影响消费方应用。

总结

模块联邦为前端微服务化提供了一种极其优雅和高效的实现方案。通过本文的实战,相信你已经掌握了如何在 Vue 3 项目中利用它来共享组件。告别繁琐的 npm 发布流程,拥抱更加动态和灵活的架构吧!

这只是模块联邦的冰山一角,它还可以用于共享工具函数、路由、甚至整个页面。快在你的项目中尝试一下,体验开发的"加速度"!

相关推荐
光影少年5 天前
vite打包优化有哪些
前端·vite·掘金·金石计划
代码小学僧6 天前
Vite 项目最简单方法解决部署后 Failed to fetch dynamically imported Error问题
前端·vue.js·vite
萌萌哒草头将军9 天前
Oxc 和 Rolldown Q4 更新计划速览!🚀🚀🚀
javascript·vue.js·vite
xiaoyan20159 天前
Electron38-Winchat聊天系统|vite7+electron38+vue3电脑端聊天Exe
vue.js·electron·vite
月下点灯9 天前
✨项目上线后产品要求把应用字体改大点📏怎么办?一招教你快速解决🔧
前端·vite
李重楼9 天前
Vite 默认端口启动被拒绝解决办法
vite
xiaohe06019 天前
👋 一起写一个基于虚拟模块的密钥管理 Rollup 插件吧(三)
vite·rollup.js
streaker30312 天前
Vue3 开发态轻量组件文档方案:基于动态路由 + Markdown
vue.js·vite
Java陈序员12 天前
听歌体验直接拉满!推荐一款高颜值音乐播放器!
vue.js·docker·vite