关于qiankun:清晰简单易懂的qiankun主流程分析

微前端系列之:
一、记一次微前端技术选型
二、清晰简略易懂的qiankun主流程剖析
三、记一次落地qiankun

本文是系列之二。

综述

qiankun 是在 single-spa 根底上进行二次开发的。本文外围剖析利用加载利用切换利用隔离 这三个外围性能原理。

一开始打算是间接从源码角度上介绍流程和原理的,但会导致篇幅过长,且重点抓不住。

所以打算把一些主流程间接用流程图和文字说明,一些外围实现,能够再联合原来来解读。

残缺流程图:https://github.com/Rockergmai…

主流程

路由劫持

路由劫持性能,是在 single-spa 实现的。

hashchange / popstate 全局事件做监听,触发 reroute 办法。

window.addEventListener window.removeEventListener window.history.pushState window.history.replaceState做劫持。

外围性能是 reroute,下文会重点介绍,这个办法被好几个办法都调用了。

注册微利用

用户调用 qiankun 提供的 registerMicroApps 注册子利用。它其实是调用了 single-sparegisterApplication 来进行注册的,其中的 appOrLoadApp 这个选项用来传入加载微利用那个的逻辑。当路由匹配 activeWhen 时,会用来加载微利用。

qiankun 帮咱们实现了加载微利用的逻辑,是通过 loadApp 办法来加载的,其外围是应用到了 import-html-entry 这个库来实现的。

最初还是会调用 reroute 办法。

启动qiankun

用户调用 qiankunstart 办法,启动 qiankun

启用预加载微利用策略,依据不同的策略,加载微利用。外围实现就是用 requestIdelCallbackcpu 闲暇时预加载微利用的入口文件、以及近程 scriptsstyles

最初也会调用 reroute 办法。

reroute

调用 getAppChanges 获取 toUnload toUnmount toLoad toMount 这四种状态的微利用(依据以后url和微利用注册的门路是否匹配,以及微利用以后状态来做过滤的)。

如果 qiankun 已启动,则对 toUnload toUnmount toLoad toMount 中的微利用对象做一些相应改变(如 state 的扭转、生命周期钩子的增删等等),以及触发对应的生命周期钩子。其中 toLoad 的微利用会先进行加载微利用,再进行微利用对象的改变。

如果 qiankun 未启动,对 toLoad 的微利用会进行加载微利用操作。

加载微利用

加载微利用是 single-spa 调用 registerApplicationappOrLoadApp 这个传入的办法实现的。传入的办法是 qiankunloadApp 办法,是通过 import-html-entry 这个库来是事实的。

通过 fetch 办法来加载入口html。失去html文本后通过正则剖析出inline scripts、近程scripts、inline style和近程styles。加载近程styles之后,把inline styles和近程下载下来的styles文本笼罩到html的style和link标签。而后返回template(html文本)、assetPublicPath、getExternalScripts、getExternalStyleSheets、execScripts(script执行器)。

用 div#__qiankun_microapp_wrapper_for_微利用名字__ 来包裹入口html内容,而后挂载到微利用容器中。在挂载前,如果设置了款式隔离会进行款式隔离。

创立沙箱。

调用script执行器(execScripts),传入沙箱的代理对象作为微利用的全局对象。

取出微利用导出的bootstrap、mount、unmount、update生命周期函数。取出用于跟以后微利用通信的办法:onGlobalStateChange, setGlobalState, offGlobalStateChange。

返回微利用对象(app),其中有bootstrap、mount和unmount这三个生命周期钩子属性。

bootstrap就是微利用提供的bootstrap办法

mount封装了以下办法:挂载前用户提供的loader(true)、确保每次利用加载前容器 dom 构造曾经设置结束,对款式做隔离、挂载沙箱、触发beforeMount钩子、微利用提供的mount办法、再次确保每次利用加载前容器 dom 构造曾经设置结束、触发afterMount钩子、挂载后用户提供的loader(false)

unmount封装了以下办法:触发beforeUnmount钩子、调用微利用提供的unmount办法、触发afterUnmount钩子、删除div#__qiankun_microapp_wrapper_for_微利用名字__

查看微利用是否有导出bootstrap、mount、unmount生命周期函数,没有则报错

微利用对象app,置status为NOT_BOOTSTRAPPED,以及对几个参数做格式化

(图片是从下到上网上看)

reroute

当初再来看reroute,如果 qiankun 已启动,则对 toUnload toUnmount toLoad toMount 中的微利用对象做一些相应改变(如 state 的扭转、生命周期钩子的增删等等),以及触发对应的生命周期钩子。这里的生命周期钩子,就是刚刚说的注册的bootstrap、mount、unmount生命钩子。

