一篇能够让你轻易理解微前端到底是个什么东西

2,543 阅读10分钟

什么是微前端

微前端是一种将大型前端应用拆分成多个独立小应用(微应用)的架构模式。

核心要点:

  1. 独立开发部署:每个微应用可单独用不同技术栈(如 React、Vue)开发,独立测试上线,团队分工更灵活。
  2. 集成运行:在同一个页面中,微应用会像 “积木” 一样组合运行,共享导航栏等公共布局,对外呈现为一个完整应用。
  3. 灵活迭代:某个微应用升级时不影响整体系统,资源按需加载也能提升性能。

通俗一点:

是不是听起来就像是一个大的网站中嵌入了其他的网站。

这样你是不是就想到了iframe嵌套呢,那么iframe和微前端有没有关系呢?

iframe 和微前端在 “拆分应用” 的思路上有相似性,但本质上是不同的技术方案,两者的关系可以从关联点差异点两方面分析:

一、两者的关联:iframe 是早期 “微前端” 的朴素实现

  1. 核心思路相似

    • 都通过 “嵌套子应用” 的方式,将多个独立页面整合到主页面中,实现 “整体应用” 的效果。
    • 子应用(iframe 内容)可独立开发、部署,与主应用技术栈解耦。
  2. 历史演进关联

    • 在微前端框架成熟前,iframe 是实现 “前端应用拆分” 的常见方案(如早期管理系统通过 iframe 嵌套不同功能模块)。
    • 微前端的部分设计思想(如 “独立隔离”)借鉴了 iframe 的特性,但通过更底层的技术优化解决了 iframe 的固有缺陷。

二、核心差异:微前端是对 iframe 的系统性升级

维度iframe微前端(现代方案)
渲染机制在主页面中嵌入独立浏览器窗口,与主应用完全隔离。通过 JS 动态加载子应用代码,与主应用共享 DOM 树,渲染在同一页面中。
交互体验- 滚动条、地址栏独立,切换时有 “页面跳转感”。 - 弹窗、全屏等操作受浏览器安全策略限制(如跨域弹窗拦截)。- 与主应用共享 UI 布局(如顶部导航),交互更流畅。 - 可自定义事件机制,实现跨应用通信(如弹窗、状态共享)。
性能表现- 每次加载需完整渲染子页面(包括重复的 HTML、CSS、JS),资源浪费严重。 - 内存占用高,多 iframe 切换可能卡顿。- 按需加载子应用资源(如 JS、CSS),重复依赖可共享(如共用 React 库)。 - 资源加载和渲染更高效,首屏性能更优。
样式与 JS 隔离天然隔离(属于不同浏览器上下文),但无法灵活共享样式。需手动实现隔离(如 CSS Module、Shadow DOM),也可按需共享样式和 JS 逻辑。
搜索引擎优化(SEO)iframe 内容难以被搜索引擎抓取,不利于 SEO。子应用内容与主页面共享 DOM,可正常被爬虫解析(需配合 SSR 等技术)。
跨域限制严格受浏览器同源策略限制,主应用与 iframe 通信需通过postMessage,功能受限(如无法直接操作 iframe DOM)。子应用与主应用同域时,可直接通过 JS API 通信(如访问 DOM、共享状态),跨域场景也可通过约定机制处理。

三、微前端对 iframe 缺陷的优化

  1. 解决 “隔离过度” 问题

    • iframe 的完全隔离导致主应用与子应用交互困难(如无法统一顶部导航的选中状态),而微前端通过共享 DOM 和通信机制,让子应用与主应用更紧密协同。
  2. 优化性能与体验

    • iframe 每次加载都相当于打开一个新页面,而微前端通过动态资源加载组件级渲染,避免重复加载公共资源(如 React 框架),提升加载速度和内存利用率。
  3. 增强可维护性

    • 微前端框架(如 qiankun、Single-SPA)提供了标准化的生命周期管理(如mountunmount)和路由规则,而 iframe 需要手动处理 URL 同步、状态存储等复杂逻辑,维护成本高。

四、何时仍会用 iframe?

尽管微前端更先进,但 iframe 在以下场景仍有价值:

  • 嵌入第三方应用:如嵌入地图、广告等外部服务(无法修改其代码,只能通过 iframe 隔离)。
  • 快速原型开发:无需复杂集成,直接嵌套现有独立页面,适合临时需求。
  • 严格隔离场景:如金融系统中需隔离敏感操作模块,iframe 的天然隔离性更安全。

