微前端框架 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<HTMLLinkElement | HTMLStyleElement> = [];

  HTMLHeadElement.prototype.appendChild = function appendChild<T extends Node>(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, [`<script>${text}</script>`], 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代碼,則在元素加載完之後通知。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章