前端微前端之模块联邦
概要
- 前端微前端架构的核心是对资源作用域的控制,我调研了无界、乾坤、micro-app、iframe、模块联邦等方案,发现没有一个能完美解决问题的,相对来说利用
模块联邦+iframe
的方案更好些,调研结果见《微前端调研.xlsx》。
使用场景
应用级集成 - 将多个独立的应用集成在一起,构建微前端架构
- 实现应用之间的无缝切换
- 支持不同团队独立开发、部署应用组件
- 提升大型前端项目的可维护性
动态特性 - 实现区别于npm的动态包管理器
- 安装包不需要重新编译
- 在运行时可直接使用
- 支持动态加载资源,提高应用性能
集成选型
项目类型 | 推荐技术方案 | 优势 |
---|---|---|
基于vite/vue3的项目 | 模块联邦 | 高性能、交互灵活 |
基于vue-cli的vue3项目 | 模块联邦 (需先改成vite打包) | 高性能、交互灵活 |
其他技术栈 | iframe | 无技术栈限制 |
应用级集成
注意事项
部署结构
- 为兼容iframe方式,将各子应用打包后放在同一域名端口下,以应用目录区分
- 各子应用需配置正确的
baseUrl
以确保资源路径正确
路由配置
- 使用模块联邦进行应用级集成时,路由需选择history模式
- 配置正确的路由前缀,避免多应用间路由冲突
- iframe和模块联邦路由统一约定为:
- 子应用独立运行时路由为(/app1/*)
- 集成到主应用后路由为(/main/app1/*)
资源隔离
iframe方式集成
- 由于各应用处于同一域名端口下,localStorage/sessionStorage的key需带应用前缀,避免冲突
- 示例:
const storageKey =
${APP_PREFIX}:user-settings;
模块联邦方式集成
- CSS隔离:避免修改html、body等DOM节点的全局样式
- 组件内CSS应带scoped属性:
<style scoped>
- 全局样式包含在根节点作用域下
css/* 推荐做法 */ .app-container .global-style { /* 样式定义 */ } /* 避免使用 */ body, html { /* 全局样式 */ }
- JS隔离:避免将变量直接挂在window上
- 推荐将变量provide在app实例上或使用带应用前缀的命名
js// 推荐做法 app.provide('appState', state); // 或 window['APP_PREFIX_variable'] = value;
- 浏览器API:考虑对其他应用的影响,如localStorage/sessionStorage使用前缀
应用生命周期
集成结构对比:
- iframe集成:主应用→页面→iframe→子应用→页面
- 模块联邦集成:主应用→页面→子应用→页面
职责划分:
- 主应用:负责应用级的加载、卸载、保活等生命周期
- 子应用:负责组件级的生命周期管理
事件通信:
- eventbus需兼容两种集成结构,提供生命周期相关事件
js// 事件总线示例 const eventBus = { events: {}, on(event, callback) { if (!this.events[event]) this.events[event] = []; this.events[event].push(callback); }, emit(event, data) { if (this.events[event]) { this.events[event].forEach(cb => cb(data)); } } }; // 生命周期事件 eventBus.emit('app:mounted', { appName: 'childApp1' });
交互体验
iframe集成的挑战:
- 路由同步:浏览器刷新、前进、后退可能出现非预期效果
- DOM隔离:某些场景交互体验割裂
- 加载性能:应用加载较慢
- 解决方案:可考虑实现路由同步机制,或对小型组件使用模块联邦
模块联邦集成的优势:
- 无路由同步问题
- 无DOM隔离限制
- 加载性能更好
- 注意点:需重点关注资源隔离
组件级集成
与应用集成的区别
路由
- 子应用携带自己的路由,需要统一规划避免相互冲突,而远程组件不带路由,所以不会引起路由冲突。
应用实例
- 子应用有自己的应用实例,和主应用的实例隔离,注册在不同实例上的各模块不通,需要事件通信。
- 远程组件没有自己的应用实例,一般直接挂载到主应用上,与原主应用组件使用无区别。
服务结构
- 区别于中心化的应用集成,组件级集成是去中心化的网状结构,一个服务既可以是远程组件的消费者也可以是提供者。
注意事项
全局资源
- 组件依赖的全局资源,要么share主应用的,要么改为组件内import,否则找不到资源
- 组件内部也要避免全局变量及样式的定义,可以带前缀定义自己的作用域
共享范围
- 不局限于共享组件,还可共享函数、类等模块
模块联邦配置
Vite配置示例
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { federation } from '@module-federation/vite'
export default defineConfig({
plugins: [
vue(),
federation({
name: 'remoteApp1',
filename: 'remoteEntry.js',
// 暴露远程模块
exposes: {
'./RApp1Dialog': './src/views/Page1.vue', // 暴露组件
'./RApp1Table': './src/views/Page2.vue',
'./RApp1App': './src/export-app.ts', // 暴露应用
},
// 注册远程模块
remotes: {
remoteApp2: {
type: 'module',
name: 'remoteApp2',
entry:
env.NODE_ENV === 'production'
? 'http://localhost/app2/remoteEntry.js'
: 'http://localhost:8668/app2/remoteEntry.js',
},
},
// 共享全局依赖
shared: [
'vue',
'vue-router',
'pinia',
'vuex',
'element-plus',
'@xmagital/hsf-common-ui',
'@xmagital/hsf-event-bus',
],
}),
],
})
远程模块使用示例
js
// 在路由中使用远程应用
const RApp1App = createRemoteComponent({
loader: () => import('remoteApp1/RApp1App'),
basename: `${import.meta.env.BASE_URL}/app1`,
})
{
path: '/app1/:pathMatch(.*)*',
name: 'app1',
component: RApp1App,
}
// 在页面中使用远程组件
<template>
<h2>远程组件集合</h2>
<RApp1Dialog />
<RApp1Table />
</template>
<script setup lang="ts">
import RApp1Dialog from 'remoteApp1/RApp1Dialog'
import RApp1Table from 'remoteApp1/RApp1Table'
</script>
动态注册远程模块
- 对门户应用来说,更好的方式是利用模块联邦的运行时,动态注册其他远程模块
- 注意:模块联邦的运行时要求生产者用到
manifest
和getPublicPath
两个配置 - 参考:Module Federation Runtime