一句话就是:

iframe 是微前端的 “雏形”,但微前端通过更底层的技术方案(动态资源加载、共享状态管理、组件级渲染)解决了 iframe 的性能、交互和维护问题。两者的关系类似 “马车” 与 “汽车”—— 目标都是 “运输”,但实现方式和效率天差地别。

以微前端框架qiankun为例简单介绍下

乾坤(qiankun)是蚂蚁集团开源的微前端框架,基于 Single-SPA 封装,提供了更易用的 API 和更完善的生态支持,帮助开发者快速搭建微前端架构。以下是其核心特点和功能的简要介绍:

核心特性

  1. 技术栈无关
    主应用和子应用可使用不同技术栈(如 React、Vue、Angular),支持渐进式升级。

  2. 零配置接入
    子应用无需修改代码,只需遵循乾坤的生命周期协议(如 mountunmount)即可接入主应用。

  3. 样式隔离
    自动处理 CSS 冲突,支持 Shadow DOM 和样式前缀两种隔离模式,避免子应用样式影响全局。

  4. JS 沙箱
    通过 Proxy 实现运行时 JS 隔离,防止子应用修改全局变量(如 window 对象),支持快照沙箱和代理沙箱两种模式。

  5. 资源预加载
    支持在空闲时间预加载子应用资源,提升后续访问速度。

  6. 应用间通信
    提供全局状态管理工具(initGlobalState),方便主应用与子应用、子应用与子应用之间的数据传递。

什么叫与技术栈无关呢?

我们先来看下一个简单的微前端组成架构:

image.png

这里的架构可以是多样的:

可以是如上面的构架这样:

image.png

或者这样:

image.png

有兴趣的小伙伴可以从码云上下载到本地简单调试下:点击克隆下载

那么以上面main-app的目录结构为例,运行流程是怎样的呢?

image.png

(以克隆下载的代码为例) 既然有两个子应用,我们就需要配置两个子应用在主应用中配置:

我们的主容器如下:

image.png

对两个子应用进行注册

image.png

registerMicroApps参数简单介绍
参数必选作用
name子应用唯一标识
entry子应用入口地址
container子应用渲染容器
activeRule子应用激活规则
props传递给子应用的数据和方法
loader子应用加载状态监听
生命周期钩子子应用加载、挂载、卸载阶段的全局回调函数

完整的代码示例如下:

registerMicroApps(
  [
    {
      name: 'vueApp',
      entry: '//localhost:8081',
      container: '#subapp-container',
      activeRule: '/vue',
      props: {
        onLogin: (user) => console.log('主应用登录回调', user),
      },
      loader: (loading) => {
        document.getElementById('app-loading').style.display = loading ? 'block' : 'none';
      },
    },
  ],
  {
    beforeLoad: (app) => {
      console.log('准备加载子应用:', app.name);
      return Promise.resolve();
    },
    afterMount: (app) => {
      console.log('子应用挂载成功:', app.name);
    },
  }
);

生命周期钩子作用如下:

1. beforeLoad

  • 触发时机:子应用资源加载前。
  • 用途:可用于加载子应用前的准备工作(如权限校验)。

2. beforeMount

  • 触发时机:子应用挂载到 DOM 前。
  • 用途:可用于修改子应用 props 或 DOM 操作。

3. afterMount

  • 触发时机:子应用挂载完成后。
  • 用途:可用于埋点统计或通知其他子应用。

4. beforeUnmount

  • 触发时机:子应用卸载前。
  • 用途:可用于子应用状态保存。

5. afterUnmount

  • 触发时机:子应用卸载完成后。
  • 用途:可用于资源清理。

既然存在两个子应用,那么他们之间的通信是怎样的呢?

主应用--->子应用

image.png

一、主应用代码(Vue 示例)
// 主应用入口文件(main.js)
import { registerMicroApps, start } from 'qiankun';

// 注册子应用并传递 props
registerMicroApps([
  {
    name: 'child-app',
    entry: '//localhost:8081', // 子应用地址
    container: '#subapp-container',
    activeRule: '/child',
    props: {
      // 1. 传递静态数据
      user: { id: 1, name: 'doubao' },
      
      // 2. 传递方法(供子应用调用)
      logout: () => {
        console.log('主应用:执行登出逻辑');
        // 主应用登出操作(如清除 token)
      },
      
      // 3. 传递状态更新函数
      updateTheme: (theme) => {
        console.log('主应用:更新主题为', theme);
        // 更新主应用全局主题
      },
    },
  },
]);

