微前端:子应用的加载

2,191 阅读2分钟

微前端实现的核心要素

  • 子应用的加载
  • 应用间运行时隔离
  • 应用间通信
  • 路由劫持

对于qiankun来说,路由劫持是在singleSpa上去做的

qiankun是single-spa的一层封装

在qiankun中,真正去加载解析子应用的逻辑是在import-html-entry这个包中实现的

子应用加载流程

  1. 拿到子应用的entry配置的链接
  2. 通过fetch获取链接返回的html字符串
  3. 解析html字符串
  4. 拿到html模板
  5. 通过fetch获取外联css内容,并插入到html模板中
  6. 通过fetch获取外联js,并执行
  7. 通过appendChild将html字符串插入到container配置的节点下

html解析

  1. 当我们配置子应用的entry后,qiankun会去通过fetch获取到子应用的html字符串(子应用资源需要允许跨域);
  2. 拿到html字符串后,会调用processTpl方法通过一大堆正则去匹配获取html中对应得js(内联、外联)、css(内联、外联)、注释、入口脚本entry等等。

processTpl方法会返回我们加载子应用所需要的四个组成部分:

  • template : html模板

  • script : js脚本(内联、外联)

  • styles : css样式表(内联、外联)

  • entry : 子应用入口js脚本文件,如果没有默认解析到最后一个js脚本代替

function processTpl(tpl, baseURI) {
 
  return {
    template: template,
    scripts: scripts,
    styles: styles,
    // set the last script as entry if have not set
    entry: entry || scripts[scripts.length - 1]
  };
}

CSS处理

接下来在拿到子应用依赖的各种资源关系后,会去通过fetch获取css,并将css全部以内联的形式插入到html模板中。到此对css的处理大致就完成了。

function getEmbedHTML(template, styles) {
  var opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
  var _opts$fetch = opts.fetch,
      fetch = _opts$fetch === void 0 ? defaultFetch : _opts$fetch;
  var embedHTML = template;
  return _getExternalStyleSheets(styles, fetch).then(function (styleSheets) {
    embedHTML = styles.reduce(function (html, styleSrc, i) {
      html = html.replace((0, _processTpl2.genLinkReplaceSymbol)(styleSrc), "<style>/* ".concat(styleSrc, " */").concat(styleSheets[i], "</style>"));
      return html;
    }, embedHTML);
    return embedHTML;
  });
}

js处理

接下来是对js的处理,这里qiankun 和 icestark的处理模式就不同了

首先简单说下icestark, icestark是在解析完html后拿到子应用的js依赖,通过动态创建script标签的形式去加载js,因此在icestark是无视js跨域的(icestark的entry模式和url模式均是如此, 区别在于entry模式多了异步fetch拉html字符串并解析js、css依赖,而url模式只需要指定子应用的脚本和样式依赖即可)。

而qiankun则采用了另一种办法,首先同理会通过fetch获取外联的js字符串。

function _getExternalScripts(scripts) {
  var fetch = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultFetch;
  var errorCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () {};

  var fetchScript = function fetchScript(scriptUrl) {
    return scriptCache[scriptUrl] || (scriptCache[scriptUrl] = fetch(scriptUrl).then(function (response) {
      // usually browser treats 4xx and 5xx response of script loading as an error and will fire a script error event
      // https://stackoverflow.com/questions/5625420/what-http-headers-responses-trigger-the-onerror-handler-on-a-script-tag/5625603
      if (response.status >= 400) {
        errorCallback();
        throw new Error("".concat(scriptUrl, " load failed with status ").concat(response.status));
      }

      return response.text();
    }));
  };

  return Promise.all(scripts.map(function (script) {
    if (typeof script === 'string') {
      if (isInlineCode(script)) {
        // if it is inline script
        return (0, _utils.getInlineCode)(script);
      } else {
        // external script
        return fetchScript(script);
      }
    } else {
      // use idle time to load async script
      var src = script.src,
          async = script.async;

      if (async) {
        return {
          src: src,
          async: true,
          content: new Promise(function (resolve, reject) {
            return (0, _utils.requestIdleCallback)(function () {
              return fetchScript(src).then(resolve, reject);
            });
          })
        };
      }

      return fetchScript(src);
    }
  }));
}

接下来会创建一个匿名自执行函数包裹住获取到的js字符串,最后通过eval去创建一个上下文执行js代码。

function _execScripts(entry, scripts) {
  
  return _getExternalScripts(scripts, fetch, error).then(function (scriptsText) {
    var geval = function geval(scriptSrc, inlineScript) {
      var rawCode = beforeExec(inlineScript, scriptSrc) || inlineScript;
      var code = getExecutableScript(scriptSrc, rawCode, proxy, strictGlobal);
      (0, eval)(code);
      afterExec(inlineScript, scriptSrc);
    };

子应用的挂载相关代码

一个子应用的配置如下

{
      name: 'react16',
      entry: '//localhost:7100',
      container: '#subapp-viewport',
      loader,
      activeRule: '/react16',
    },

通过entry配置的链接,获取到的html字符串,最终会通过appendChild挂载到容器#subapp-viewport中

  1. 调用loadApp()方法,
export async function loadApp<T extends ObjectType>(
  app: LoadableApp<T>,
  configuration: FrameworkConfiguration = {},
  lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObjectGetter> {
    const initialContainer = 'container' in app ? app.container : undefined;
      const legacyRender = 'render' in app ? app.render : undefined;

      const render = getRender(appName, appContent, legacyRender);

      // 第一次加载设置应用可见区域 dom 结构
      // 确保每次应用加载前容器 dom 结构已经设置完毕
      render({ element: initialAppWrapperElement, loading: true, container: initialContainer }, 'loading');

}
  1. 调用render()方法,render函数的第一个参数为html字符串

  const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);


const appContent = getDefaultTplWrapper(appInstanceId, appName)(template);


let initialAppWrapperElement: HTMLElement | null = createElement(
    appContent,
    strictStyleIsolation,
    scopedCSS,
    appName,
  );
  
  function  getRender(appName: string, appContent: string, legacyRender?: HTMLContentRender) {
  const render: ElementRender = ({ element, loading, container }, phase) => {
    if (legacyRender) {
      if (process.env.NODE_ENV === 'development') {
        console.warn(
          '[qiankun] Custom rendering function is deprecated, you can use the container element setting instead!',
        );
      }

      return legacyRender({ loading, appContent: element ? appContent : '' });
    }

    const containerElement = getContainer(container!);

    // The container might have be removed after micro app unmounted.
    // Such as the micro app unmount lifecycle called by a react componentWillUnmount lifecycle, after micro app unmounted, the react component might also be removed
    if (phase !== 'unmounted') {
      const errorMsg = (() => {
        switch (phase) {
          case 'loading':
          case 'mounting':
            return `Target container with ${container} not existed while ${appName} ${phase}!`;

          case 'mounted':
            return `Target container with ${container} not existed after ${appName} ${phase}!`;

          default:
            return `Target container with ${container} not existed while ${appName} rendering!`;
        }
      })();
      assertElementExist(containerElement, errorMsg);
    }

    if (containerElement && !containerElement.contains(element)) {
      // clear the container
      while (containerElement!.firstChild) {
        rawRemoveChild.call(containerElement, containerElement!.firstChild);
      }

      // append the element to container if it exist
      if (element) {
        rawAppendChild.call(containerElement, element);
      }
    }

    return undefined;
  };

  return render;
}

参考链接:

juejin.cn/post/689188…