微前端框架 qiankun 源码解读 1.3 hijackers/动态加载标签

import { execScripts } from 'import-html-entry';
import { isFunction } from 'lodash';
import { checkActivityFunctions } from 'single-spa';
import { Freer } from '../interfaces';

const styledComponentSymbol = Symbol('styled-component');

declare global {
  interface HTMLStyleElement {
    [styledComponentSymbol]?: CSSRuleList;
  }
}

const rawHtmlAppendChild = HTMLHeadElement.prototype.appendChild;

const SCRIPT_TAG_NAME = 'SCRIPT';
const LINK_TAG_NAME = 'LINK';
const STYLE_TAG_NAME = 'STYLE';

/**
 * Check if a style element is a styled-component liked.
 * A styled-components liked element is which not have textContext but keep the rules in its styleSheet.cssRules.
 * Such as the style element generated by styled-components and emotion.
 * @param element
 */
function isStyledComponentsLike(element: HTMLStyleElement) {
  return !element.textContent && ((element.sheet as CSSStyleSheet)?.cssRules.length || getCachedRules(element)?.length);
}

function getCachedRules(element: HTMLStyleElement) {
  return element[styledComponentSymbol];
}

function setCachedRules(element: HTMLStyleElement, cssRules: CSSRuleList) {
  Object.defineProperty(element, styledComponentSymbol, { value: cssRules, configurable: true, enumerable: false });
}

export default function hijack(appName: string, proxy: Window): Freer {
  const dynamicStyleSheetElements: Array = [];

  HTMLHeadElement.prototype.appendChild = function appendChild(this: any, newChild: T) {
    const element = newChild as any;
    if (element.tagName) {
      switch (element.tagName) {
        case LINK_TAG_NAME:
        case STYLE_TAG_NAME: {
          const stylesheetElement: HTMLLinkElement | HTMLStyleElement = newChild as any;

          const activated = checkActivityFunctions(window.location).some(name => name === appName);
          // only hijack dynamic style injection when app activated
          if (activated) {
            dynamicStyleSheetElements.push(stylesheetElement);
          }

          break;
        }

        case SCRIPT_TAG_NAME: {
          const { src, text } = element as HTMLScriptElement;

          if (src) {
            execScripts(null, [src], proxy).then(
              () => {
                // we need to invoke the onload event manually to notify the event listener that the script was completed
                const loadEvent = new CustomEvent('load');
                if (isFunction(element.onload)) {
                  element.onload(loadEvent);
                } else {
                  element.dispatchEvent(loadEvent);
                }
              },
              () => {
                const errorEvent = new CustomEvent('error');
                if (isFunction(element.onerror)) {
                  element.onerror(errorEvent);
                } else {
                  element.dispatchEvent(errorEvent);
                }
              },
            );

            const dynamicScriptCommentElement = document.createComment(`dynamic script ${src} replaced by qiankun`);
            return rawHtmlAppendChild.call(this, dynamicScriptCommentElement) as T;
          }

          execScripts(null, [``], proxy).then(element.onload, element.onerror);
          const dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun');
          return rawHtmlAppendChild.call(this, dynamicInlineScriptCommentElement) as T;
        }

        default:
          break;
      }
    }

    return rawHtmlAppendChild.call(this, element) as T;
  };

  return function free() {
    HTMLHeadElement.prototype.appendChild = rawHtmlAppendChild;
    dynamicStyleSheetElements.forEach(stylesheetElement => {
      // the dynamic injected stylesheet may had been removed by itself while unmounting
      if (document.head.contains(stylesheetElement)) {
       
        if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) {
          if (stylesheetElement.sheet) {
            // record the original css rules of the style element for restore
            setCachedRules(stylesheetElement, (stylesheetElement.sheet as CSSStyleSheet).cssRules);
          }
        }

        document.head.removeChild(stylesheetElement);
      }
    });

    return function rebuild() {
      dynamicStyleSheetElements.forEach(stylesheetElement => {
        document.head.appendChild(stylesheetElement);

        if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) {
          const cssRules = getCachedRules(stylesheetElement);
          if (cssRules) {
            // eslint-disable-next-line no-plusplus
            for (let i = 0; i < cssRules.length; i++) {
              const cssRule = cssRules[i];
              (stylesheetElement.sheet as CSSStyleSheet).insertRule(cssRule.cssText);
            }
          }
        }
      });
    };
  };
}

劫持HTMLHeadElement.prototype.appendChild,判断添加的是样式还是js代码。

传入的是style标签时:当路由符合,即应用生效的时候加入样式。

传入的是script标签时:判断是通过src获取的js还是标签里面执行js代码。

如果是src获取,则需要在资源加载完之后通知监听器,script标签已经生效。

如果是内容js代码,则在元素加载完之后通知。

你可能感兴趣的:(qiankun)