用模块联邦(Module Federation)优雅共享组件、工具函数、路由页面

509 阅读8分钟

Vue 3 微前端终极指南:从共享组件到路由、工具函数的全方位实战 🚀

摘要:还在为跨项目共享组件而烦恼吗?每次组件更新都要发版 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 刷新页面就能获取到最新的功能!

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

场景设定与目录结构

附 Gitee 仓库地址: Module Federation: vite+vue 远程模块联邦技术,组件共享 ,建议下载代码查看

  • packages-center (Remote - 提供方)

    • 职责:存放和暴露公共模块,包括 UI 组件、工具函数和完整的路由页面。
    • 运行在http://localhost:3001
  • main-app (Host - 消费方)

    • 职责:主应用,负责整体布局和路由,并消费 packages-center 提供的各种模块。
    • 运行在http://localhost:3000

项目目录结构概览:

清晰的目录结构有助于我们理解模块的组织方式。

packages-center (提供方) 目录结构:

Bash

packages-center/
├── src/
│   ├── components/
│   │   └── CoolButton.vue       # 共享的 UI 组件
│   ├── utils/
│   │   └── format.js            # 共享的工具函数
│   ├── view/
│   │   ├── About.vue            # 共享的关于页面
│   │   ├── Home.vue             # 共享的主页面
│   │   └── Share.vue            # 共享的分享页面
│   ├── router/
│   │   └── index.js             # 应用自身的路由定义
│   └── App.vue                  # 应用主入口
├── vite.config.js               # 核心联邦配置
└── package.json

main-app (消费方) 目录结构:

Bash

main-app/
├── src/
│   ├── view/
│   │   └── main.vue             # 应用自己的主页面,用于消费远程模块
│   ├── router/
│   │   └── index.js             # 核心路由配置,负责集成远程页面
│   └── App.vue                  # 应用主入口,包含 <router-view>
├── vite.config.js               # 核心联邦配置
└── package.json

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

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

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

1. 安装插件

Bash

# 进入 packages-center 项目目录 vite7.0以下版本
npm install @originjs/vite-plugin-federation --save-dev

#vite7.0以上版本
pnpm add @module-federation/vite -D

2. 准备好要共享的各类模块

根据上面的目录结构,确保 componentsutilsview 目录下已经创建好了相应的模块文件。

3. 修改 vite.config.js

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

JavaScript

// packages-center/vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import federation from '@originjs/vite-plugin-federation'
//版本不兼容 使用 @module-federation/vite
//import { federation } from '@module-federation/vite'
import path from 'path'

export default defineConfig({
  server: {
    port: 3001,
  },
  plugins: [
    vue(),
    federation({
      // 模块联邦的全局唯一名称
      name: 'packages-center', 
      // 导出的入口清单文件
      filename: 'remoteEntry.js', 
      
      // exposes: 定义需要暴露的模块
      exposes: {
        // 1. 暴露 UI 组件
        './CoolButton': './src/components/CoolButton.vue',

        // 2. 暴露工具函数
        './formatters': './src/utils/format.js',

        // 3. 暴露路由页面
        './ShareView': './src/view/Share.vue',
        './AboutView': './src/view/About.vue',
        './HomeView': './src/view/Home.vue'
      },
      
      // shared: 定义需要与 Host 共享的依赖
      shared: ['vue', 'vue-router'] 
    })
  ],
  // ... build 和 resolve 配置
})

OK!此时,packages-center 已经准备就绪,可以向外提供服务了。

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

现在,我们来 main-app 项目里接收并使用这些丰富多样的模块。

1. 安装插件 (同上)

Bash

# 进入 main-app 项目目录  vite7.0以下版本
npm install @originjs/vite-plugin-federation --save-dev

#vite7.0以上版本
pnpm add @module-federation/vite -D

2. 修改 vite.config.js

JavaScript

// main-app/vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import federation from '@originjs/vite-plugin-federation'
//版本不兼容 使用 @module-federation/vite
//import { federation } from '@module-federation/vite'
import path from 'path'

export default defineConfig({
  server: {
    port: 3000
  },
  plugins: [
    vue(),
    federation({
      name: 'main-app',
      // remotes: 声明需要引用的远程模块
      remotes: {
       // 'packages_center' 是我们在本地使用的别名
       // 'http://localhost:3001/dist/assets/remoteEntry.js' 是远程模块的地址
       packages_center: 'http://localhost:3001/dist/assets/remoteEntry.js'
      },
      // 共享的依赖,应该和 remote 方保持一致
      shared: ['vue', 'vue-router'], 
    })
  ],
  // ... resolve 配置
})

📌 注意:这里的 remoteEntry.js 地址指向了 dist/assets 目录。这意味着 packages-center 项目需要先执行 npm run build 进行打包,然后通过 npm run dev 或者 vite preview 这样的命令来提供 dist 目录的访问。

第三步:在 main-app 中消费远程模块

1. 基础篇:消费远程组件和工具函数

main-app/src/view/main.vue 页面中,我们同时使用了远程按钮和远程工具函数。

代码段

