likes
comments
collection
share

一图吃透Qiankun原理详细

作者站长头像
站长
· 阅读数 36

一. 相比single-spa的Qiankun封装能力

心中熟知这四点主要差异,很容易理清并记忆qiankun的思路。

  1. 封装了registerApplication。注册应用时,不用手动编写拉取子应用代码资源,并返回接入协议。
  2. 封装了loadApp方法,封装了css沙箱和js沙箱,复写了一些dom、计时器和事件监听api。
  3. 封装了子应用返回的接入协议。可以注销沙箱,并释放计时器和事件监听。
  4. 封装了start方法。乾坤的start可以在首个子应用渲染完成后兼浏览器空闲时,对NOT_LOADED的子应用进行资源加载。

二. 流程图

一图吃透Qiankun原理详细

三. 详解加载

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 沙箱

  1. 在实验性模式下,就会对外联的link和内嵌的style标签中的内容做前缀处理。

一图吃透Qiankun原理详细

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的包装核心逻辑

一图吃透Qiankun原理详细

拦截监听事件

子应用的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
评论
请登录