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

前言

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

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

  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 依赖:

bash 复制代码
npm install @micro-zoe/micro-app

然后,在主应用的入口文件 main.js 中进行初始化:

javascript 复制代码
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

javascript 复制代码
// 子应用名称常量
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. 子应用内部跳转:子应用已激活,需要在子应用内部进行路由跳转

判断是否为子应用路由

javascript 复制代码
// 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 };
}

跳转子应用的核心方法

javascript 复制代码
/**
 * 跳转子应用:区分已激活和未激活状态
 */
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 });
}

路由同步流程:

markdown 复制代码
用户点击跳转
    ↓
判断目标是否为子应用路由
    ↓
├─ 是子应用路由
│   ↓
│   判断子应用是否已激活
│   ├─ 已激活 → 发送消息通知子应用切换路由
│   └─ 未激活 → 主应用路由跳转到子应用容器页
│
└─ 不是子应用路由
    ↓
    正常的主应用路由跳转

这样设计的好处是:子应用无论是否已加载,都能正确跳转到目标页面。

主子应用通信设计

micro-app 提供了两种通信方式:全局通信局部通信

1. 全局通信

全局通信适用于广播消息,所有子应用都能收到:

javascript 复制代码
// 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. 局部通信

局部通信适用于与特定子应用交互:

javascript 复制代码
/**
 * 向指定子应用发送消息
 */
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. 事件类型定义

为了规范通信,我们定义了统一的事件类型:

javascript 复制代码
// 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. 定义共享数据结构

javascript 复制代码
// 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. 主应用主动推送

当关键状态变化时(如登录成功),主应用主动推送:

javascript 复制代码
// 登录成功后
await globalNotifySubApps(
  microAppGlobalEventKey.ackGlobalData,
  generateGlobalDataFormStore(this.$store)
);

3. 子应用主动请求

子应用也可以主动请求数据:

javascript 复制代码
// 监听全局消息
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 传递:

javascript 复制代码
router.push({
  name: 'appsEquipment',
  query: { room_id: '123', type: 'device' }
});

这样做有两个问题:

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

解决方案:

我们封装了参数加解密方法,使用 DES 加密算法:

javascript 复制代码
// 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 {};
  }
}

使用方式:

javascript 复制代码
// 跳转时加密
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. 登录成功后主动推送

javascript 复制代码
// 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. 子应用主动请求

对于在主应用登录成功后才加载的子应用,它可以通过事件主动请求数据:

javascript 复制代码
// 主应用监听全局消息
export function listenGlobalEvent() {
  microApp.addGlobalDataListener(data => {
    for (const key of Object.keys(data)) {
      if (key === microAppGlobalEventKey.reqGlobalData) {
        // 子应用请求数据,主应用响应
        globalNotifySubApps(
          microAppGlobalEventKey.ackGlobalData,
          generateGlobalDataFormStore(store)
        );
      }
    }
  });
}

子应用端代码(参考):

javascript 复制代码
// 子应用 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. 清除主应用全局数据

javascript 复制代码
// 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. 通知子应用执行清理

javascript 复制代码
// 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));
      }
    }
  });
}

子应用处理登出事件(参考):

javascript 复制代码
// 子应用监听登出事件
window.microApp.addGlobalDataListener((data) => {
  if (data.reqLogout) {
    // 清除子应用本地数据
    localStorage.clear();
    sessionStorage.clear();

    // 重置子应用状态
    // ...
  }
});

同时,在主应用初始化时配置了 'clear-data': true,这会确保子应用卸载时自动清除 micro-app 内部的缓存数据:

javascript 复制代码
microApp.start({
  'clear-data': true
});

这样就形成了完整的清理链路,确保用户数据不会残留。

子应用容器组件

问题描述:

最初我们直接在路由组件中使用 <micro-app> 标签加载子应用,但随着需求增加,我们需要处理:

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

解决方案:

我们创建了专门的容器组件 subEquipment.vue

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. 初始化配置与生命周期

子应用的初始化需要区分独立运行和嵌入模式,并正确处理生命周期钩子。

入口文件配置

javascript 复制代码
// 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();
  });
}

环境检测方法

javascript 复制代码
// src/micro-common/utils/micro/index.js
export function isMicroAppEnv() {
  return !!window.__MICRO_APP_ENVIRONMENT__;
}

关键点说明:

要点 说明
双模式运行 通过 isMicroAppEnv() 区分独立/嵌入模式
先挂载后判断 避免微前端环境下应用无法启动
事件监听 仅在微前端环境下监听主应用消息
卸载清理 监听 unmount 事件释放资源

生命周期流程:

scss 复制代码
子应用加载
    ↓
mount() 挂载应用
    ↓
判断微前端环境
    ├─ 是 → 注册事件监听
    │       ↓
    │   监听 unmount 事件
    │       ↓
    │   unmount() 卸载清理
    │
    └─ 否 → 正常独立运行

2. 环境检测与路由跳转

子应用在不同环境下需要采用不同的路由跳转策略,以下介绍核心的路由工具方法。

核心工具方法

javascript 复制代码
// 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();

使用示例

javascript 复制代码
// 在子应用组件中使用
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 提供全局通信和局部通信两种方式,子应用需要根据场景选择合适的通信方式。

事件类型定义

javascript 复制代码
// 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' // 页面跳转
};

发送消息方法

javascript 复制代码
// 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);
    }
  });
}

监听消息方法

javascript 复制代码
// 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 路由跳转、业务交互 仅主应用收到

通信流程图:

scss 复制代码
子应用                          主应用
  │                              │
  │  setGlobalData(reqGlobalData) │
  │─────────────────────────────>│
  │                              │ 响应数据
  │  ackGlobalData               │
  │<─────────────────────────────│
  │                              │
  │  dispatch(RouterChange)      │
  │─────────────────────────────>│ 处理路由跳转
  │                              │

4. 状态同步与登出处理

子应用需要与主应用保持关键状态同步,包括用户信息、权限数据和登录状态。

状态同步机制

子应用有两种方式获取主应用状态:

方式一:主应用主动推送

当主应用登录成功或状态变化时,主动向子应用推送数据:

scss 复制代码
主应用                              子应用
  │                                  │
  │ 登录成功                          │
  │                                  │
  │  setGlobalData(ackGlobalData)    │
  │─────────────────────────────────>│
  │                                  │ 更新本地状态
  │                                  │

方式二:子应用主动请求

子应用初始化时主动请求数据,例如在路由守卫中请求权限数据:

javascript 复制代码
// 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();
});

登出处理

登出是最关键的状态同步场景,需要确保所有应用数据正确清理:

javascript 复制代码
// 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 };
}

登出流程图:

css 复制代码
用户点击登出
    │
    ▼
子应用 A 执行登出
    │
    ├─ 清理本地状态
    ├─ 清空全局缓存
    │
    ▼
发送 reqLogout 全局消息
    │
    ├────────────────────┬────────────────────┐
    ▼                    ▼                    ▼
 主应用              子应用 B             子应用 C
    │                    │                    │
 清理主应用状态      handleOtherAppLogout  handleOtherAppLogout
    │                    │                    │
    ▼                    ▼                    ▼
 跳转登录页          重置本地状态          重置本地状态

关键点说明:

要点 说明
双向同步 主应用推送 + 子应用请求,确保数据一致性
登出广播 一个子应用登出,所有应用都收到通知
忽略自身 处理登出消息时需判断是否为自己发送
清理顺序 先清缓存,再发通知,避免状态回写

5. 完整示例与最佳实践

完整初始化示例

javascript 复制代码
// 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);
}

业务组件示例

vue 复制代码
<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 沙箱模式

调试技巧

javascript 复制代码
// 在开发环境开启详细日志
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. 微前端架构初探
相关推荐
ooseabiscuit10 小时前
Laravel6.x核心优化与特性全解析
android·开发语言·javascript
哆啦A梦158811 小时前
20, Springboot3+vue3实现前台轮播图和详情页的设计
javascript·数据库·spring boot·mybatis·vue3
gogoing11 小时前
ESLint 配置字段说明
前端·javascript
Lkstar11 小时前
面试官让我手写 Promise.all / Promise.race / Promise.allSettled,我直接水灵灵地写出来了
javascript·面试
gogoing11 小时前
webpack 的性能优化
前端·javascript
gogoing11 小时前
Node.js 模块查找策略(require 完整流程)
javascript·node.js
gogoing11 小时前
await fetch() 的两阶段设计
前端·javascript
gogoing11 小时前
前端首屏加载优化
前端·javascript
gogoing11 小时前
重排与重绘
前端·javascript
徐小夕12 小时前
100小时,我做了一款AI CAD建模软件,开源!
前端·vue.js·github