款式隔离

款式隔离,qiankun 提供了两种计划:shadow dom计划、scoped计划。

在挂载沙箱之前,进行款式隔离操作,能够在 qiankunsrc/loader.ts 查看源码

function createElement(
  appContent: string,
  strictStyleIsolation: boolean,
  scopedCSS: boolean,
  appInstanceId: string,
): HTMLElement {
  const containerElement = document.createElement('div');
  containerElement.innerHTML = appContent;
  // appContent always wrapped with a singular div
  const appElement = containerElement.firstChild as HTMLElement;
  if (strictStyleIsolation) {
    if (!supportShadowDOM) {
      console.warn(
        '[qiankun]: As current browser not support shadow dom, your strictStyleIsolation configuration will be ignored!',
      );
    } else {
      const { innerHTML } = appElement;
      appElement.innerHTML = '';
      let shadow: ShadowRoot;

      if (appElement.attachShadow) {
        shadow = appElement.attachShadow({ mode: 'open' });
      } else {
        // createShadowRoot was proposed in initial spec, which has then been deprecated
        shadow = (appElement as any).createShadowRoot();
      }
      shadow.innerHTML = innerHTML;
    }
  }

  if (scopedCSS) {
    const attr = appElement.getAttribute(css.QiankunCSSRewriteAttr);
    if (!attr) {
      appElement.setAttribute(css.QiankunCSSRewriteAttr, appInstanceId);
    }

    const styleNodes = appElement.querySelectorAll('style') || [];
    forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
      css.process(appElement!, stylesheetElement, appInstanceId);
    });
  }

  return appElement;
}

如果是 shadow dom 模式,为每个微利用的容器包裹上一个 shadow dom 节点,从而确保微利用的款式不会对全局造成影响。

如果是 scoped 模式,能够到 qiankunsrc/sandbox/patchers/css.ts查看源码。次要做了,监听style节点的监听,而后有变动,则做 rewrite 操作。rewrite 次要做了以下工作:针对每个款式规定前都加 div#微利用名 来实现 scoped 的成果。如原本是.test{width: 100%;},scoped之后变成 div[data-qiankun=微利用名] .test{width: 100%:}

沙箱

能够到 qiankunsrc/sandbox/index.ts 查看源码。

依据环境是否反对proxy、以及是否多例模式,抉择不同的沙箱实现计划(共三种)。很多文章曾经做了源码剖析,我就不再重复劳动了。无非是针对全局变量的增、删、改操作做代理,在卸载的时候,把全局变量切回去。

script执行器(execScripts)

能够到 import-html-entrysrc/index.js 查看源码。

export function execScripts(entry, scripts, proxy = window, opts = {}) {
    const {
        fetch = defaultFetch, strictGlobal = false, success, error = () => {
        }, beforeExec = () => {
        }, afterExec = () => {
        },
    } = opts;

    return getExternalScripts(scripts, fetch, error)
        .then(scriptsText => {

            const geval = (scriptSrc, inlineScript) => {
                const rawCode = beforeExec(inlineScript, scriptSrc) || inlineScript;
                const code = getExecutableScript(scriptSrc, rawCode, proxy, strictGlobal);

                evalCode(scriptSrc, code);

                afterExec(inlineScript, scriptSrc);
            };

            function exec(scriptSrc, inlineScript, resolve) {

                const markName = `Evaluating script ${scriptSrc}`;
                const measureName = `Evaluating Time Consuming: ${scriptSrc}`;

                if (process.env.NODE_ENV === 'development' && supportsUserTiming) {
                    performance.mark(markName);
                }

                if (scriptSrc === entry) {
                    noteGlobalProps(strictGlobal ? proxy : window);

                    try {
                        // bind window.proxy to change `this` reference in script
                        geval(scriptSrc, inlineScript);
                        const exports = proxy[getGlobalProp(strictGlobal ? proxy : window)] || {};
                        resolve(exports);
                    } catch (e) {
                        // entry error must be thrown to make the promise settled
                        console.error(`[import-html-entry]: error occurs while executing entry script ${scriptSrc}`);
                        throw e;
                    }
                } else {
                    if (typeof inlineScript === 'string') {
                        try {
                            // bind window.proxy to change `this` reference in script
                            geval(scriptSrc, inlineScript);
                        } catch (e) {
                            // consistent with browser behavior, any independent script evaluation error should not block the others
                            throwNonBlockingError(e, `[import-html-entry]: error occurs while executing normal script ${scriptSrc}`);
                        }
                    } else {
                        // external script marked with async
                        inlineScript.async && inlineScript?.content
                            .then(downloadedScriptText => geval(inlineScript.src, downloadedScriptText))
                            .catch(e => {
                                throwNonBlockingError(e, `[import-html-entry]: error occurs while executing async script ${inlineScript.src}`);
                            });
                    }
                }

                if (process.env.NODE_ENV === 'development' && supportsUserTiming) {
                    performance.measure(measureName, markName);
                    performance.clearMarks(markName);
                    performance.clearMeasures(measureName);
                }
            }

            function schedule(i, resolvePromise) {

                if (i < scripts.length) {
                    const scriptSrc = scripts[i];
                    const inlineScript = scriptsText[i];

                    exec(scriptSrc, inlineScript, resolvePromise);
                    // resolve the promise while the last script executed and entry not provided
                    if (!entry && i === scripts.length - 1) {
                        resolvePromise();
                    } else {
                        schedule(i + 1, resolvePromise);
                    }
                }
            }

            return new Promise(resolve => schedule(0, success || resolve));
        });
}

