基于micro-app的微前端落地实践

0 阅读17分钟

前言

随着公司平台业务的不断发展,项目规模逐渐扩大,部分功能模块需要独立迭代和部署。传统的单体应用架构已经难以满足团队协作和快速迭代的需求,微前端架构应运而生。

微前端架构能够很好地解决以下问题:

  1. 模块解耦:不同业务模块可以独立开发、测试和部署
  2. 独立部署:单个模块的更新不需要重新部署整个应用
  3. 技术栈无关:子应用可以使用不同的技术栈,逐步升级老旧项目

本文将分享我们在 Vue 2.x 主应用中集成 micro-app 的完整实战经验,希望能为有类似需求的开发者提供参考。

技术选型

在决定采用微前端架构后,我们对比了当前主流的几种方案:

方案特点适用场景
qiankun基于 single-spa,生态成熟,沙箱隔离完善需要完善沙箱隔离的大型项目
micro-app基于 WebComponent,接入成本低,使用简单快速接入,对侵入性要求低的项目
iframe天然隔离,最简单,但通信受限简单隔离场景,对通信要求不高

为什么选择 micro-app?

经过调研和评估,我们最终选择了 micro-app,主要原因如下:

  1. 接入成本低:只需在主应用中引入 @micro-zoe/micro-app,子应用几乎无需改动
  2. 对子应用侵入性小:不像 qiankun 需要子应用导出特定的生命周期函数
  3. 天然样式隔离:基于 WebComponent 的 Shadow DOM,样式隔离开箱即用
  4. 支持 Vue 2.x:我们的主应用是 Vue 2.x 项目,micro-app 完美支持
  5. 文档清晰:官方文档完善,社区活跃,遇到问题容易找到解决方案

当然,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 数组中添加配置即可。

路由同步机制

微前端架构中,路由同步是一个核心问题。我们需要处理两种场景:

  1. 主应用跳转子应用:用户从主应用跳转到子应用页面
  2. 子应用内部跳转:子应用已激活,需要在子应用内部进行路由跳转

判断是否为子应用路由

// 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) {
        // 收到主应用的响应,更新本地状态
        // 子应用处理逻辑
      }
    }
  });
}

共享数据边界原则:

  1. 最小化原则:只共享必要的、全局性的数据
  2. 只读原则:子应用只能读取,不能直接修改主应用状态
  3. 明确职责:状态变更由主应用统一管理

踩坑与解决

在实际开发过程中,我们遇到了不少问题,这里分享几个典型的踩坑经历和解决方案。

路由参数传递与加密

问题描述:

在跳转子应用时,我们通常需要传递一些参数,如 room_idtype 等。最初我们直接通过 URL query 传递:

router.push({
  name: 'appsEquipment',
  query: { room_id: '123', type: 'device' }
});

这样做有两个问题:

  1. 参数明文暴露在 URL 中,存在安全风险
  2. 参数可能被用户随意修改,导致不可预期的行为

解决方案:

我们封装了参数加解密方法,使用 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;
  }
});

通过这种方式,无论子应用何时加载,都能获取到最新的用户状态。

登出时数据清理

问题描述:

用户退出登录后,主应用清除了本地存储的用户信息,但子应用内部可能还缓存着用户数据。如果不清除,可能导致:

  1. 下一个用户登录后看到上一个用户的数据
  2. 子应用使用过期的 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> 标签加载子应用,但随着需求增加,我们需要处理:

  1. 子应用生命周期事件
  2. 路由参数的传递和解密
  3. 错误处理和加载状态

解决方案:

我们创建了专门的容器组件 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()
权限不同步未请求或未收到推送检查 reqPermissionDataackGlobalData 处理
样式冲突未启用样式隔离主应用使用 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 的设计理念,天然支持样式隔离

架构设计:

  • 通信机制设计是微前端的核心,需要提前规划事件类型和数据结构
  • 路由同步需要区分子应用的激活状态,采用不同的跳转策略
  • 状态共享要有明确的边界,子应用只读,主应用统一管理

实践经验:

  • 敏感参数需要加密传递,避免明文暴露
  • 登录状态同步采用「主动推送 + 按需请求」双重机制
  • 登出时需要清理所有应用的数据,避免用户数据残留
  • 子应用容器组件的封装提高了代码的可维护性

后续优化方向:

  1. 子应用预加载:在用户可能访问前预加载子应用资源,提升体验
  2. 公共依赖抽离:将 Vue、Element UI 等公共依赖抽离,减少重复加载
  3. 错误边界处理:完善子应用加载失败的处理和重试机制
  4. 性能监控:添加子应用加载性能监控,持续优化用户体验

微前端不是银弹,它会带来一定的复杂度。在选择微前端之前,需要评估团队规模、业务复杂度和维护成本。如果项目规模较小,单体应用可能更合适。

参考文档

  1. micro-app 官方文档
  2. 微前端架构初探