start();
二、子应用代码(Vue 示例)
// 子应用入口文件(public-path.js)
export async function mount(props) {
  // 1. 接收主应用传递的 props
  console.log('子应用接收到的 props:', props);
  
  // 2. 使用 props 数据
  const { user, logout, updateTheme } = props;
  console.log('当前用户:', user.name); // 输出: doubao
  
  // 3. 调用主应用方法
  document.getElementById('logout-btn').addEventListener('click', logout);
  
  // 4. 触发主应用状态更新
  document.getElementById('theme-btn').addEventListener('click', () => {
    updateTheme('dark');
  });
}

export async function unmount() {
  // 子应用卸载时的清理工作
}
三、子应用接收 props 的其他方式
<template>
  <div>
    <p>当前用户: {{ user.name }}</p>
    <button @click="logout">退出登录</button>
  </div>
</template>

<script>
export default {
  mounted() {
    if (window.__POWERED_BY_QIANKUN__) {
      // 从全局获取 props(需在 mount 钩子中保存)
      this.user = window.mainProps.user;
      this.logout = window.mainProps.logout;
    }
  },
};
</script>

子应用--->主应用

image.png

1. 主应用:注册事件监听
// 主应用入口文件(如 main.js)
import { registerMicroApps } from 'qiankun';

registerMicroApps([
  {
    name: 'child-app',
    entry: '//localhost:8081',
    container: '#subapp-container',
    activeRule: '/child',
    props: {
      // 向子应用传递事件监听函数
      onLogin: (userInfo) => {
        console.log('主应用接收到登录信息:', userInfo);
        // 更新主应用状态或执行其他逻辑
      },
    },
  },
]);
1. 子应用:触发事件
// 子应用登录组件(Vue 示例)
export default {
  methods: {
    async handleLogin() {
      // 登录逻辑...
      const userInfo = { id: 1, name: 'doubao' };
      
      // 方式一:通过 props 传递的方法(推荐)
      if (window.__POWERED_BY_QIANKUN__) {
        this.$props.onLogin(userInfo);
      }
      
      // 方式二:全局事件总线(需主应用预先注册监听)
      window.__POWERED_BY_QIANKUN__.emit('login', userInfo);
    },
  },
};
子应用<--->子应用

image.png

1. 主应用:注册事件
/ 主应用:注册事件监听
registerMicroApps([
  {
    name: 'appA',
    props: {
      sendToB: (data) => {
        // 通过全局事件通知子应用B
        window.eventBus.emit('fromA', data);
      },
    },
  },
  {
    name: 'appB',
    props: {
      onReceive: (data) => {
        console.log('appB 收到消息:', data);
      },
    },
  },
]);

// 主应用:创建全局事件总线
window.eventBus = {
  listeners: {},
  on(event, callback) {
    this.listeners[event] = this.listeners[event] || [];
    this.listeners[event].push(callback);
  },
  emit(event, data) {
    this.listeners[event]?.forEach(callback => callback(data));
  },
};
1. 子应用A:发送消息
// 子应用A:发送消息
export async function mount(props) {
  props.sendToB({ message: 'Hello from AppA' });
}
1. 子应用B:接收消息
// 子应用B:接收消息
export async function mount(props) {
  // 方式一:通过主应用传递的回调
  props.onReceive = (data) => {
    console.log('收到主应用转发的消息:', data);
  };
  
  // 方式二:直接监听全局事件
  window.eventBus.on('fromA', (data) => {
    console.log('收到AppA的消息:', data);
  });
}

是不是有小伙伴看到 window.POWERED_BY_QIANKUN,不知道在哪里注册的呢?

window.__POWERED_BY_QIANKUN__ 是乾坤框架在加载子应用时动态注入的全局变量,其注册逻辑位于乾坤的核心代码中,而非用户项目文件。以下是详细说明:

1. 注入时机

当主应用通过乾坤加载子应用时,框架会在子应用代码执行前注入该变量。具体流程:

  1. 主应用调用 registerMicroApps 注册子应用配置
  2. 当路由匹配到子应用的 activeRule 时,乾坤开始加载子应用资源
  3. 在子应用的 JS 代码执行前,乾坤会在子应用的运行环境中注入 window.__POWERED_BY_QIANKUN__ = true
  4. 子应用代码执行时,可通过检测该变量判断自身运行环境
