前言
随着公司平台业务的不断发展,项目规模逐渐扩大,部分功能模块需要独立迭代和部署。传统的单体应用架构已经难以满足团队协作和快速迭代的需求,微前端架构应运而生。
微前端架构能够很好地解决以下问题:
- 模块解耦:不同业务模块可以独立开发、测试和部署
- 独立部署:单个模块的更新不需要重新部署整个应用
- 技术栈无关:子应用可以使用不同的技术栈,逐步升级老旧项目
本文将分享我们在 Vue 2.x 主应用中集成 micro-app 的完整实战经验,希望能为有类似需求的开发者提供参考。
技术选型
在决定采用微前端架构后,我们对比了当前主流的几种方案:
| 方案 | 特点 | 适用场景 |
|---|---|---|
| qiankun | 基于 single-spa,生态成熟,沙箱隔离完善 | 需要完善沙箱隔离的大型项目 |
| micro-app | 基于 WebComponent,接入成本低,使用简单 | 快速接入,对侵入性要求低的项目 |
| iframe | 天然隔离,最简单,但通信受限 | 简单隔离场景,对通信要求不高 |
为什么选择 micro-app?
经过调研和评估,我们最终选择了 micro-app,主要原因如下:
- 接入成本低:只需在主应用中引入
@micro-zoe/micro-app,子应用几乎无需改动 - 对子应用侵入性小:不像 qiankun 需要子应用导出特定的生命周期函数
- 天然样式隔离:基于 WebComponent 的 Shadow DOM,样式隔离开箱即用
- 支持 Vue 2.x:我们的主应用是 Vue 2.x 项目,micro-app 完美支持
- 文档清晰:官方文档完善,社区活跃,遇到问题容易找到解决方案
当然,micro-app 也有其局限性,比如沙箱隔离不如 qiankun 完善,但对于我们的业务场景来说已经足够。
主应用集成实战
初始化配置
首先,安装 micro-app 依赖:
npm install @micro-zoe/micro-app
然后,在主应用的入口文件 main.js 中进行初始化:
import microApp from '@micro-zoe/micro-app';
import { registerMainAppRouter } from '@/utils/micro/microHandler';
import { initMicroAppNotify } from '@/utils/micro/event';
import { listenGlobalEvent, listenPartialEvent } from '@/utils/micro/listenEvent';
// 开启微前端基座
microApp.start({
'clear-data': true // 子应用卸载时清除缓存数据
});
// 注册主应用路由,用于控制子应用路由跳转
registerMainAppRouter(router);
// 初始化全局通知方法,挂载到 Vue 原型
initMicroAppNotify(Vue);
// 监听全局消息(来自所有子应用)
listenGlobalEvent();
// 监听局部消息(来自特定子应用)
listenPartialEvent();
关键配置说明:
microApp.start():启动微前端基座,必须在应用初始化时调用'clear-data': true:子应用卸载时自动清除其缓存数据,避免内存泄漏registerMainAppRouter(router):注册 Vue Router 实例,让 micro-app 能够控制子应用路由listenGlobalEvent()和listenPartialEvent():初始化消息监听,后续章节会详细说明
子应用配置管理
为了方便管理多个子应用,我们创建了统一的配置文件 src/utils/micro/config.js:
// 子应用名称常量
export const APP_EQUIPMENT = 'app-equipment';
export const APP_EQUIPMENT_MAIN_ROUTE_NAME = 'appsEquipment';
// 开发环境子应用地址
const config = {
[APP_EQUIPMENT]: 'http://localhost:5173'
};
// 子应用配置列表
export const MICRO_APPS = [
{
name: APP_EQUIPMENT, // 子应用唯一标识
oldMainRoute: 'equipmentManage', // 原主应用路由(用于匹配跳转)
mainRoute: APP_EQUIPMENT_MAIN_ROUTE_NAME, // 主应用定义的子应用路由名
entry: config[APP_EQUIPMENT], // 子应用入口地址
publicPath: '/bmd/equipment', // 生产环境部署路径
props: {} // 传递给子应用的额外属性
}
];
// 生产环境自动拼接域名
if (process.env.NODE_ENV === 'production') {
Object.keys(config).forEach(key => {
const microInfo = MICRO_APPS.find(item => item.name === key);
config[key] = window.location.origin + microInfo.publicPath;
});
}
export default config;
配置项说明:
| 配置项 | 说明 |
|---|---|
name | 子应用的唯一标识,用于通信和路由控制 |
oldMainRoute | 原来在主应用中的路由名称,用于判断跳转目标 |
mainRoute | 主应用中定义的子应用容器路由名称 |
entry | 子应用的访问地址 |
publicPath | 生产环境的部署路径 |
通过这种配置化的方式,我们可以轻松扩展新的子应用,只需在 MICRO_APPS 数组中添加配置即可。
路由同步机制
微前端架构中,路由同步是一个核心问题。我们需要处理两种场景:
- 主应用跳转子应用:用户从主应用跳转到子应用页面
- 子应用内部跳转:子应用已激活,需要在子应用内部进行路由跳转
判断是否为子应用路由
// src/utils/micro/microHandler.js
/**
* 通过 URL 路径判断是否为子应用
*/
export function isSubAppByPath(currentPath = window.location.pathname, basePath = '/apps/') {
return currentPath.includes(basePath);
}
/**
* 根据原始路由路径查找子应用信息
*/
export function findAppInfoByOriginalPath(path) {
const findApp = MICRO_APPS.find(app => path?.includes(app.oldMainRoute));
if (findApp) {
return {
isSubApp: true,
...findApp
};
}
return { isSubApp: false };
}
跳转子应用的核心方法
/**
* 跳转子应用:区分已激活和未激活状态
*/
export async function navigateToSubApp(data = {}) {
const { router, appName, path, params, callback } = data;
// 获取当前已激活的子应用列表
const activeApps = microApp.getActiveApps();
if (activeApps.includes(appName)) {
// 子应用已激活,通过消息通知子应用切换路由
await notifySubApps(appName, microAppPartialEventKey.RouterChange, { path, params }, callback);
} else {
// 子应用未激活,通过主应用路由跳转加载子应用
const appInfo = getAppInfoByName(appName);
await router.push({
name: appInfo.mainRoute,
query: genAppRouterParams(path, params)
});
}
}
/**
* 子应用已激活时的路由跳转
*/
export function navigateToSubAppInActive(appName, path) {
microApp.router.push({ name: appName, path: path });
}
路由同步流程:
用户点击跳转
↓
判断目标是否为子应用路由
↓
├─ 是子应用路由
│ ↓
│ 判断子应用是否已激活
│ ├─ 已激活 → 发送消息通知子应用切换路由
│ └─ 未激活 → 主应用路由跳转到子应用容器页
│
└─ 不是子应用路由
↓
正常的主应用路由跳转
这样设计的好处是:子应用无论是否已加载,都能正确跳转到目标页面。
主子应用通信设计
micro-app 提供了两种通信方式:全局通信和局部通信。
1. 全局通信
全局通信适用于广播消息,所有子应用都能收到:
// src/utils/micro/event.js
import microApp from '@micro-zoe/micro-app';
/**
* 主应用向所有子应用发送全局消息
*/
export function sendGlobalMessageToMainApp(data, callback) {
return new Promise((resolve, reject) => {
try {
if (typeof callback === 'function') {
microApp.setGlobalData(data, res => {
callback(res);
resolve(res);
});
} else {
microApp.setGlobalData(data, () => {
resolve(true);
});
}
} catch (error) {
console.error('发送消息失败:', error);
reject(error);
}
});
}
/**
* 发送特定类型的全局数据
*/
export function globalNotifySubApps(eventType, payload = null, callback = null) {
if (!payload) {
payload = {};
}
payload.app_id = 'main-app';
payload.create_time = getCurrentTime();
const data = { [eventType]: payload };
return sendGlobalMessageToMainApp(data, callback);
}
2. 局部通信
局部通信适用于与特定子应用交互:
/**
* 向指定子应用发送消息
*/
export function sendMessageToSub(appName, data, callback) {
return new Promise((resolve, reject) => {
try {
if (typeof callback === 'function') {
microApp.setData(appName, data, res => {
callback(res);
resolve(res);
});
} else {
microApp.setData(appName, data, () => {
resolve(true);
});
}
} catch (error) {
console.error('发送消息失败:', error);
reject(error);
}
});
}
/**
* 发送特定类型的消息给指定子应用
*/
export function notifySubApps(appName, eventType, payload = null, callback = null) {
if (!payload) payload = {};
payload.app_id = 'main-app';
payload.create_time = getCurrentTime();
const data = { [eventType]: payload };
return sendMessageToSub(appName, data, callback);
}
3. 事件类型定义
为了规范通信,我们定义了统一的事件类型:
// src/utils/micro/config.js
// 全局通信事件
export const microAppGlobalEventKey = {
reqGlobalData: 'reqGlobalData', // 子应用请求全局数据
ackGlobalData: 'ackGlobalData', // 主应用响应全局数据
reqLogout: 'reqLogout', // 请求登出
reqPermissionData: 'reqPermissionData' // 请求权限数据
};
// 局部通信事件
export const microAppPartialEventKey = {
RequestStartSip: 'RequestStartSip', // 请求发起 SIP 通话
RouterChange: 'RouterChange', // 路由变化通知
SetWinLocationHref: 'SetWinLocationHref' // 页面跳转
};
通信方式对比:
| 通信方式 | API | 适用场景 |
|---|---|---|
| 全局通信 | setGlobalData / addGlobalDataListener | 广播消息,如登录状态变更 |
| 局部通信 | setData / addDataListener | 与特定子应用交互 |
全局状态共享
子应用通常需要获取主应用的一些全局状态,如用户信息、Token、权限等。我们通过以下方式实现状态共享:
1. 定义共享数据结构
// src/utils/micro/genData.js
import { getPlatformUrl, getMqttInfo } from '@/utils/dynamic';
import { AccessToken, getStorage } from '@/utils/storage';
export function generateGlobalDataFormStore(store) {
const HOST_INFO = getPlatformUrl();
const mqttInfo = getMqttInfo();
return {
// 用户信息
userInfo: store.state.userInfo,
token: getStorage(AccessToken),
// 系统信息
mainPlatformInfo: store.state.config.mainPlatformInfo,
// 平台地址
platformUrl: {},
// MQTT 连接信息
mqttInfo: {},
// 权限数据
permission: {}
};
}
2. 主应用主动推送
当关键状态变化时(如登录成功),主应用主动推送:
// 登录成功后
await globalNotifySubApps(
microAppGlobalEventKey.ackGlobalData,
generateGlobalDataFormStore(this.$store)
);
3. 子应用主动请求
子应用也可以主动请求数据:
// 监听全局消息
export function listenGlobalEvent() {
microApp.addGlobalDataListener(data => {
for (const key of Object.keys(data)) {
if (key === microAppGlobalEventKey.reqGlobalData) {
// 子应用请求全局数据,主应用响应
globalNotifySubApps(
microAppGlobalEventKey.ackGlobalData,
generateGlobalDataFormStore(store)
);
} else if (key === microAppGlobalEventKey.ackGlobalData) {
// 收到主应用的响应,更新本地状态
// 子应用处理逻辑
}
}
});
}
共享数据边界原则:
- 最小化原则:只共享必要的、全局性的数据
- 只读原则:子应用只能读取,不能直接修改主应用状态
- 明确职责:状态变更由主应用统一管理
踩坑与解决
在实际开发过程中,我们遇到了不少问题,这里分享几个典型的踩坑经历和解决方案。
路由参数传递与加密
问题描述:
在跳转子应用时,我们通常需要传递一些参数,如 room_id、type 等。最初我们直接通过 URL query 传递:
router.push({
name: 'appsEquipment',
query: { room_id: '123', type: 'device' }
});
这样做有两个问题:
- 参数明文暴露在 URL 中,存在安全风险
- 参数可能被用户随意修改,导致不可预期的行为
解决方案:
我们封装了参数加解密方法,使用 DES 加密算法:
// src/utils/micro/microHandler.js
import { encryptByDES, decryptByDES } from '@/business/cryptoTool';
/**
* 加密子应用跳转参数
*/
export function genAppRouterParams(path, params) {
if (!path || typeof path !== 'string') {
console.warn('Invalid path parameter');
return { _tag: '' };
}
const data = {
path: path,
params: params || {}
};
try {
return {
_tag: encodeURIComponent(encryptByDES(JSON.stringify(data))),
};
} catch (error) {
console.error('Failed to encrypt router parameters:', error);
return { _tag: '' };
}
}
/**
* 解密子应用跳转参数
*/
export function decryptAppRouterParams(params) {
if (!params || typeof params !== 'object') {
return {};
}
const encryptedTag = params._tag;
if (!encryptedTag || typeof encryptedTag !== 'string') {
return {};
}
try {
const decodeStr = decodeURIComponent(encryptedTag);
const decryptedData = decryptByDES(decodeStr);
if (!decryptedData) {
return {};
}
const parsedData = JSON.parse(decryptedData);
return {
path: parsedData.path || '',
params: parsedData.params || {}
};
} catch (error) {
console.error('Failed to decrypt:', error);
return {};
}
}
使用方式:
// 跳转时加密
await router.push({
name: appInfo.mainRoute,
query: genAppRouterParams('/deviceControl', { room_id: '123' })
});
// 子应用容器中解密
created() {
const query = this.$route.query || {};
this.jumpParams = decryptAppRouterParams(query);
// jumpParams: { path: '/deviceControl', params: { room_id: '123' } }
}
这样既保证了参数安全,又能正常传递复杂的参数对象。
登录状态同步
问题描述:
用户在主应用登录成功后,子应用无法感知登录状态变化。如果子应用在登录前就已经加载,它会一直处于未登录状态。
解决方案:
我们采用「主动推送 + 按需请求」的双重机制:
1. 登录成功后主动推送
// src/pc/view/login/login.vue
async getUserDetail() {
// ... 登录逻辑
// 登录成功,更新 Vuex 中的用户信息
await this.$store.dispatch('setUserInfo', { userInfo });
// 通知所有子应用
await globalNotifySubApps(
microAppGlobalEventKey.ackGlobalData,
generateGlobalDataFormStore(this.$store)
);
this._notify('登录成功', 'success');
// ... 后续跳转逻辑
}
2. 子应用主动请求
对于在主应用登录成功后才加载的子应用,它可以通过事件主动请求数据:
// 主应用监听全局消息
export function listenGlobalEvent() {
microApp.addGlobalDataListener(data => {
for (const key of Object.keys(data)) {
if (key === microAppGlobalEventKey.reqGlobalData) {
// 子应用请求数据,主应用响应
globalNotifySubApps(
microAppGlobalEventKey.ackGlobalData,
generateGlobalDataFormStore(store)
);
}
}
});
}
子应用端代码(参考):
// 子应用 mounted 时请求数据
mounted() {
// 向主应用发送数据请求
window.microApp.dispatch({ reqGlobalData: {} });
}
// 监听主应用响应
window.microApp.addGlobalDataListener((data) => {
if (data.ackGlobalData) {
// 更新子应用状态
this.userInfo = data.ackGlobalData.userInfo;
this.token = data.ackGlobalData.token;
}
});
通过这种方式,无论子应用何时加载,都能获取到最新的用户状态。
登出时数据清理
问题描述:
用户退出登录后,主应用清除了本地存储的用户信息,但子应用内部可能还缓存着用户数据。如果不清除,可能导致:
- 下一个用户登录后看到上一个用户的数据
- 子应用使用过期的 Token 发起请求
解决方案:
我们在登出时做了两件事:
1. 清除主应用全局数据
// src/pc/components/mixin/logout.js
import microApp from '@micro-zoe/micro-app';
export default {
methods: {
logout() {
logout().then(res => {
if (res.error === OK) {
// ... 清除本地存储
removeAllStorage();
// 清空微前端全局数据
microApp.clearGlobalData();
// 跳转登录页
this.$router.replace({ name: 'login' });
}
});
}
}
};
2. 通知子应用执行清理
// src/utils/micro/listenEvent.js
export function listenGlobalEvent() {
microApp.addGlobalDataListener(data => {
for (const key of Object.keys(data)) {
// ... 其他事件处理
if (key === microAppGlobalEventKey.reqLogout) {
// 子应用发起登出请求
// 触发全局登出事件
window.dispatchEvent(new CustomEvent(MAIN_APP_RECEIVE_SUB_APP_LOGOUT_EVENT));
}
}
});
}
子应用处理登出事件(参考):
// 子应用监听登出事件
window.microApp.addGlobalDataListener((data) => {
if (data.reqLogout) {
// 清除子应用本地数据
localStorage.clear();
sessionStorage.clear();
// 重置子应用状态
// ...
}
});
同时,在主应用初始化时配置了 'clear-data': true,这会确保子应用卸载时自动清除 micro-app 内部的缓存数据:
microApp.start({
'clear-data': true
});
这样就形成了完整的清理链路,确保用户数据不会残留。
子应用容器组件
问题描述:
最初我们直接在路由组件中使用 <micro-app> 标签加载子应用,但随着需求增加,我们需要处理:
- 子应用生命周期事件
- 路由参数的传递和解密
- 错误处理和加载状态
解决方案:
我们创建了专门的容器组件 subEquipment.vue:
<template>
<div>
<micro-app
:name="APP_EQUIPMENT"
:url="url"
iframe
@created="handleCreate"
@beforemount="handleBeforeMount"
@mounted="handleMount"
@unmount="handleUnmount"
@error="handleError"
@datachange="handleDataChange"
></micro-app>
</div>
</template>
<script>
import config, { APP_EQUIPMENT } from '@/utils/micro/config';
import { decryptAppRouterParams, navigateToSubAppInActive } from '@/utils/micro/microHandler';
import { urlJoinQuery } from '@/utils/util';
export default {
name: 'appsEquipment',
data() {
return {
APP_EQUIPMENT,
url: `${config[APP_EQUIPMENT]}/`,
jumpParams: {},
isVirtual: false
};
},
created() {
// 解密路由参数
const query = this.$route.query || {};
this.isVirtual = !!query[APP_EQUIPMENT];
this.jumpParams = decryptAppRouterParams(query);
},
methods: {
handleCreate() {
console.log('子应用创建了');
},
handleBeforeMount() {
console.log('子应用即将渲染');
// 在挂载前执行路由跳转
this.jumpTo();
},
handleMount() {
console.log('子应用渲染完成');
},
handleUnmount() {
console.log('子应用卸载了');
},
handleError() {
console.log('子应用加载出错了');
this.$message.error('子应用加载失败,请刷新重试');
},
handleDataChange(e) {
console.log('来自子应用的数据:', e.detail.data);
},
jumpTo() {
if (this.jumpParams.path && !this.isVirtual) {
const tempUrl = urlJoinQuery(this.jumpParams.path, this.jumpParams.params);
navigateToSubAppInActive(this.APP_EQUIPMENT, tempUrl);
this.jumpParams = {};
}
}
}
};
</script>
关键点说明:
| 属性/事件 | 说明 |
|---|---|
name | 子应用唯一标识 |
url | 子应用入口地址 |
iframe | 使用 iframe 沙箱模式,更好的隔离性 |
@created | 子应用创建时触发 |
@beforemount | 子应用即将渲染时触发,适合做路由跳转 |
@mounted | 子应用渲染完成时触发 |
@unmount | 子应用卸载时触发 |
@error | 子应用加载出错时触发 |
@datachange | 子应用向主应用发送消息时触发 |
通过容器组件的封装,我们实现了子应用加载的标准化,后续新增子应用只需创建类似的容器组件即可。
子应用集成实战
上一节介绍了主应用的集成方式,本节将详细介绍子应用端的开发实践。子应用需要考虑两种运行场景:独立运行和作为微应用嵌入。
1. 初始化配置与生命周期
子应用的初始化需要区分独立运行和嵌入模式,并正确处理生命周期钩子。
入口文件配置
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './stores';
import { listenMainEvent, listenPartialEvent } from '@/micro-common/utils/micro/listenEvent.js';
import { isMicroAppEnv } from '@/micro-common/utils/micro/index.js';
let app = null;
// 应用挂载
function mount() {
app = createApp(App);
app.use(store);
app.use(router);
// ... 其他全局配置
app.mount('#app');
}
// 应用卸载
function unmount() {
app?.unmount();
app = null;
console.log('微应用卸载了');
}
// 无论如何都先挂载
mount();
// 微前端环境下额外处理
if (isMicroAppEnv()) {
// 监听主应用事件
listenMainEvent();
listenPartialEvent();
// 监听卸载操作
window.addEventListener('unmount', () => {
unmount();
});
}
环境检测方法
// src/micro-common/utils/micro/index.js
export function isMicroAppEnv() {
return !!window.__MICRO_APP_ENVIRONMENT__;
}
关键点说明:
| 要点 | 说明 |
|---|---|
| 双模式运行 | 通过 isMicroAppEnv() 区分独立/嵌入模式 |
| 先挂载后判断 | 避免微前端环境下应用无法启动 |
| 事件监听 | 仅在微前端环境下监听主应用消息 |
| 卸载清理 | 监听 unmount 事件释放资源 |
生命周期流程:
子应用加载
↓
mount() 挂载应用
↓
判断微前端环境
├─ 是 → 注册事件监听
│ ↓
│ 监听 unmount 事件
│ ↓
│ unmount() 卸载清理
│
└─ 否 → 正常独立运行
2. 环境检测与路由跳转
子应用在不同环境下需要采用不同的路由跳转策略,以下介绍核心的路由工具方法。
核心工具方法
// src/micro-common/utils/micro/index.js
/**
* 检查当前是否为微前端环境
*/
export function isMicroAppEnv() {
return !!window.__MICRO_APP_ENVIRONMENT__;
}
/**
* 获取主应用路由实例
*/
export function getMainAppRouter() {
return window.microApp?.router?.getBaseAppRouter() || null;
}
/**
* 跳转主应用页面
* @param {Object} routerOptions - 路由配置 { name, path, query }
* @param {Boolean} replace - 是否使用 replace 模式
*/
export function jumpToMainApp(routerOptions = {}, replace = false) {
const baseAppRouter = getMainAppRouter();
if (!baseAppRouter) {
console.error('无法获取主应用路由实例');
return;
}
return replace
? baseAppRouter.replace(routerOptions)
: baseAppRouter.push(routerOptions);
}
/**
* 跳转其他子应用
* @param {Object} options - { name: 应用名, path: 路径, state: 状态 }
*/
export function pushToOtherApp(options = {}) {
const { name, path, state, replace = false } = options;
const routeParams = { replace };
if (name) routeParams.name = name;
if (path) routeParams.path = path;
if (state !== undefined) routeParams.state = state;
return window.microApp?.router?.push(routeParams);
}
/**
* 路由历史操作
*/
export const routerGo = (n) => window.microApp?.router?.go(n);
export const routerBack = () => window.microApp?.router?.back();
export const routerForward = () => window.microApp?.router?.forward();
使用示例
// 在子应用组件中使用
import { isMicroAppEnv, jumpToMainApp, pushToOtherApp, routerBack } from '@/micro-common/utils/micro';
export default {
methods: {
// 跳转到主应用的某个页面
goToMainPage() {
if (isMicroAppEnv()) {
jumpToMainApp({ name: 'dashboard' });
} else {
// 独立环境下的降级处理
this.$router.push({ name: 'home' });
}
},
// 跳转到其他子应用
goToOtherSubApp() {
if (isMicroAppEnv()) {
pushToOtherApp({
name: 'app-other',
path: '/detail',
state: { id: '123' }
});
}
},
// 返回上一页(支持跨应用)
goBack() {
if (isMicroAppEnv()) {
routerBack();
} else {
this.$router.back();
}
}
}
}
路由跳转场景对照表:
| 场景 | 方法 | 说明 |
|---|---|---|
| 子应用内部跳转 | this.$router.push() | 正常使用 Vue Router |
| 跳转主应用页面 | jumpToMainApp() | 获取主应用路由实例跳转 |
| 跳转其他子应用 | pushToOtherApp() | 通过 micro-app 路由 API |
| 返回上一页 | routerBack() | 支持跨应用返回 |
3. 主子应用通信机制
micro-app 提供全局通信和局部通信两种方式,子应用需要根据场景选择合适的通信方式。
事件类型定义
// src/micro-common/utils/micro/config.js
// 全局通信事件(广播到所有应用)
export const microAppGlobalEventKey = {
reqGlobalData: 'reqGlobalData', // 请求全局数据
ackGlobalData: 'ackGlobalData', // 接收全局数据
reqLogout: 'reqLogout', // 请求登出
reqPermissionData: 'reqPermissionData' // 请求权限数据
};
// 局部通信事件(定向通信)
export const microAppPartialEventKey = {
RequestStartSip: 'RequestStartSip', // SIP 通话请求
RouterChange: 'RouterChange', // 路由变化通知
SetWinLocationHref: 'SetWinLocationHref' // 页面跳转
};
发送消息方法
// src/micro-common/utils/micro/event.js
/**
* 发送全局消息(广播)
*/
export function sendGlobalData(eventType, payload = {}, callback = null) {
payload.app_id = APP_UNIQUE_NAME;
payload.create_time = formatDateTime();
const data = { [eventType]: payload };
return new Promise((resolve, reject) => {
try {
if (callback) {
window.microApp.setGlobalData(data, res => {
callback(res);
resolve(res);
});
} else {
window.microApp.setGlobalData(data, () => resolve(true));
}
} catch (error) {
reject(error);
}
});
}
/**
* 发送局部消息(定向)
*/
export function dispatchData(eventType, payload = {}, callback = null) {
payload.app_id = APP_UNIQUE_NAME;
payload.create_time = formatDateTime();
const data = { [eventType]: payload };
return new Promise((resolve, reject) => {
try {
if (callback) {
window.microApp.dispatch(data, res => {
callback(res);
resolve(res);
});
} else {
window.microApp.dispatch(data, () => resolve(true));
}
} catch (error) {
reject(error);
}
});
}
监听消息方法
// src/micro-common/utils/micro/listenEvent.js
/**
* 监听全局事件
*/
export function listenMainEvent() {
window.microApp.addGlobalDataListener(async (data) => {
console.log('子应用收到全局数据', data);
// 接收全局数据
if (data[microAppGlobalEventKey.ackGlobalData]) {
const globalData = data[microAppGlobalEventKey.ackGlobalData];
// 更新用户信息、系统信息、权限等
userStore.setUserToken(globalData.token);
userStore.refreshUserInfo(globalData.userInfo);
systemStore.syncMainAppData(globalData);
permissionStore.updatePermissionNameList(globalData?.permission?.permissionMenuNameList);
}
// 处理登出通知
if (data[microAppGlobalEventKey.reqLogout]) {
const logoutMsg = data[microAppGlobalEventKey.reqLogout];
// 忽略自己发送的登出消息
if (logoutMsg.app_id !== APP_UNIQUE_NAME) {
await handleOtherAppLogout();
}
}
}, true);
}
/**
* 监听局部事件
*/
export function listenPartialEvent() {
window.microApp.addDataListener(async (data) => {
console.log('收到主应用消息', data);
// 处理路由变化
if (data[microAppPartialEventKey.RouterChange]) {
const { path, params } = data[microAppPartialEventKey.RouterChange];
await router.push({ path, query: params });
}
});
}
通信方式选择指南:
| 通信方式 | API | 适用场景 | 特点 |
|---|---|---|---|
| 全局通信 | setGlobalData | 登出、全局状态变更 | 所有应用都能收到 |
| 局部通信 | dispatch | 路由跳转、业务交互 | 仅主应用收到 |
通信流程图:
子应用 主应用
│ │
│ setGlobalData(reqGlobalData) │
│─────────────────────────────>│
│ │ 响应数据
│ ackGlobalData │
│<─────────────────────────────│
│ │
│ dispatch(RouterChange) │
│─────────────────────────────>│ 处理路由跳转
│ │
4. 状态同步与登出处理
子应用需要与主应用保持关键状态同步,包括用户信息、权限数据和登录状态。
状态同步机制
子应用有两种方式获取主应用状态:
方式一:主应用主动推送
当主应用登录成功或状态变化时,主动向子应用推送数据:
主应用 子应用
│ │
│ 登录成功 │
│ │
│ setGlobalData(ackGlobalData) │
│─────────────────────────────────>│
│ │ 更新本地状态
│ │
方式二:子应用主动请求
子应用初始化时主动请求数据,例如在路由守卫中请求权限数据:
// src/permission.js
router.beforeEach(async (to, from, next) => {
// ... 其他逻辑
// 检查是否已有权限数据
if (permissionNameList.length === 0) {
// 向主应用请求权限数据
await sendGlobalData(microAppGlobalEventKey.reqPermissionData, {}, (res) => {
permissionStore.updatePermissionNameList(res);
});
}
// 构建动态路由
const routeList = await permissionStore.buildAsyncRoutes();
routeList.forEach(item => router.addRoute(item));
next();
});
登出处理
登出是最关键的状态同步场景,需要确保所有应用数据正确清理:
// src/micro-common/hooks/useLogout.js
export function useLogout() {
/**
* 子应用主动登出
*/
const logoutBase = async (userFunc = null, isApi = true) => {
if (isMicroAppEnv()) {
// 1. 执行通用清理
await commonLogout();
// 2. 执行用户自定义操作
if (userFunc) userFunc();
// 3. 清空全局数据缓存
window.microApp.clearGlobalData();
// 4. 通知主应用和其他子应用
await sendGlobalData(microAppGlobalEventKey.reqLogout);
} else {
// 独立环境:调用登出API并跳转
const res = await logout();
if (res.error === API_CODE.OK) {
removeAllStorage();
clearIndexDbData();
if (userFunc) userFunc();
window.location.href = '/login';
await commonLogout();
}
}
};
/**
* 收到其他子应用登出通知
*/
const handleOtherAppLogout = async () => {
// 清除本地存储
removeAllStorage();
clearIndexDbData();
// 重置状态
await commonLogout();
};
return { logoutBase, handleOtherAppLogout };
}
登出流程图:
用户点击登出
│
▼
子应用 A 执行登出
│
├─ 清理本地状态
├─ 清空全局缓存
│
▼
发送 reqLogout 全局消息
│
├────────────────────┬────────────────────┐
▼ ▼ ▼
主应用 子应用 B 子应用 C
│ │ │
清理主应用状态 handleOtherAppLogout handleOtherAppLogout
│ │ │
▼ ▼ ▼
跳转登录页 重置本地状态 重置本地状态
关键点说明:
| 要点 | 说明 |
|---|---|
| 双向同步 | 主应用推送 + 子应用请求,确保数据一致性 |
| 登出广播 | 一个子应用登出,所有应用都收到通知 |
| 忽略自身 | 处理登出消息时需判断是否为自己发送 |
| 清理顺序 | 先清缓存,再发通知,避免状态回写 |
5. 完整示例与最佳实践
完整初始化示例
// main.js - 完整的子应用入口配置
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './stores';
import { listenMainEvent, listenPartialEvent } from '@/micro-common/utils/micro/listenEvent.js';
import { isMicroAppEnv } from '@/micro-common/utils/micro/index.js';
let app = null;
function mount() {
app = createApp(App);
app.use(store);
app.use(router);
app.mount('#app');
}
function unmount() {
app?.unmount();
app = null;
}
// 启动应用
mount();
// 微前端环境额外处理
if (isMicroAppEnv()) {
listenMainEvent();
listenPartialEvent();
window.addEventListener('unmount', unmount);
}
业务组件示例
<template>
<div>
<button @click="handleLogout">退出登录</button>
<button @click="goToMainDashboard">返回主应用</button>
</div>
</template>
<script>
import { isMicroAppEnv, jumpToMainApp } from '@/micro-common/utils/micro';
import { useLogout } from '@/micro-common/hooks/useLogout';
export default {
methods: {
async handleLogout() {
const { logoutBase } = useLogout();
await logoutBase();
},
goToMainDashboard() {
if (isMicroAppEnv()) {
jumpToMainApp({ name: 'dashboard' });
}
}
}
}
</script>
最佳实践清单
| 实践 | 说明 | 重要性 |
|---|---|---|
| 环境判断优先 | 所有微前端相关操作前先判断环境 | ⭐⭐⭐ |
| 双模式兼容 | 确保子应用可独立运行调试 | ⭐⭐⭐ |
| 错误边界 | 通信失败时提供降级方案 | ⭐⭐ |
| 状态只读 | 子应用不应直接修改主应用状态 | ⭐⭐⭐ |
| 资源清理 | 卸载时清理定时器、事件监听等 | ⭐⭐⭐ |
| 日志记录 | 通信过程添加调试日志 | ⭐ |
常见问题与解决
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 路由跳转无效 | 未区分环境使用路由方法 | 使用 jumpToMainApp 等封装方法 |
| 登出后数据残留 | 未清理所有存储 | 调用 removeAllStorage() 和 clearIndexDbData() |
| 权限不同步 | 未请求或未收到推送 | 检查 reqPermissionData 和 ackGlobalData 处理 |
| 样式冲突 | 未启用样式隔离 | 主应用使用 iframe 沙箱模式 |
调试技巧
// 在开发环境开启详细日志
if (process.env.NODE_ENV === 'development') {
window.microApp.addGlobalDataListener((data) => {
console.log('[MicroApp Debug] 全局数据:', data);
});
window.microApp.addDataListener((data) => {
console.log('[MicroApp Debug] 局部数据:', data);
});
}
总结
通过这次微前端改造实践,我们成功将设备管控模块拆分为独立的子应用,实现了模块的独立开发和部署。以下是我们的关键收获:
技术选型:
- micro-app 接入成本低,适合快速落地,特别适合 Vue 2.x 项目
- 基于 WebComponent 的设计理念,天然支持样式隔离
架构设计:
- 通信机制设计是微前端的核心,需要提前规划事件类型和数据结构
- 路由同步需要区分子应用的激活状态,采用不同的跳转策略
- 状态共享要有明确的边界,子应用只读,主应用统一管理
实践经验:
- 敏感参数需要加密传递,避免明文暴露
- 登录状态同步采用「主动推送 + 按需请求」双重机制
- 登出时需要清理所有应用的数据,避免用户数据残留
- 子应用容器组件的封装提高了代码的可维护性
后续优化方向:
- 子应用预加载:在用户可能访问前预加载子应用资源,提升体验
- 公共依赖抽离:将 Vue、Element UI 等公共依赖抽离,减少重复加载
- 错误边界处理:完善子应用加载失败的处理和重试机制
- 性能监控:添加子应用加载性能监控,持续优化用户体验
微前端不是银弹,它会带来一定的复杂度。在选择微前端之前,需要评估团队规模、业务复杂度和维护成本。如果项目规模较小,单体应用可能更合适。