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代碼,則在元素加載完之後通知。