// main-app/src/view/main.vue
<template>
  <div class="section">
    <h2 class="section-title">远程组件</h2>
    <Suspense>
      <template #default>
        <RemoteCoolButton @click="handleRemoteClick" />
      </template>
      <template #fallback>
        <div>正在努力加载远程按钮...</div>
      </template>
    </Suspense>
  </div>
  <div class="section">
    <h2 class="section-title">共享工具函数</h2>
    <div>格式化后的金额:{{ money }}</div>
    <div>格式化后的日期:{{ date }}</div>
  </div>
</template>

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

// 1. 异步加载远程组件
const RemoteCoolButton = defineAsyncComponent(() => import('packages_center/CoolButton'));

// 2. 直接导入远程工具函数
import { formatCurrency, formatDate } from 'packages_center/formatters';

const money = formatCurrency(123456.78);
const date = formatDate(new Date());

const handleRemoteClick = () => {
  alert('你点击了来自 `packages-center` 的按钮!');
}
</script>
  • 消费组件:我们使用 defineAsyncComponent 异步加载,并用 <Suspense> 处理加载状态,这是加载远程UI的最佳方式。
  • 消费工具函数:对于非UI类的纯JS模块,我们可以直接 import 使用,就像使用本地模块一样简单!

2. 终极篇:消费并集成远程路由页面

这是模块联邦最强大的应用之一。主应用 main-app 可以将 packages-center 的整个页面无缝集成到自己的路由体系中。

main-app 的路由配置如下:

JavaScript

// main-app/src/router/index.js
import { createWebHistory, createRouter } from 'vue-router'
import { defineAsyncComponent } from 'vue'

const main = () => import('@/view/main.vue')

// 异步加载远程路由页面
const ShareView = defineAsyncComponent(() =>
  import('packages_center/ShareView')
);
const ShareAboutView = defineAsyncComponent(() =>
  import('packages_center/AboutView')
);
const ShareHomeView = defineAsyncComponent(() =>
  import('packages_center/HomeView')
);

const routes = [
  { path: '/', component: main },
  // 将主应用的路由路径,映射到远程应用的页面组件
  { path: '/shared', component: ShareView },
  { path: '/about', component: ShareAboutView },
  { path: '/share', component: ShareView },
  { path: '/home', component: ShareHomeView },
  
 
   
]

const router = createRouter({
  history: createWebHistory(),
  routes,
})

export default router

通过这种配置,当用户访问 main-app/shared/about/home 路径时,实际展示的是由 packages-center 提供的页面。更酷的是,在这些远程页面内部的路由跳转(例如在 Share.vue 中点击按钮执行 router.push('/about')),也会被 main-app 的路由系统接管,实现无缝的跨应用导航体验!

当然也可以通过 module 方式引入

 { path: '/home', component: () => import('packages_center/HomeView')},

那么 vite.config.js 需要改成

federation({
       name: env.VITE_APP_PROJECT_NAME,
       filename: 'remoteEntry.js',
       remotes: {
         socialSecurity: {
           type: 'module',
           name: 'packages_center',
           entry:  env.VITE_APP_APP_BASE_URL  + '/cephr-web-socialSecurity/remoteEntry.js',
         }
       },
       shared: ['vue', 'vue-router']
     })

env.VITE_APP_APP_BASE_URL 为域名。

第四步:运行与验证

  1. 打包并预览 packages-center

    Bash

    # 进入 packages-center 目录
    npm run build
    npm run dev # 或者 vite preview,确保 dist 目录能被访问
    
  2. 启动 main-app

    Bash

    # 进入 main-app 目录
    npm run dev
    

现在,访问 http://localhost:3000,你就能看到一个功能丰富的页面,它聚合了来自 packages-center 的组件、工具函数和可导航的页面,而这一切对于用户来说是完全无感的!


📌 核心要点与避坑指南

  • shared 是关键:务必共享 vuevue-routerpinia 等核心库。否则,每个应用都会打包一份,不仅增大体积,还可能因为实例不唯一导致各种奇怪的 Bug(例如 router.push 失效)。
  • 异步加载与 Suspense:远程组件加载需要网络请求,使用 defineAsyncComponent<Suspense> 能极大提升用户体验,避免页面白屏。
  • 远程模块地址:注意 remotesremoteEntry.js 的地址。开发环境和生产环境可能不同。生产环境通常指向CDN地址。
  • CSS 样式隔离:在编写共享组件时,尽量使用 scoped CSS 来避免样式污染。
  • 版本控制:虽然可以实时更新,但也带来了版本不一致的风险。重要更新建议做好通知,避免 breaking change 影响消费方应用。
  • 如果报错 str is not iterable 可能是由于 vite 版本不兼容, vite@7.*.*以上的版本请使用官方维护的 pnpm add @module-federation/vite -D www.npmjs.com/package/@mo…

总结

模块联邦为前端微服务化提供了一种极其优雅和高效的实现方案。通过本文的实战,相信你已经掌握了如何在 Vue 3 项目中利用它来共享组件工具函数路由页面

告别繁琐的 npm 发布流程,拥抱更加动态和灵活的架构吧!这不仅仅是组件复用,更是应用功能的自由组合与无缝集成。