execScripts的entry,是取入口html的最初一个script作为入口js。对每个scripts串行一一执行exec,exec又调用到geval。直到执行到最初一个scripts,就resolve掉promise。

exec,如果script不是入口js,则执行geval。如果script是入口js,执行geval之后,resolve掉promise,传入代理全局window的proxy对象。

geval,将js代码,用iife封装,且传入全局window的proxy,即微利用的沙箱全局对象。

function getExecutableScript(scriptSrc, scriptText, proxy, strictGlobal) {
    const sourceUrl = isInlineCode(scriptSrc) ? '' : `//# sourceURL=${scriptSrc}\n`;

    // 通过这种形式获取全局 window,因为 script 也是在全局作用域下运行的,所以咱们通过 window.proxy 绑定时也必须确保绑定到全局 window 上
    // 否则在嵌套场景下, window.proxy 设置的是内层利用的 window,而代码其实是在全局作用域运行的,会导致闭包里的 window.proxy 取的是最外层的微利用的 proxy
    const globalWindow = (0, eval)('window');
    globalWindow.proxy = proxy;
    // TODO 通过 strictGlobal 形式切换 with 闭包,待 with 形式坑趟平后再合并
    return strictGlobal
        ? `;(function(window, self, globalThis){with(window){;${scriptText}\n${sourceUrl}}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`
        : `;(function(window, self, globalThis){;${scriptText}\n${sourceUrl}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`;
}

这里的 (0, eval)('window') 是获取全局window对象的办法,为什么不 eval('window'),是因为这样执行,是在以后作用域上执行,获取到的window对象不能保障是全局window对象,有可能是在某层微利用中的作用域,指向了该微利用的全局对象。对于eval的黑魔法,能够看这两篇文章:1 2

而后执行evalCode办法。实质就是执行这段代码。

export function evalCode(scriptSrc, code) {
    const key = scriptSrc;
    if (!evalCache[key]) {
        const functionWrappedCode = `window.__TEMP_EVAL_FUNC__ = function(){${code}}`;
        (0, eval)(functionWrappedCode);
        evalCache[key] = window.__TEMP_EVAL_FUNC__;
        delete window.__TEMP_EVAL_FUNC__;
    }
    const evalFunc = evalCache[key];
    evalFunc.call(window);
}

能够看到,是通过iife封装微利用js,而后传入全局window的proxy对象作为微利用的全局对象。以达到执行微利用、隔离微利用的成果。

assetPublicPath

assetPublicPath是加载微利用资源的publicPath,它默认的获取形式,是基于defaultGetPublicPath办法获取的

export function defaultGetPublicPath(entry) {
    if (typeof entry === 'object') {
        return '/';
    }
    try {
        const { origin, pathname } = new URL(entry, location.href);
        const paths = pathname.split('/');
        // 移除最初一个元素
        paths.pop();
        return `${origin}${paths.join('/')}/`;
    } catch (e) {
        console.warn(e);
        return '';
    }
}

能够看到,它会把html入口地址的path,去掉最初一个元素,再返回,当做 assetPublicPath

这里的 assetPublicPath 就是 qiankun 注入到全局的 __INJECTED_PUBLIC_PATH_BY_QIANKUN__ 变量。

后记

很多细节没有在这篇写进去,如果感兴趣,能够去啃源码+打断点去摸索下。如以下这些:

  1. 如果是singular模式,会在加载微利用前,先卸载已有的微利用等等
  2. 统计性能
  3. reroute调度,有一些事件被延后执行的
  4. qiankun源码正文中,说3.x会废除props通信计划、shadow dom 款式隔离计划
  5. 利用通信,通过props通信

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据