什么是微前端
微前端是一种将大型前端应用拆分成多个独立小应用(微应用)的架构模式。
核心要点:
- 独立开发部署:每个微应用可单独用不同技术栈(如 React、Vue)开发,独立测试上线,团队分工更灵活。
- 集成运行:在同一个页面中,微应用会像 “积木” 一样组合运行,共享导航栏等公共布局,对外呈现为一个完整应用。
- 灵活迭代:某个微应用升级时不影响整体系统,资源按需加载也能提升性能。
通俗一点:
是不是听起来就像是一个大的网站中嵌入了其他的网站。
这样你是不是就想到了iframe嵌套呢,那么iframe和微前端有没有关系呢?
iframe 和微前端在 “拆分应用” 的思路上有相似性,但本质上是不同的技术方案,两者的关系可以从关联点和差异点两方面分析:
一、两者的关联:iframe 是早期 “微前端” 的朴素实现
-
核心思路相似:
- 都通过 “嵌套子应用” 的方式,将多个独立页面整合到主页面中,实现 “整体应用” 的效果。
- 子应用(iframe 内容)可独立开发、部署,与主应用技术栈解耦。
-
历史演进关联:
- 在微前端框架成熟前,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 缺陷的优化
-
解决 “隔离过度” 问题:
- iframe 的完全隔离导致主应用与子应用交互困难(如无法统一顶部导航的选中状态),而微前端通过共享 DOM 和通信机制,让子应用与主应用更紧密协同。
-
优化性能与体验:
- iframe 每次加载都相当于打开一个新页面,而微前端通过动态资源加载和组件级渲染,避免重复加载公共资源(如 React 框架),提升加载速度和内存利用率。
-
增强可维护性:
- 微前端框架(如 qiankun、Single-SPA)提供了标准化的生命周期管理(如
mount、unmount)和路由规则,而 iframe 需要手动处理 URL 同步、状态存储等复杂逻辑,维护成本高。
- 微前端框架(如 qiankun、Single-SPA)提供了标准化的生命周期管理(如
四、何时仍会用 iframe?
尽管微前端更先进,但 iframe 在以下场景仍有价值:
- 嵌入第三方应用:如嵌入地图、广告等外部服务(无法修改其代码,只能通过 iframe 隔离)。
- 快速原型开发:无需复杂集成,直接嵌套现有独立页面,适合临时需求。
- 严格隔离场景:如金融系统中需隔离敏感操作模块,iframe 的天然隔离性更安全。
一句话就是:
iframe 是微前端的 “雏形”,但微前端通过更底层的技术方案(动态资源加载、共享状态管理、组件级渲染)解决了 iframe 的性能、交互和维护问题。两者的关系类似 “马车” 与 “汽车”—— 目标都是 “运输”,但实现方式和效率天差地别。
以微前端框架qiankun为例简单介绍下
乾坤(qiankun)是蚂蚁集团开源的微前端框架,基于 Single-SPA 封装,提供了更易用的 API 和更完善的生态支持,帮助开发者快速搭建微前端架构。以下是其核心特点和功能的简要介绍:
核心特性
-
技术栈无关
主应用和子应用可使用不同技术栈(如 React、Vue、Angular),支持渐进式升级。 -
零配置接入
子应用无需修改代码,只需遵循乾坤的生命周期协议(如mount、unmount)即可接入主应用。 -
样式隔离
自动处理 CSS 冲突,支持 Shadow DOM 和样式前缀两种隔离模式,避免子应用样式影响全局。 -
JS 沙箱
通过 Proxy 实现运行时 JS 隔离,防止子应用修改全局变量(如window对象),支持快照沙箱和代理沙箱两种模式。 -
资源预加载
支持在空闲时间预加载子应用资源,提升后续访问速度。 -
应用间通信
提供全局状态管理工具(initGlobalState),方便主应用与子应用、子应用与子应用之间的数据传递。
什么叫与技术栈无关呢?
我们先来看下一个简单的微前端组成架构:
这里的架构可以是多样的:
可以是如上面的构架这样:
或者这样:
有兴趣的小伙伴可以从码云上下载到本地简单调试下:点击克隆下载
那么以上面main-app的目录结构为例,运行流程是怎样的呢?
(以克隆下载的代码为例) 既然有两个子应用,我们就需要配置两个子应用在主应用中配置:
我们的主容器如下:
对两个子应用进行注册
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
- 触发时机:子应用卸载完成后。
- 用途:可用于资源清理。
既然存在两个子应用,那么他们之间的通信是怎样的呢?
主应用--->子应用
一、主应用代码(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>
子应用--->主应用
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);
},
},
};
子应用<--->子应用
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. 注入时机
当主应用通过乾坤加载子应用时,框架会在子应用代码执行前注入该变量。具体流程:
- 主应用调用
registerMicroApps注册子应用配置 - 当路由匹配到子应用的
activeRule时,乾坤开始加载子应用资源 - 在子应用的 JS 代码执行前,乾坤会在子应用的运行环境中注入
window.__POWERED_BY_QIANKUN__ = true - 子应用代码执行时,可通过检测该变量判断自身运行环境
2. 源码位置
在乾坤框架的源码中,该变量的注入逻辑位于:
plaintext
qiankun/src/loadApp.ts
那么乾坤在实际开发中有哪些缺点呢?
一、性能与加载问题
-
首屏加载缓慢
-
原因:主应用需加载多个子应用的资源(JS、CSS),尤其是未优化的大型子应用。
-
案例:某电商平台集成 3 个子应用后,首屏加载时间从 2s 增至 5s。
-
解决方案:
- 子应用按需加载(懒加载)。
- 使用 CDN 加速静态资源。
- 提取公共依赖(如 React、Vue)为共享资源。
-
-
频繁切换子应用卡顿
-
原因:子应用切换时需卸载当前沙箱并初始化新沙箱,涉及大量 DOM 操作和 JS 执行。
-
案例:管理系统中频繁切换不同业务模块时,有明显延迟感。
-
解决方案:
- 预加载子应用(
prefetch配置)。 - 使用内存缓存(如保留不活跃子应用的状态)。
- 预加载子应用(
-
二、兼容性与集成问题
-
第三方库冲突
-
问题:
- 不同子应用使用同一库的不同版本(如 React 16 和 React 17)。
- 全局插件(如
window.$)被多个子应用覆盖。
-
案例:主应用使用 jQuery 3.5,子应用使用 jQuery 3.6,导致部分功能异常。
-
解决方案:
- 统一依赖版本(如强制所有子应用使用 React 17)。
- 使用 externals 配置排除重复依赖。
-
-
样式隔离不彻底
-
问题:
- CSS 沙箱无法隔离动态生成的样式(如
document.createElement('style'))。 - 第三方组件库(如 Ant Design)样式冲突。
- CSS 沙箱无法隔离动态生成的样式(如
-
案例:子应用 A 的模态框样式被主应用的全局样式覆盖。
-
解决方案:
- 使用
strictStyleIsolation: true(基于 Shadow DOM)。 - 为子应用添加唯一前缀(如
.app1-modal)。
- 使用
-
三、开发与调试复杂度
-
多项目管理困难
-
问题:
- 主应用与多个子应用的版本管理和依赖升级复杂。
- 开发环境启动多个服务(如主应用 + 3 个子应用)耗时。
-
案例:某企业系统包含 10 个子应用,每次全量启动需 5 分钟。
-
解决方案:
- 使用 Monorepo 管理多项目(如 Lerna、Yarn Workspaces)。
- 开发时仅启动必要的子应用。
-
-
调试工具不足
-
问题:
- 浏览器开发者工具难以区分主应用和子应用的堆栈信息。
- 沙箱环境掩盖真实错误(如
window变量被拦截导致的未定义错误)。
-
案例:子应用报错
Cannot read property 'xxx' of undefined,实际是沙箱隔离问题。 -
解决方案:
- 在开发环境关闭沙箱隔离(
sandbox: false)。 - 使用乾坤提供的调试 API(如
window.__QIAN_KUN_DEV_TOOLS__)。
- 在开发环境关闭沙箱隔离(
-
四、部署与运维挑战
-
版本兼容性问题
-
问题:
- 主应用与子应用的版本不兼容(如主应用升级乾坤版本后,旧子应用无法正常加载)。
- 子应用间的依赖冲突(如子应用 A 依赖 lodash 4.x,子应用 B 依赖 lodash 5.x)。
-
案例:主应用升级到乾坤 2.0 后,部分子应用无法卸载。
-
解决方案:
- 严格控制版本升级流程(如灰度发布、版本兼容性测试)。
- 使用依赖锁定工具(如
package-lock.json)。
-
-
监控与告警困难
-
问题:
- 难以定位跨应用错误(如子应用崩溃导致主应用白屏)。
- 缺乏统一的性能监控(如子应用切换耗时、资源加载失败)。
-
案例:生产环境中,子应用 B 的某个 API 调用失败导致整个页面无响应,但日志未明确报错位置。
-
解决方案:
-
集成统一的错误监控系统(如 Sentry)。
-
在乾坤生命周期钩子中添加性能埋点:
javascript
registerMicroApps( apps, { afterMount: (app) => { console.timeEnd(`子应用${app.name}加载耗时`); }, } );
-
-