• Vite模块联邦(vite-plugin-federation)实现去中心化微前端后台管理系统架构
  • 发布于 3天前
  • 37 热度
    0 评论
  • 阳光
  • 1 粉丝 39 篇博客
  •   
一、项目概述
1.1 前言

在现代大型前端项目开发中,多团队协作时往往面临代码隔离与集成的挑战。为了解决这一问题,我们需要一种能够让各个微前端模块既能独立开发部署,又能作为完整系统一部分的解决方案。基于 Vite 的模块联邦插件@originjs/vite-plugin-federation提供了一种去中心化的微前端架构实现方式,实现了组件、路由的跨应用共享和动态加载。本文将结合实际项目经验,详细介绍如何利用模块联邦技术在 「vue3 生态」中构建去中心化的微前端架构。


1.2 依赖版本要求
node: 18.20.5
{
  "vite": "^6.0.2",
  "@originjs/vite-plugin-federation": "1.4.1",
}
二、架构设计
2.1 模块联邦基础概念
2.1.1 什么是模块联邦
@originjs/vite-plugin-federation是 Vite 生态中实现模块联邦功能的插件,它允许多个独立应用在运行时共享模块和代码,无需重复构建或打包依赖。这一特性在去中心化微前端架构中尤为关键。
2.1.2 基本配置示例
一个微前端模块的典型配置如下:
federation({
  name: "pageAModule",        // 微前端模块名称
  filename: "pageAEntry.js",  // 入口文件名
  exposes: {                  // 暴露的模块,此处为路由
    './routes': './src/routes/index.ts'
  },
  remote: {
    menuModule: ModuleUrl, // 引入的远程模块
  }
  shared: ['vue', 'vue-router', 'pinia']  // 共享依赖
})
// 使用远程模块
import 'menuModule/xxx'
2.2 去中心化微前端架构设计
2.2.1 传统微前端 vs 去中心化微前端

传统微前端架构通常采用 "基座模式",即一个主应用作为基座控制其他子应用的加载和渲染。而去中心化微前端架构没有明确的主应用,各个子应用可以独立部署、独立运行,同时又能无缝协作。


去中心化微前端架构的主要特点:
1.「无中心基座」:没有固定的主应用控制全局状态
2.「平等协作」:每个应用都可作为入口,并动态加载其他应用模块
3.「路由共享机制」:所有微前端模块共享路由表,实现无缝导航
4.「共享运行时」:应用间共享关键依赖和状态

2.3 特殊架构设计:menuModule 的角色
虽然我们采用去中心化的微前端架构,但在后台管理系统中,我们仍然需要一个统一的菜单管理模块(menuModule)。来提供系统的 layout 和管理一些公共文件。
2.3.1 为什么需要 menuModule?
「统一的菜单管理」:后台管理系统需要一个统一的菜单体系,确保用户体验的一致性
「权限控制中心」:集中管理各个页面模块的访问权限
「导航状态维护」:统一处理菜单的激活状态、展开状态等
「系统级功能集成」:包含用户信息、全局设置等系统级功能
2.3.2 menuModule 的特殊定位
menuModule 模块需要在所有的页面项目中引入,且所有项目在 vite 的 federation 中都只需引入这一个 menuModule 模块就行。 其他剩余的模块引入方式,在下文有提到。
menuModule 承担以下职责:
提供统一的布局容器(Layout)
管理全局导航菜单
处理用户认证和权限

提供公共组件和工具函数


三、技术实现
3.1 模块联邦配置详解
在实际项目中,每个页面的微前端模块的联邦配置通常包含以下部分:
federation({
  name: config.projectPrefix + 'Module',      // 微前端模块名称
  filename: config.projectPrefix + 'Entry.js', // 入口文件名
  exposes: {
    './routes': './src/routes/index.ts',      // 暴露路由配置(关键)
  },
  remotes: {
     // 堆代码 duidaima.com
    // 后台管理系统中需要单独引入菜单模块
    menuModule: ModuleUrl,
  },
  shared: ['vue', 'vue-router', 'pinia', 'dayjs', 'axios', 'sass', 'element-plus'], // 共享依赖
})
3.1.1 关键配置参数解析
「name」:微前端模块的唯一标识符,其他模块通过此名称引用
「filename」:构建后的入口文件名,通常以 Entry.js 结尾
「exposes」:暴露的模块,主要是路由配置,这是实现去中心化的关键
「remotes」:需要引用的其他微前端模块,指定模块的 URL 或加载方式
「shared」:所有微前端模块间共享的依赖,确保单一实例,避免重复加载
3.2 构建时处理
在构建微前端模块时,vite-plugin-federation会进行以下处理:
.识别exposes配置中声明的路由模块
.为每个模块生成独立的构建产物
.创建容器模块,管理模块的导出和依赖关系