2. 源码位置

在乾坤框架的源码中,该变量的注入逻辑位于:

plaintext

qiankun/src/loadApp.ts

那么乾坤在实际开发中有哪些缺点呢?

一、性能与加载问题

  1. 首屏加载缓慢

    • 原因:主应用需加载多个子应用的资源(JS、CSS),尤其是未优化的大型子应用。

    • 案例:某电商平台集成 3 个子应用后,首屏加载时间从 2s 增至 5s。

    • 解决方案

      • 子应用按需加载(懒加载)。
      • 使用 CDN 加速静态资源。
      • 提取公共依赖(如 React、Vue)为共享资源。
  2. 频繁切换子应用卡顿

    • 原因:子应用切换时需卸载当前沙箱并初始化新沙箱,涉及大量 DOM 操作和 JS 执行。

    • 案例:管理系统中频繁切换不同业务模块时,有明显延迟感。

    • 解决方案

      • 预加载子应用(prefetch 配置)。
      • 使用内存缓存(如保留不活跃子应用的状态)。

二、兼容性与集成问题

  1. 第三方库冲突

    • 问题

      • 不同子应用使用同一库的不同版本(如 React 16 和 React 17)。
      • 全局插件(如 window.$)被多个子应用覆盖。
    • 案例:主应用使用 jQuery 3.5,子应用使用 jQuery 3.6,导致部分功能异常。

    • 解决方案

      • 统一依赖版本(如强制所有子应用使用 React 17)。
      • 使用 externals 配置排除重复依赖。
  2. 样式隔离不彻底

    • 问题

      • CSS 沙箱无法隔离动态生成的样式(如 document.createElement('style'))。
      • 第三方组件库(如 Ant Design)样式冲突。
    • 案例:子应用 A 的模态框样式被主应用的全局样式覆盖。

    • 解决方案

      • 使用 strictStyleIsolation: true(基于 Shadow DOM)。
      • 为子应用添加唯一前缀(如 .app1-modal)。

三、开发与调试复杂度

  1. 多项目管理困难

    • 问题

      • 主应用与多个子应用的版本管理和依赖升级复杂。
      • 开发环境启动多个服务(如主应用 + 3 个子应用)耗时。
    • 案例:某企业系统包含 10 个子应用,每次全量启动需 5 分钟。

    • 解决方案

      • 使用 Monorepo 管理多项目(如 Lerna、Yarn Workspaces)。
      • 开发时仅启动必要的子应用。
  2. 调试工具不足

    • 问题

      • 浏览器开发者工具难以区分主应用和子应用的堆栈信息。
      • 沙箱环境掩盖真实错误(如 window 变量被拦截导致的未定义错误)。
    • 案例:子应用报错 Cannot read property 'xxx' of undefined,实际是沙箱隔离问题。

    • 解决方案

      • 在开发环境关闭沙箱隔离(sandbox: false)。
      • 使用乾坤提供的调试 API(如 window.__QIAN_KUN_DEV_TOOLS__)。

四、部署与运维挑战

  1. 版本兼容性问题

    • 问题

      • 主应用与子应用的版本不兼容(如主应用升级乾坤版本后,旧子应用无法正常加载)。
      • 子应用间的依赖冲突(如子应用 A 依赖 lodash 4.x,子应用 B 依赖 lodash 5.x)。
    • 案例:主应用升级到乾坤 2.0 后,部分子应用无法卸载。

    • 解决方案

      • 严格控制版本升级流程(如灰度发布、版本兼容性测试)。
      • 使用依赖锁定工具(如 package-lock.json)。
  2. 监控与告警困难

    • 问题

      • 难以定位跨应用错误(如子应用崩溃导致主应用白屏)。
      • 缺乏统一的性能监控(如子应用切换耗时、资源加载失败)。
    • 案例:生产环境中,子应用 B 的某个 API 调用失败导致整个页面无响应,但日志未明确报错位置。

    • 解决方案

      • 集成统一的错误监控系统(如 Sentry)。

      • 在乾坤生命周期钩子中添加性能埋点:

        javascript

        registerMicroApps(
          apps,
          {
            afterMount: (app) => {
              console.timeEnd(`子应用${app.name}加载耗时`);
            },
          }
        );
        

官网地址:qiankun.umijs.org/zh