一图吃透Qiankun原理详细
一. 相比single-spa的Qiankun封装能力
心中熟知这四点主要差异,很容易理清并记忆qiankun的思路。
- 封装了registerApplication。注册应用时,不用手动编写拉取子应用代码资源,并返回接入协议。
- 封装了loadApp方法,封装了css沙箱和js沙箱,复写了一些dom、计时器和事件监听api。
- 封装了子应用返回的接入协议。可以注销沙箱,并释放计时器和事件监听。
- 封装了start方法。乾坤的start可以在首个子应用渲染完成后兼浏览器空闲时,对NOT_LOADED的子应用进行资源加载。
二. 流程图
三. 详解加载
qiankun封装的start其实只做了两件事:预加载未load的子应用、调用single-spa的start。
重点是预加载未load的子应用。
具体要预加载子应用的什么? 要预加载的东西是子应用的html,其实就是用import-html去加载还未load的子应用。
何时预加载? 在single-spa完成第一个子应用挂载后的浏览器空闲时。
如何判断 single-spa 挂载完第一个子应用? single-spa会创建一个customerEvent,用addEventListner('single-spa:first-mount', listeners)
即可实现。
如何在listeners中找到浏览器空闲时? 借助 js 的原生方法 requestIdleCallback()
requestIdleCallback(async () => {
const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, opts);
requestIdleCallback(getExternalStyleSheets);
requestIdleCallback(getExternalScripts);
});
四. 详解沙箱
css 沙箱
- 在实验性模式下,就会对外联的link和内嵌的style标签中的内容做前缀处理。
js 沙箱
处理动态css和动态js
所谓动态样式或者动态脚本,本质上都是通过dom原生方法,将script和link或者style标签插入到head或者body中。因此只需要对dom原生方法做包装即可。
乾坤对一些关键的dom操作方法都进行了包装,操作逻辑很多,这里只研究通过 head.appendChild,body.appendChild,head.insertBefore 创建的动态css和动态js。
- 对于动态css而言,新增的可能是link可能是style标签。无论如何,他们的样式都需要经过隔离处理。
- 对于js而言,新增的只能是script标签,但script标签中的内容只有两种,一种是src,一种是直接包含js脚本的,他们的执行都需要在沙箱中进行。
下图流程展示了,对 head和body的appendChild以及insertBefore的包装核心逻辑:
拦截监听事件
子应用的dom卸载掉,js沙箱关闭,但先前js运行时创建的全局事件监听器还遗留在了全局环境中,为了在子应用卸载时,同时删除所有由子应用创建的事件监听器,防止内存泄露。
思路就是建立一个 type-listeners Map,在包装的事件方法中,维护这个Map。
最后要提供一个 free 方法,以能一次性卸载和子应用相关的所有事件监听。
function patch (global) {
const listenerMap = new Map()
const rawAddEventListener = window.addEventListener
const rawRemoveEventListener = window.removeEventListener
// 包装 window.addEventListener
global.addEventListener = (type, listener, options) => {
const listeners = listenerMap.get(type) || []
listenersMap.set(type, [...listeners, listener])
return rawAddEventListener.addEventListener.call(window, type, listener, options)
}
// 包装 window.removeEventListener
global.removeEventListener = (type, listener, options) => {
const storedTypeListeners = listenerMap.get(type)
if (storedTypeListeners && storedTypeListeners.length && storedTypeListeners.indexOf(listener) !== -1) {
// 说明当前listener有被注册过,现在要从set中删除它
storedTypeListeners.splice(storedTypeListeners.indexOf(listener), 1)
}
return rawRemoveEventListener.call(window, type, listener, options)
}
return function free () {
listeners.forEach((listeners, type) => {
// 调用包装后的remove方法删除所有子应用已经注册的type-listener
[...listeners].forEach((listener) => global.removeEventListener(type, listener))
})
// 还原global上的事件监听器至原生 (这一步我认为也没必要这么做,不懂为什么?)
global.addEventListener = rawAddEventListener
global.removeEventListener = rawRemoveEventListener
}
}
拦截计时器
类似于事件监听器的拦截。只不过储存的是计时器的id,也需要返回free方法。
const rawWindowInterval = window.setInterval;
const rawWindowClearInterval = window.clearInterval;
export default function patch(global: Window) {
let intervals: number[] = [];
global.clearInterval = (intervalId: number) => {
intervals = intervals.filter((id) => id !== intervalId);
return rawWindowClearInterval.call(window, intervalId as any);
};
global.setInterval = (handler: CallableFunction, timeout?: number, ...args: any[]) => {
const intervalId = rawWindowInterval(handler, timeout, ...args);
intervals = [...intervals, intervalId];
return intervalId;
};
return function free() {
intervals.forEach((id) => global.clearInterval(id));
global.setInterval = rawWindowInterval;
global.clearInterval = rawWindowClearInterval;
return noop;
};
}
转载自:https://juejin.cn/post/7369120920146886691