.生成远程入口文件,处理模块的加载和解析


四、动态模块的导入与路由整合
4.1 传统导入方式的局限
传统的模块联邦使用方式是预先声明远程模块并直接导入:
// 常规的模块联邦导入方法
import Component from 'remoteModule/Component'
这种方式存在一个重要限制:「无法使用动态字符串拼接模块名」。例如,以下代码在模块联邦中不起作用:
// 这种写法在模块联邦中不支持
const moduleName = 'remoteModule'
import Component from `${moduleName}/Component` // 不支持!
这样就会导致一个问题就是 在去中心化微前端架构中,每个项目模块在开发时并不知道全局系统中到底有多少个联邦模块,也无法预先确定所有模块的名称和地址。为了支持新增模块的灵活扩展,需要一个动态的机制来发现和加载模块。通常,我们会通过一个配置文件(如 registry.json)来集中管理所有联邦模块的注册信息,允许新模块随时加入系统。然而,官方的模块联邦导入方式,它不支持动态拼接模块名称的字符串。

为了解决这一问题,我实现了一个支持动态拼接模块名称的加载函数 getRemoteEntries。通过该函数,我们可以在运行时根据配置文件动态获取模块的 URL 并加载模块,从而实现真正的动态模块发现和集成。这种方式不仅解决了官方导入方式的限制,还为系统的扩展性和灵活性提供了强有力的支持。

