第3章:qiankun框架入门:核心原理、主应用与子应用通信、生命周期
3.1 qiankun到底解决了什么问题?
说实话,我第一次接触微前端的时候,脑子里全是问号。
“不就是把几个前端项目拼在一起吗?iframe不就行了?”
嗯,我当时也是这么想的。直到我在一个真实项目中,用iframe嵌入了三个不同技术栈的子应用——结果呢?用户体验一塌糊涂。URL不同步、全局弹窗被遮挡、登录状态要各自维护……说白了,iframe就像把几个独立网站硬塞进一个页面,它们之间几乎没有沟通。
qiankun要解决的,正是这个痛点。它让多个独立的前端应用,能像“插件”一样插到一个主应用里,共享一个浏览器窗口、一套路由、一套全局状态。而且每个子应用依然可以独立开发、独立部署。
核心价值一句话:qiankun = 微前端框架 = 让多个SPA应用共存于一个页面,且互不干扰。
3.2 qiankun的核心原理:沙箱与加载
qiankun的核心原理,我总结为三个关键词:加载、隔离、通信。
3.2.1 加载子应用
qiankun通过registerMicroApps注册子应用,然后调用start启动。它本质上是一个“应用加载器”。
它会根据当前路由,动态加载子应用的入口HTML。然后解析HTML中的JS和CSS,把它们放到一个隔离的环境中执行。
// 主应用注册子应用
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'react-app', // 子应用名称,唯一
entry: '//localhost:3001', // 子应用入口地址
container: '#sub-app', // 挂载的DOM节点
activeRule: '/react', // 激活规则(路由匹配)
},
{
name: 'vue-app',
entry: '//localhost:3002',
container: '#sub-app',
activeRule: '/vue',
},
]);
start(); // 启动qiankun
我在项目中遇到过一个问题:子应用的入口地址写成了相对路径,结果部署到生产环境后,资源加载404。后来我统一用绝对路径,或者通过环境变量动态拼接,才彻底解决。
3.2.2 沙箱隔离
这是qiankun最核心的能力。它要保证子应用之间、子应用与主应用之间,不互相污染全局变量。
qiankun用了两种沙箱策略:
- Proxy沙箱(现代浏览器):通过Proxy拦截window对象的读写操作,每个子应用拥有自己的“虚拟window”。
- 快照沙箱(IE兼容):在子应用激活时,记录当前window状态;失活时,恢复原始状态。
我的建议:如果你的项目不需要兼容IE,直接用Proxy沙箱。性能更好,隔离更彻底。我曾经在IE11上踩过快照沙箱的坑——某些第三方库会偷偷修改window,导致恢复时出错。
3.2.3 样式隔离
qiankun默认使用Shadow DOM或scoped CSS来做样式隔离。但说实话,Shadow DOM在某些场景下会有问题,比如弹窗组件会逃逸出Shadow边界。
我个人更推荐的做法是:子应用自己约定CSS命名空间,比如所有类名加前缀.react-app-。qiankun的样式隔离作为兜底方案。
3.3 主应用与子应用通信
通信是微前端里绕不开的话题。qiankun提供了两种官方方式:
3.3.1 基于props传递
主应用可以在注册子应用时,通过props传递数据。子应用在生命周期钩子里接收。
// 主应用
registerMicroApps([
{
name: 'react-app',
entry: '//localhost:3001',
container: '#sub-app',
activeRule: '/react',
props: {
userInfo: { name: '张三', role: 'admin' },
onLogout: () => { /* 退出登录逻辑 */ },
},
},
]);
// 子应用(React)
export async function mount(props) {
const { userInfo, onLogout } = props;
ReactDOM.render(<App userInfo={userInfo} onLogout={onLogout} />, container);
}
这种方式适合“主应用主动给子应用传数据”。我在项目中常用它来传递用户登录态、主题配置等全局信息。
3.3.2 基于全局事件总线
qiankun官方推荐使用initGlobalState来创建全局状态。主应用和所有子应用都能监听和修改这个状态。
// 主应用
import { initGlobalState } from 'qiankun';
const actions = initGlobalState({
token: '',
theme: 'light',
});
actions.onGlobalStateChange((state, prev) => {
console.log('全局状态变了:', state, prev);
});
// 子应用
export async function mount(props) {
props.onGlobalStateChange((state, prev) => {
console.log('子应用监听到全局状态变化:', state);
}, true); // 第二个参数为true,表示立即执行一次回调
props.setGlobalState({ theme: 'dark' }); // 子应用也可以修改全局状态
}
注意:全局状态是“共享”的,不是“隔离”的。所有子应用都能读写。我曾经见过一个团队,把大量业务数据塞进全局状态,结果子应用之间互相覆盖,排查了整整一天。我的建议是:全局状态只放“基础设施”数据,比如用户token、主题、语言等。业务数据走各自子应用的内部状态管理。
3.4 生命周期详解
qiankun为每个子应用定义了三个生命周期钩子:bootstrap、mount、unmount。还有一个可选的update。
| 生命周期 | 触发时机 | 典型用途 |
|---|---|---|
bootstrap |
子应用第一次加载时,只执行一次 | 初始化全局配置、加载公共资源 |
mount |
每次子应用激活时 | 渲染DOM、监听路由、获取数据 |
unmount |
每次子应用失活时 | 销毁DOM、清除定时器、取消事件监听 |
update |
可选,手动调用时 | 更新子应用状态(较少使用) |
子应用需要导出这些生命周期函数。以React为例:
// 子应用入口文件
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
// 渲染函数
function render(props) {
const { container } = props;
ReactDOM.render(
<App />,
container ? container.querySelector('#root') : document.getElementById('root')
);
}
// 生命周期 - 初始化
export async function bootstrap() {
console.log('子应用初始化');
// 可以在这里做一次性的初始化,比如加载公共样式
}
// 生命周期 - 挂载
export async function mount(props) {
console.log('子应用挂载', props);
render(props);
}
// 生命周期 - 卸载
export async function unmount(props) {
console.log('子应用卸载');
const { container } = props;
ReactDOM.unmountComponentAtNode(
container ? container.querySelector('#root') : document.getElementById('root')
);
}
避坑指南:我曾经在unmount里忘记清除定时器,结果子应用切走后,定时器还在跑,不断报错。后来我养成了一个习惯:所有定时器、事件监听、WebSocket连接,都在unmount里清理干净。你想想看,如果用户打开了10个子应用,每个都留下几个定时器……那页面迟早卡死。
3.5 一个完整的通信示例
最后,我分享一个我在项目中实际用过的通信模式。主应用传递一个“事件总线”给子应用,子应用通过它来发布和订阅事件。
// 主应用
import { registerMicroApps, start, initGlobalState } from 'qiankun';
const actions = initGlobalState({});
// 自定义事件总线
const eventBus = {
listeners: {},
on(event, callback) {
if (!this.listeners[event]) this.listeners[event] = [];
this.listeners[event].push(callback);
},
emit(event, data) {
(this.listeners[event] || []).forEach(cb => cb(data));
},
off(event, callback) {
if (!callback) delete this.listeners[event];
else this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
},
};
registerMicroApps([
{
name: 'react-app',
entry: '//localhost:3001',
container: '#sub-app',
activeRule: '/react',
props: { actions, eventBus },
},
]);
start();
// 子应用
export async function mount(props) {
const { eventBus } = props;
// 订阅事件
eventBus.on('order:created', (orderData) => {
console.log('收到订单创建事件:', orderData);
// 更新子应用UI
});
// 发布事件
eventBus.emit('user:logout', { reason: 'token expired' });
}
这种模式的好处是:通信逻辑清晰,事件名一目了然。而且子应用之间不需要知道彼此的存在,完全通过事件解耦。
嗯,这一章的内容就到这里。qiankun的核心原理、通信方式和生命周期,说白了就是三个字:加载、隔离、通信。下一章我会带你手写一个qiankun的简易实现,让你彻底搞懂它背后的机制。
课后思考:如果子应用A和子应用B需要共享一个复杂的状态(比如购物车数据),你会怎么设计通信方案?用全局状态?事件总线?还是其他方式?