4.2 动态模块实现
为解决上述限制,我实现了getRemoteEntries函数,通过@originjs/vite-plugin-federation的getAPI 实现,运行时动态的模块声明和加载:
// src/utils/federation.ts
export const getRemoteEntries = async(name ? :string, moduleName ? :string) : Promise < any[] > =>{
	try {
		// 从注册中心获取所有可用模块的信息   
		const response = await axios.get(`$ {
			baseUrl
		}
		/federation/registry.json`)
		// 定义过滤条件   
		const filterByName = (distDir: any) = >{
			// 过滤掉与当前项目前缀相同的模块     
			if (distDir.name.toLowerCase().includes(config.projectPrefix.toLowerCase())) {
				return false
			}
			// 如果提供了name参数,则按name过滤    
			return ! name || distDir.name.includes(name)
		}
		// 过滤模块   
		const filteredModules = response.data.filter(filterByName)
		// 动态加载匹配的模块 
		const loadedComponents = []
		for (const moduleInfo of filteredModules) {
			try {
				// 动态构建模块URL      
				const moduleUrl = `$ {
					baseUrl
				}
				/${moduleInfo.path}/$ {
					moduleInfo.name.replace('Module', 'Entry')
				}.js`
				// 使用模块联邦API动态加载模块    
				const remote = await import(
				/* @vite-ignore */
				moduleUrl) const moduleFactory = await remote.get('./' + moduleName) const module = await moduleFactory() loadedComponents.push(module)
			} catch(error) {
				console.error(`加载模块$ {
					moduleInfo.name
				}失败: `, error)
			}
		}
		return loadedComponents
	} catch(error) {
		console.error('获取远程模块失败:', error) return []
	}
}
这种动态模块发现机制的优势在于:
「运行时集成」:应用可以在运行时发现并加载其他模块
「可扩展性」:新模块可以随时添加到系统中,无需修改全部现有模块
「自动注册」:新增的微前端模块自动成为整体系统的一部分
「动态模块名」:支持通过字符串拼接方式构建模块 URL
4.3 路由表的共享与整合
共享路由表是去中心化架构的核心,使每个模块都能作为独立入口。在我们的实现中,路由整合分两步进行:
「路由收集」:收集所有微前端模块的路由配置
「路由合并」:将所有路由合并到主布局路由下
// src/routes/index.ts
export const createRouterInstance = async() = >{
        // 堆代码 duidaima.com
	// 存储布局路由 
	let layoutRoute: RouteRecordRaw | undefined
	if (ENV_TYPE === 'federation') {
		// 获取远程入口配置   
		const remoteRoutes = await getRemoteEntries('', 'routes')
		// 找到Layout路由并合并
		layoutRoute = mergeLayoutChildren(remoteRoutes || [])
	}
	// 创建路由实例 
	const router = createRouter({
		history: createWebHashHistory(),
		routes: ENV_TYPE === 'federation' ? (layoutRoute as any).
	default:
		baseRoutes,
	})
	// 全局前置守卫 
	router.beforeEach(async(to, from, next) = >{
		// 路由守卫逻辑...   
		next()
	}) return router
}
4.4 路由合并核心逻辑
路由合并的核心实现在mergeLayoutChildren函数中,它实现了以下功能:
「识别布局路由」:找到包含主布局的路由模块
「处理独立页面」:将需要独立展示的页面 (outLayout) 提取出来
「嵌套路由合并」:将所有子模块的路由合并到主布局的 children 中
「顶层路由保留」:保留顶层路由如登录页、404 页面等
// 合并路由
const mergeLayoutChildren = (data: RouteRecordRaw[]) = >{
	let layoutIndex = -1
	// 添加变量记录Layout的索引 
	const outLayoutItems: {
		moduleIndex: number;
		route: any
	} [] = []
	// 存储所有outLayoutItem及其位置  
	// 首先处理 baseRoutes 中的 outLayout 路由 
	const baseOutLayoutRoutes = baseRoutes.filter((route) = >route.meta ? .outLayout) baseOutLayoutRoutes.forEach((route) = >{
		outLayoutItems.push({
			moduleIndex: -1,
			route
		})
	})
	// 遍历远程数据处理布局路由和独立路由
	data.forEach((item, index) = >{
		const defaultRoutes = (item as any).
	default const layoutItem = defaultRoutes.find((route: any) = >route.name === 'Layout') const outLayoutItem = defaultRoutes.find((route: any) = >route.meta ? .outLayout) if (layoutItem) {
			layoutIndex = index
			// 记录Layout所在的索引   
		}
		if (outLayoutItem) {
			outLayoutItems.push({
				moduleIndex: index,
				route: outLayoutItem
			})
			// 从原数组中移除outLayoutItem避免重复    
			const outLayoutIndex = defaultRoutes.findIndex((route: any) = >route.meta ? .outLayout) if (outLayoutIndex > -1) {
				defaultRoutes.splice(outLayoutIndex, 1)
			}
		}
	})
	// 获取Layout路由作为合并基础 
	const layoutRoute = (data[layoutIndex] as any).
default.find((route:
	any) = >route.name === 'Layout') if (layoutRoute) {
		// 将所有outLayoutItem添加到data[layoutIndex].default中作为顶层路由   
		outLayoutItems.forEach((item) = >{; (data[layoutIndex] as any).
		default.push(item.route)
		}) if (layoutRoute.children) {
			// 将主应用中非outLayout的路由添加到Layout的children中     
			const nonOutLayoutBaseRoutes = baseRoutes.filter((route) = >!route.meta ? .outLayout) layoutRoute.children.push(...nonOutLayoutBaseRoutes)
			// 遍历所有其他模块的路由添加到Layout的children中   
			data.forEach((item, index) = >{
				if (index !== layoutIndex) {
					const routes = (item as any).
				default
					if (Array.isArray(routes)) {
						layoutRoute.children.push(...routes)
					}
				}
			})
		}
	}
	return data[layoutIndex]
}
五、状态管理与模块通信
在去中心化微前端架构中,各模块间的状态管理和通信是一个关键问题。本项目采用了基于 pinia 持久化缓存的状态共享机制,主要通过src/stores/modules/cache.ts实现:
5.1 基于缓存的状态管理
我们使用了专门的缓存存储模块来管理跨模块的状态:
// stores/modules/cache.ts
import {
	defineStore
}
from 'pinia'import {
	ref
}
from 'vue'import config from '@/config'interface CacheData {
	value: unknown expire ? :number
}
export const useCacheStore = defineStore('cache', () = >{
	// 状态定义 
	const prefix = ref < string > (config.cachePrefix) const cacheData = ref < Record < string,
	CacheData >> ({})
	// 获取当前时间戳  
	const getTime = () : number = >{
		return Math.round(new Date().getTime() / 1000)
	}
	// 获取完整的键名   
	const getKey = (key: string, customPrefix ? :string) : string = >{
		return (customPrefix ? ?prefix.value) + key
	}
	// 设置缓存数据  
	const set = (key: string, value: unknown, expire ? :number, customPrefix ? :string) = >{
		const fullKey = getKey(key, customPrefix) cacheData.value[fullKey] = {
			value,
			expire: expire ? getTime() + expire: undefined,
		}
	}
	// 获取缓存数据  
	const get = (key: string, customPrefix ? :string) = >{
		const fullKey = getKey(key, customPrefix) const data = cacheData.value[fullKey]
		if (!data) return null
		if (data.expire && data.expire < getTime()) {
			remove(key, customPrefix) return null
		}
		return data.value
	}
	// 删除缓存数据   
	const remove = (key: string, customPrefix ? :string) = >{
		const fullKey = getKey(key, customPrefix) delete cacheData.value[fullKey]
	}
	return {
		prefix,
		cacheData,
		set,
		get,
		remove,
		getKey,
		getTime,
	}
},
{
	persist: {
		storage: localStorage,
	},
})
5.2 缓存模块的核心特性
缓存存储模块具有以下特点:
「命名空间隔离」:通过前缀机制,确保不同模块间缓存键名不冲突
「过期时间控制」:支持设置数据过期时间,自动清理失效数据
「本地持久化」:使用 Pinia 的持久化功能,确保页面刷新后状态不丢失
「跨模块共享」:所有微前端模块都可以访问同一缓存存储,实现数据共享
5.3 模块间通信方式
基于缓存存储的通信机制主要有以下几种模式:
「直接状态共享」:模块间通过相同的缓存键读写数据
// 页面A中设置数据
const cacheStore = useCacheStore()
cacheStore.set('sharedData', { count: 1 })
// 页面B中读取数据
const cacheStore = useCacheStore()
const data = cacheStore.get('sharedData')
「基于过期时间的临时共享」:对于临时性数据,设置合适的过期时间
// 设置1小时过期的共享数据
cacheStore.set('temporaryData', value, 3600)
「跨应用的全局状态」:通过特定前缀获取不同微前端的状态
// 获取user微前端模块的状态
cacheStore.set('userInfo', settings, undefined, 'user')
// 读取user微前端模块的状态
const settings = cacheStore.get('userInfo', 'user')
六、项目结构与配置
完整的 vite.config.ts 示例
// vite.config.ts
import {
	defineConfig
}
from 'vite'import vue from '@vitejs/plugin-vue'import {
	federation
}
from '@originjs/vite-plugin-federation'import path from 'path'import {
	config
}
from './config'const baseUrl = process.env.VITE_APP_BASE_URL || ''
// 需要暴露的模块
const exposes: Record < string,
string > ={
	// 暴露路由表,供其他应用使用 
	'./routes': './src/routes/index.ts',
}
export
default defineConfig({
		// 基础配置 
		base:
		baseUrl || './',
		// 通过构建脚本传入实际静态文件地址,所有联邦模块文件都会遵循这个地址  
		// 插件配置  
		plugins: [vue(),
		// 模块联邦配置  
		federation({
			name: config.projectPrefix + 'Module',
			// 微前端模块名称   
			filename: config.projectPrefix + 'Entry.js',
			// 入口文件名      
			exposes,
			// 暴露的模块   
			remotes: {
				// 只需单独引入menuModule,其他的模块走federation    
				menuModule: ModuleUrl,
			},
			// 共享依赖配置     
			shared: ['vue', 'vue-router', 'pinia', 'dayjs', 'axios', 'sass', ],
		})],
		// 解析配置 
		resolve: {
			alias: {
				'@': path.resolve(__dirname, 'src')
			}
		},
	})
部署与注册机制
去中心化微前端架构采用集中式的模块注册机制,通过registry.json文件管理所有可用模块:
CDN/静态资源服务器
├── federation/
│   └── registry.json (模块注册信息)
│
├── front-pc-page/ (页面模块)
│   └── pageEntry.js (入口文件)
│
├── front-pc-menu/ (菜单模块)
    └── menuEntry.js (菜单模块入口)
registry.json 示例:
[
  {
    "name": "pageModule",
    "path": "front-pc-page"
  },
  {
    "name": "menuModule",
    "path": "front-pc-menu"
  }
]
七、总结
@originjs/vite-plugin-federation为实现去中心化微前端架构提供了强大的技术基础。通过共享路由表机制,我们可以构建出真正去中心化、高度灵活且可扩展的微前端系统,使得不同团队可以独立开发、测试和部署自己的模块,同时保持系统整体的一致性和可用性。
用户评论