From d537790f7da001853c4451aceda4342678c90297 Mon Sep 17 00:00:00 2001 From: Kuitos Date: Wed, 14 Oct 2020 14:26:58 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20split=20dynamic=20append=20patcher?= =?UTF-8?q?=20for=20loose=20and=20strict=20sandbox=20(#995)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ split dynamic append patcher for proxy and non-proxy sandbox * ✨ change useLooseSandbox to sandbox.loose configuration * ♿ improve warning message for non-proxy environment --- examples/main/multiple.html | 4 + examples/main/multiple.js | 34 +- src/apis.ts | 7 +- src/interfaces.ts | 4 + src/loader.ts | 13 +- src/sandbox/index.ts | 17 +- src/sandbox/patchers/dynamicAppend.ts | 479 ------------------ src/sandbox/patchers/dynamicAppend/common.ts | 313 ++++++++++++ .../patchers/dynamicAppend/forLooseSandbox.ts | 84 +++ .../dynamicAppend/forStrictSandbox.ts | 107 ++++ src/sandbox/patchers/dynamicAppend/index.ts | 7 + src/sandbox/patchers/index.ts | 39 +- src/sandbox/proxySandbox.ts | 12 +- src/utils.ts | 20 +- 14 files changed, 585 insertions(+), 555 deletions(-) delete mode 100644 src/sandbox/patchers/dynamicAppend.ts create mode 100644 src/sandbox/patchers/dynamicAppend/common.ts create mode 100644 src/sandbox/patchers/dynamicAppend/forLooseSandbox.ts create mode 100644 src/sandbox/patchers/dynamicAppend/forStrictSandbox.ts create mode 100644 src/sandbox/patchers/dynamicAppend/index.ts diff --git a/examples/main/multiple.html b/examples/main/multiple.html index aa86eaa..c9026d1 100644 --- a/examples/main/multiple.html +++ b/examples/main/multiple.html @@ -14,6 +14,10 @@ + + + +
react loading...
vue loading...
diff --git a/examples/main/multiple.js b/examples/main/multiple.js index c4130fb..bc7c61e 100644 --- a/examples/main/multiple.js +++ b/examples/main/multiple.js @@ -1,28 +1,16 @@ import { loadMicroApp } from '../../es'; -const app1 = loadMicroApp( - { name: 'react15', entry: '//localhost:7102', container: '#react15' }, - { - sandbox: { - // strictStyleIsolation: true, - }, - }, -); +let app; -// for cached scenario -setTimeout(() => { - app1.unmount(); +function mount() { + app = loadMicroApp({ name: 'react15', entry: '//localhost:7102', container: '#react15' }); +} - setTimeout(() => { - loadMicroApp({ name: 'react15', entry: '//localhost:7102', container: '#react15' }); - }, 1000 * 5); -}, 1000 * 5); +function unmount() { + app.unmount(); +} -const app2 = loadMicroApp( - { name: 'vue', entry: '//localhost:7101', container: '#vue' }, - { - sandbox: { - // strictStyleIsolation: true, - }, - }, -); +document.querySelector('#mount').addEventListener('click', mount); +document.querySelector('#unmount').addEventListener('click', unmount); + +loadMicroApp({ name: 'vue', entry: '//localhost:7101', container: '#vue' }); diff --git a/src/apis.ts b/src/apis.ts index d37c84e..f1cda4d 100644 --- a/src/apis.ts +++ b/src/apis.ts @@ -108,10 +108,11 @@ export function start(opts: FrameworkConfiguration = {}) { if (sandbox) { if (!window.Proxy) { console.warn('[qiankun] Miss window.Proxy, proxySandbox will degenerate into snapshotSandbox'); - // 快照沙箱不支持非 singular 模式 + frameworkConfiguration.sandbox = typeof sandbox === 'object' ? { ...sandbox, loose: true } : { loose: true }; if (!singular) { - console.error('[qiankun] singular is forced to be true when sandbox enable but proxySandbox unavailable'); - frameworkConfiguration.singular = true; + console.warn( + '[qiankun] Setting singular as false may cause unexpected behavior while your browser not support window.Proxy', + ); } } } diff --git a/src/interfaces.ts b/src/interfaces.ts index 69125c5..ddc603d 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -60,6 +60,10 @@ type QiankunSpecialOpts = { | { strictStyleIsolation?: boolean; experimentalStyleIsolation?: boolean; + /** + * @deprecated We use strict mode by default + */ + loose?: boolean; patchers?: Patcher[]; }; /* diff --git a/src/loader.ts b/src/loader.ts index 0141657..d49a92a 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -244,12 +244,12 @@ export async function loadApp( await (prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise); } - const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation; - const enableScopedCSS = isEnableScopedCSS(configuration); - const appContent = getDefaultTplWrapper(appInstanceId, appName)(template); + + const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation; let appWrapperElement: HTMLElement | null = createElement(appContent, strictStyleIsolation); - if (appWrapperElement && isEnableScopedCSS(configuration)) { + const enableScopedCSS = isEnableScopedCSS(sandbox); + if (appWrapperElement && enableScopedCSS) { const styleNodes = appWrapperElement.querySelectorAll('style') || []; forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => { css.process(appWrapperElement!, stylesheetElement, appName); @@ -277,12 +277,13 @@ export async function loadApp( let global = window; let mountSandbox = () => Promise.resolve(); let unmountSandbox = () => Promise.resolve(); + const useLooseSandbox = typeof sandbox === 'object' && !!sandbox.loose; if (sandbox) { const sandboxInstance = createSandbox( appName, appWrapperGetter, - Boolean(singular), enableScopedCSS, + useLooseSandbox, excludeAssetFilter, ); // 用沙箱的代理对象作为接下来使用的全局对象 @@ -301,7 +302,7 @@ export async function loadApp( await execHooksChain(toArray(beforeLoad), app, global); // get the lifecycle hooks from module exports - const scriptExports: any = await execScripts(global, !singular); + const scriptExports: any = await execScripts(global, !useLooseSandbox); const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(scriptExports, appName, global); const { diff --git a/src/sandbox/index.ts b/src/sandbox/index.ts index 9978348..4d4487b 100644 --- a/src/sandbox/index.ts +++ b/src/sandbox/index.ts @@ -24,33 +24,26 @@ export { css } from './patchers'; * * @param appName * @param elementGetter - * @param singular * @param scopedCSS + * @param useLooseSandbox * @param excludeAssetFilter */ export function createSandbox( appName: string, elementGetter: () => HTMLElement | ShadowRoot, - singular: boolean, scopedCSS: boolean, + useLooseSandbox?: boolean, excludeAssetFilter?: (url: string) => boolean, ) { let sandbox: SandBox; if (window.Proxy) { - sandbox = singular ? new LegacySandbox(appName) : new ProxySandbox(appName); + sandbox = useLooseSandbox ? new LegacySandbox(appName) : new ProxySandbox(appName); } else { sandbox = new SnapshotSandbox(appName); } // some side effect could be be invoked while bootstrapping, such as dynamic stylesheet injection with style-loader, especially during the development phase - const bootstrappingFreers = patchAtBootstrapping( - appName, - elementGetter, - sandbox, - singular, - scopedCSS, - excludeAssetFilter, - ); + const bootstrappingFreers = patchAtBootstrapping(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter); // mounting freers are one-off and should be re-init at every mounting time let mountingFreers: Freer[] = []; @@ -80,7 +73,7 @@ export function createSandbox( /* ------------------------------------------ 2. 开启全局变量补丁 ------------------------------------------*/ // render 沙箱启动时开始劫持各类全局监听,尽量不要在应用初始化阶段有 事件监听/定时器 等副作用 - mountingFreers = patchAtMounting(appName, elementGetter, sandbox, singular, scopedCSS, excludeAssetFilter); + mountingFreers = patchAtMounting(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter); /* ------------------------------------------ 3. 重置一些初始化时的副作用 ------------------------------------------*/ // 存在 rebuilder 则表明有些副作用需要重建 diff --git a/src/sandbox/patchers/dynamicAppend.ts b/src/sandbox/patchers/dynamicAppend.ts deleted file mode 100644 index 7e28e63..0000000 --- a/src/sandbox/patchers/dynamicAppend.ts +++ /dev/null @@ -1,479 +0,0 @@ -/** - * @author Kuitos - * @since 2019-10-21 - */ -import { execScripts } from 'import-html-entry'; -import { isFunction, noop } from 'lodash'; -import { checkActivityFunctions } from 'single-spa'; -import { frameworkConfiguration } from '../../apis'; -import { Freer } from '../../interfaces'; -import { attachDocProxySymbol } from '../common'; -import * as css from './css'; - -const styledComponentSymbol = Symbol('styled-component-qiankun'); -const attachElementContainerSymbol = Symbol('attach-proxy-container'); - -declare global { - interface HTMLStyleElement { - // eslint-disable-next-line no-undef - [styledComponentSymbol]?: CSSRuleList; - } -} - -const rawHeadAppendChild = HTMLHeadElement.prototype.appendChild; -const rawHeadRemoveChild = HTMLHeadElement.prototype.removeChild; -const rawBodyAppendChild = HTMLBodyElement.prototype.appendChild; -const rawBodyRemoveChild = HTMLBodyElement.prototype.removeChild; -const rawHeadInsertBefore = HTMLHeadElement.prototype.insertBefore; -const rawRemoveChild = HTMLElement.prototype.removeChild; - -const rawDocumentCreateElement = Document.prototype.createElement; - -const SCRIPT_TAG_NAME = 'SCRIPT'; -const LINK_TAG_NAME = 'LINK'; -const STYLE_TAG_NAME = 'STYLE'; - -const proxyContainerInfoMapper = new WeakMap>(); - -function isHijackingTag(tagName?: string) { - return ( - tagName?.toUpperCase() === LINK_TAG_NAME || - tagName?.toUpperCase() === STYLE_TAG_NAME || - tagName?.toUpperCase() === SCRIPT_TAG_NAME - ); -} - -/** - * 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 }); -} - -function patchCustomEvent(e: CustomEvent, elementGetter: () => HTMLScriptElement | null): CustomEvent { - Object.defineProperties(e, { - srcElement: { - get: elementGetter, - }, - target: { - get: elementGetter, - }, - }); - return e; -} - -function getOverwrittenAppendChildOrInsertBefore(opts: { - appName: string; - proxy: WindowProxy; - singular: boolean; - dynamicStyleSheetElements: HTMLStyleElement[]; - appWrapperGetter: CallableFunction; - rawDOMAppendOrInsertBefore: (newChild: T, refChild?: Node | null) => T; - scopedCSS: boolean; - excludeAssetFilter?: CallableFunction; - elementAttachmentMap: WeakMap; -}) { - return function appendChildOrInsertBefore( - this: HTMLHeadElement | HTMLBodyElement, - newChild: T, - refChild?: Node | null, - ) { - let element = newChild as any; - const { rawDOMAppendOrInsertBefore } = opts; - if (element.tagName) { - // eslint-disable-next-line prefer-const - let { appName, appWrapperGetter, proxy, singular, dynamicStyleSheetElements } = opts; - const { scopedCSS, excludeAssetFilter } = opts; - - const storedContainerInfo = element[attachElementContainerSymbol]; - if (storedContainerInfo) { - // eslint-disable-next-line prefer-destructuring - appName = storedContainerInfo.appName; - // eslint-disable-next-line prefer-destructuring - singular = storedContainerInfo.singular; - // eslint-disable-next-line prefer-destructuring - appWrapperGetter = storedContainerInfo.appWrapperGetter; - // eslint-disable-next-line prefer-destructuring - dynamicStyleSheetElements = storedContainerInfo.dynamicStyleSheetElements; - // eslint-disable-next-line prefer-destructuring - proxy = storedContainerInfo.proxy; - } - - const invokedByMicroApp = singular - ? // check if the currently specified application is active - // While we switch page from qiankun app to a normal react routing page, the normal one may load stylesheet dynamically while page rendering, - // but the url change listener must to wait until the current call stack is flushed. - // This scenario may cause we record the stylesheet from react routing page dynamic injection, - // and remove them after the url change triggered and qiankun app is unmouting - // see https://github.com/ReactTraining/history/blob/master/modules/createHashHistory.js#L222-L230 - checkActivityFunctions(window.location).some(name => name === appName) - : // have storedContainerInfo means it invoked by a micro app in multiply mode - !!storedContainerInfo; - - switch (element.tagName) { - case LINK_TAG_NAME: - case STYLE_TAG_NAME: { - const stylesheetElement: HTMLLinkElement | HTMLStyleElement = newChild as any; - const { href } = stylesheetElement as HTMLLinkElement; - if (!invokedByMicroApp || (excludeAssetFilter && href && excludeAssetFilter(href))) { - return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T; - } - - const mountDOM = appWrapperGetter(); - - if (scopedCSS) { - css.process(mountDOM, stylesheetElement, appName); - } - - // eslint-disable-next-line no-shadow - dynamicStyleSheetElements.push(stylesheetElement); - const referenceNode = mountDOM.contains(refChild) ? refChild : null; - return rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode); - } - - case SCRIPT_TAG_NAME: { - const { src, text } = element as HTMLScriptElement; - // some script like jsonp maybe not support cors which should't use execScripts - if (!invokedByMicroApp || (excludeAssetFilter && src && excludeAssetFilter(src))) { - return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T; - } - - const mountDOM = appWrapperGetter(); - const { fetch } = frameworkConfiguration; - const referenceNode = mountDOM.contains(refChild) ? refChild : null; - - if (src) { - execScripts(null, [src], proxy, { - fetch, - strictGlobal: !singular, - beforeExec: () => { - Object.defineProperty(document, 'currentScript', { - get(): any { - return element; - }, - configurable: true, - }); - }, - success: () => { - // we need to invoke the onload event manually to notify the event listener that the script was completed - // here are the two typical ways of dynamic script loading - // 1. element.onload callback way, which webpack and loadjs used, see https://github.com/muicss/loadjs/blob/master/src/loadjs.js#L138 - // 2. addEventListener way, which toast-loader used, see https://github.com/pyrsmk/toast/blob/master/src/Toast.ts#L64 - const loadEvent = new CustomEvent('load'); - if (isFunction(element.onload)) { - element.onload(patchCustomEvent(loadEvent, () => element)); - } else { - element.dispatchEvent(loadEvent); - } - - element = null; - }, - error: () => { - const errorEvent = new CustomEvent('error'); - if (isFunction(element.onerror)) { - element.onerror(patchCustomEvent(errorEvent, () => element)); - } else { - element.dispatchEvent(errorEvent); - } - - element = null; - }, - }); - - const dynamicScriptCommentElement = document.createComment(`dynamic script ${src} replaced by qiankun`); - opts.elementAttachmentMap.set(element, dynamicScriptCommentElement); - return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicScriptCommentElement, referenceNode); - } - - execScripts(null, [``], proxy, { - strictGlobal: !singular, - success: element.onload, - error: element.onerror, - }); - const dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun'); - opts.elementAttachmentMap.set(element, dynamicInlineScriptCommentElement); - return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, referenceNode); - } - - default: - break; - } - } - - return rawDOMAppendOrInsertBefore.call(this, element, refChild); - }; -} - -function getNewRemoveChild(opts: { - appWrapperGetter: CallableFunction; - headOrBodyRemoveChild: typeof HTMLElement.prototype.removeChild; - elementAttachmentMap: WeakMap; -}) { - return function removeChild(this: HTMLHeadElement | HTMLBodyElement, child: T) { - const { headOrBodyRemoveChild } = opts; - try { - const { tagName } = child as any; - if (isHijackingTag(tagName)) { - let { appWrapperGetter } = opts; - - const storedContainerInfo = (child as any)[attachElementContainerSymbol]; - if (storedContainerInfo) { - // eslint-disable-next-line prefer-destructuring - appWrapperGetter = storedContainerInfo.appWrapperGetter; - } - - // container may had been removed while app unmounting if the removeChild action was async - const container = appWrapperGetter(); - const attachedElement = opts.elementAttachmentMap.get(child as any) || child; - if (container.contains(attachedElement)) { - return rawRemoveChild.call(container, attachedElement) as T; - } - } - } catch (e) { - console.warn(e); - } - - return headOrBodyRemoveChild.call(this, child) as T; - }; -} - -function patchHTMLDynamicAppendPrototypeFunctions( - appName: string, - appWrapperGetter: () => HTMLElement | ShadowRoot, - proxy: Window, - singular = true, - scopedCSS = false, - dynamicStyleSheetElements: HTMLStyleElement[], - excludeAssetFilter?: CallableFunction, -) { - const elementAttachmentMap = new WeakMap(); - - // Just overwrite it while it have not been overwrite - if ( - HTMLHeadElement.prototype.appendChild === rawHeadAppendChild && - HTMLBodyElement.prototype.appendChild === rawBodyAppendChild && - HTMLHeadElement.prototype.insertBefore === rawHeadInsertBefore - ) { - HTMLHeadElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({ - rawDOMAppendOrInsertBefore: rawHeadAppendChild, - appName, - appWrapperGetter, - proxy, - singular, - dynamicStyleSheetElements, - scopedCSS, - excludeAssetFilter, - elementAttachmentMap, - }) as typeof rawHeadAppendChild; - HTMLBodyElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({ - rawDOMAppendOrInsertBefore: rawBodyAppendChild, - appName, - appWrapperGetter, - proxy, - singular, - dynamicStyleSheetElements, - scopedCSS, - excludeAssetFilter, - elementAttachmentMap, - }) as typeof rawBodyAppendChild; - - HTMLHeadElement.prototype.insertBefore = getOverwrittenAppendChildOrInsertBefore({ - rawDOMAppendOrInsertBefore: rawHeadInsertBefore as any, - appName, - appWrapperGetter, - proxy, - singular, - dynamicStyleSheetElements, - scopedCSS, - excludeAssetFilter, - elementAttachmentMap, - }) as typeof rawHeadInsertBefore; - } - - // Just overwrite it while it have not been overwrite - if ( - HTMLHeadElement.prototype.removeChild === rawHeadRemoveChild && - HTMLBodyElement.prototype.removeChild === rawBodyRemoveChild - ) { - HTMLHeadElement.prototype.removeChild = getNewRemoveChild({ - appWrapperGetter, - headOrBodyRemoveChild: rawHeadRemoveChild, - elementAttachmentMap, - }); - HTMLBodyElement.prototype.removeChild = getNewRemoveChild({ - appWrapperGetter, - headOrBodyRemoveChild: rawBodyRemoveChild, - elementAttachmentMap, - }); - } - - return function unpatch(recoverPrototype: boolean) { - if (recoverPrototype) { - HTMLHeadElement.prototype.appendChild = rawHeadAppendChild; - HTMLHeadElement.prototype.removeChild = rawHeadRemoveChild; - HTMLBodyElement.prototype.appendChild = rawBodyAppendChild; - HTMLBodyElement.prototype.removeChild = rawBodyRemoveChild; - - HTMLHeadElement.prototype.insertBefore = rawHeadInsertBefore; - } - }; -} - -function patchDocumentCreateElement( - appName: string, - appWrapperGetter: () => HTMLElement | ShadowRoot, - singular: boolean, - proxy: Window, - dynamicStyleSheetElements: HTMLStyleElement[], -) { - if (singular) { - return noop; - } - - proxyContainerInfoMapper.set(proxy, { appName, proxy, appWrapperGetter, dynamicStyleSheetElements, singular }); - - if (Document.prototype.createElement === rawDocumentCreateElement) { - Document.prototype.createElement = function createElement( - this: Document, - tagName: K, - options?: ElementCreationOptions, - ): HTMLElement { - const element = rawDocumentCreateElement.call(this, tagName, options); - if (isHijackingTag(tagName)) { - const proxyContainerInfo = proxyContainerInfoMapper.get(this[attachDocProxySymbol]); - if (proxyContainerInfo) { - Object.defineProperty(element, attachElementContainerSymbol, { - value: proxyContainerInfo, - enumerable: false, - }); - } - } - - return element; - }; - } - - return function unpatch(recoverPrototype: boolean) { - proxyContainerInfoMapper.delete(proxy); - if (recoverPrototype) { - Document.prototype.createElement = rawDocumentCreateElement; - } - }; -} - -let bootstrappingPatchCount = 0; -let mountingPatchCount = 0; - -/** - * Just hijack dynamic head append, that could avoid accidentally hijacking the insertion of elements except in head. - * Such a case: ReactDOM.createPortal(, container), - * this could made we append the style element into app wrapper but it will cause an error while the react portal unmounting, as ReactDOM could not find the style in body children list. - * @param appName - * @param appWrapperGetter - * @param proxy - * @param mounting - * @param singular - * @param scopedCSS - * @param excludeAssetFilter - */ -export default function patch( - appName: string, - appWrapperGetter: () => HTMLElement | ShadowRoot, - proxy: Window, - mounting = true, - singular = true, - scopedCSS = false, - excludeAssetFilter?: CallableFunction, -): Freer { - let dynamicStyleSheetElements: Array = []; - - const unpatchDocumentCreate = patchDocumentCreateElement( - appName, - appWrapperGetter, - singular, - proxy, - dynamicStyleSheetElements, - ); - - const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions( - appName, - appWrapperGetter, - proxy, - singular, - scopedCSS, - dynamicStyleSheetElements, - excludeAssetFilter, - ); - - if (!mounting) bootstrappingPatchCount++; - if (mounting) mountingPatchCount++; - - return function free() { - // bootstrap patch just called once but its freer will be called multiple times - if (!mounting && bootstrappingPatchCount !== 0) bootstrappingPatchCount--; - if (mounting) mountingPatchCount--; - - const allMicroAppUnmounted = mountingPatchCount === 0 && bootstrappingPatchCount === 0; - // release the overwrite prototype after all the micro apps unmounted - unpatchDynamicAppendPrototypeFunctions(allMicroAppUnmounted); - unpatchDocumentCreate(allMicroAppUnmounted); - - dynamicStyleSheetElements.forEach(stylesheetElement => { - /* - With a styled-components generated style element, we need to record its cssRules for restore next re-mounting time. - We're doing this because the sheet of style element is going to be cleaned automatically by browser after the style element dom removed from document. - see https://www.w3.org/TR/cssom-1/#associated-css-style-sheet - */ - 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); - } - } - - // As now the sub app content all wrapped with a special id container, - // the dynamic style sheet would be removed automatically while unmoutting - }); - - return function rebuild() { - dynamicStyleSheetElements.forEach(stylesheetElement => { - // re-append the dynamic stylesheet to sub-app container - // Using document.head.appendChild ensures that appendChild calls - // can also directly use the HTMLHeadElement.prototype.appendChild method which is overwritten at mounting phase - document.head.appendChild.call(appWrapperGetter(), stylesheetElement); - - /* - get the stored css rules from styled-components generated element, and the re-insert rules for them. - note that we must do this after style element had been added to document, which stylesheet would be associated to the document automatically. - check the spec https://www.w3.org/TR/cssom-1/#associated-css-style-sheet - */ - 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); - } - } - } - }); - - // As the hijacker will be invoked every mounting phase, we could release the cache for gc after rebuilding - if (mounting) { - dynamicStyleSheetElements = []; - } - }; - }; -} diff --git a/src/sandbox/patchers/dynamicAppend/common.ts b/src/sandbox/patchers/dynamicAppend/common.ts new file mode 100644 index 0000000..0ad81b2 --- /dev/null +++ b/src/sandbox/patchers/dynamicAppend/common.ts @@ -0,0 +1,313 @@ +/** + * @author Kuitos + * @since 2019-10-21 + */ +import { execScripts } from 'import-html-entry'; +import { isFunction } from 'lodash'; +import { frameworkConfiguration } from '../../../apis'; + +import * as css from '../css'; + +export const rawHeadAppendChild = HTMLHeadElement.prototype.appendChild; +const rawHeadRemoveChild = HTMLHeadElement.prototype.removeChild; +const rawBodyAppendChild = HTMLBodyElement.prototype.appendChild; +const rawBodyRemoveChild = HTMLBodyElement.prototype.removeChild; +const rawHeadInsertBefore = HTMLHeadElement.prototype.insertBefore; +const rawRemoveChild = HTMLElement.prototype.removeChild; + +const SCRIPT_TAG_NAME = 'SCRIPT'; +const LINK_TAG_NAME = 'LINK'; +const STYLE_TAG_NAME = 'STYLE'; + +export function isHijackingTag(tagName?: string) { + return ( + tagName?.toUpperCase() === LINK_TAG_NAME || + tagName?.toUpperCase() === STYLE_TAG_NAME || + tagName?.toUpperCase() === SCRIPT_TAG_NAME + ); +} + +/** + * 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 + */ +export function isStyledComponentsLike(element: HTMLStyleElement) { + return ( + !element.textContent && + ((element.sheet as CSSStyleSheet)?.cssRules.length || getStyledElementCSSRules(element)?.length) + ); +} + +const styledComponentCSSRulesMap = new WeakMap(); +const dynamicScriptAttachedCommentMap = new WeakMap(); + +export function recordStyledComponentsCSSRules(styleElements: HTMLStyleElement[]): void { + styleElements.forEach(styleElement => { + /* + With a styled-components generated style element, we need to record its cssRules for restore next re-mounting time. + We're doing this because the sheet of style element is going to be cleaned automatically by browser after the style element dom removed from document. + see https://www.w3.org/TR/cssom-1/#associated-css-style-sheet + */ + if (styleElement instanceof HTMLStyleElement && isStyledComponentsLike(styleElement)) { + if (styleElement.sheet) { + // record the original css rules of the style element for restore + styledComponentCSSRulesMap.set(styleElement, (styleElement.sheet as CSSStyleSheet).cssRules); + } + } + }); +} + +export function getStyledElementCSSRules(styledElement: HTMLStyleElement): CSSRuleList | undefined { + return styledComponentCSSRulesMap.get(styledElement); +} + +function patchCustomEvent(e: CustomEvent, elementGetter: () => HTMLScriptElement | null): CustomEvent { + Object.defineProperties(e, { + srcElement: { + get: elementGetter, + }, + target: { + get: elementGetter, + }, + }); + return e; +} + +export type ContainerConfig = { + appName: string; + proxy: WindowProxy; + strictGlobal: boolean; + dynamicStyleSheetElements: HTMLStyleElement[]; + appWrapperGetter: CallableFunction; + scopedCSS: boolean; + excludeAssetFilter?: CallableFunction; +}; + +function getOverwrittenAppendChildOrInsertBefore(opts: { + rawDOMAppendOrInsertBefore: (newChild: T, refChild?: Node | null) => T; + isInvokedByMicroApp: (element: HTMLElement) => boolean; + containerConfigGetter: (element: HTMLElement) => ContainerConfig; +}) { + return function appendChildOrInsertBefore( + this: HTMLHeadElement | HTMLBodyElement, + newChild: T, + refChild?: Node | null, + ) { + let element = newChild as any; + const { rawDOMAppendOrInsertBefore, isInvokedByMicroApp, containerConfigGetter } = opts; + if (!isInvokedByMicroApp(element)) { + return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T; + } + + if (element.tagName) { + const containerConfig = containerConfigGetter(element); + const { + appName, + appWrapperGetter, + proxy, + strictGlobal, + dynamicStyleSheetElements, + scopedCSS, + excludeAssetFilter, + } = containerConfig; + + switch (element.tagName) { + case LINK_TAG_NAME: + case STYLE_TAG_NAME: { + const stylesheetElement: HTMLLinkElement | HTMLStyleElement = newChild as any; + const { href } = stylesheetElement as HTMLLinkElement; + if (excludeAssetFilter && href && excludeAssetFilter(href)) { + return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T; + } + + const mountDOM = appWrapperGetter(); + + if (scopedCSS) { + css.process(mountDOM, stylesheetElement, appName); + } + + // eslint-disable-next-line no-shadow + dynamicStyleSheetElements.push(stylesheetElement); + const referenceNode = mountDOM.contains(refChild) ? refChild : null; + return rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode); + } + + case SCRIPT_TAG_NAME: { + const { src, text } = element as HTMLScriptElement; + // some script like jsonp maybe not support cors which should't use execScripts + if (excludeAssetFilter && src && excludeAssetFilter(src)) { + return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T; + } + + const mountDOM = appWrapperGetter(); + const { fetch } = frameworkConfiguration; + const referenceNode = mountDOM.contains(refChild) ? refChild : null; + + if (src) { + execScripts(null, [src], proxy, { + fetch, + strictGlobal, + beforeExec: () => { + Object.defineProperty(document, 'currentScript', { + get(): any { + return element; + }, + configurable: true, + }); + }, + success: () => { + // we need to invoke the onload event manually to notify the event listener that the script was completed + // here are the two typical ways of dynamic script loading + // 1. element.onload callback way, which webpack and loadjs used, see https://github.com/muicss/loadjs/blob/master/src/loadjs.js#L138 + // 2. addEventListener way, which toast-loader used, see https://github.com/pyrsmk/toast/blob/master/src/Toast.ts#L64 + const loadEvent = new CustomEvent('load'); + if (isFunction(element.onload)) { + element.onload(patchCustomEvent(loadEvent, () => element)); + } else { + element.dispatchEvent(loadEvent); + } + + element = null; + }, + error: () => { + const errorEvent = new CustomEvent('error'); + if (isFunction(element.onerror)) { + element.onerror(patchCustomEvent(errorEvent, () => element)); + } else { + element.dispatchEvent(errorEvent); + } + + element = null; + }, + }); + + const dynamicScriptCommentElement = document.createComment(`dynamic script ${src} replaced by qiankun`); + dynamicScriptAttachedCommentMap.set(element, dynamicScriptCommentElement); + return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicScriptCommentElement, referenceNode); + } + + execScripts(null, [``], proxy, { + strictGlobal, + success: element.onload, + error: element.onerror, + }); + const dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun'); + dynamicScriptAttachedCommentMap.set(element, dynamicInlineScriptCommentElement); + return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, referenceNode); + } + + default: + break; + } + } + + return rawDOMAppendOrInsertBefore.call(this, element, refChild); + }; +} + +function getNewRemoveChild( + headOrBodyRemoveChild: typeof HTMLElement.prototype.removeChild, + appWrapperGetterGetter: (element: HTMLElement) => ContainerConfig['appWrapperGetter'], +) { + return function removeChild(this: HTMLHeadElement | HTMLBodyElement, child: T) { + try { + const { tagName } = child as any; + if (isHijackingTag(tagName)) { + const appWrapperGetter = appWrapperGetterGetter(child as any); + + // container may had been removed while app unmounting if the removeChild action was async + const container = appWrapperGetter(); + const attachedElement = dynamicScriptAttachedCommentMap.get(child as any) || child; + if (container.contains(attachedElement)) { + return rawRemoveChild.call(container, attachedElement) as T; + } + } + } catch (e) { + console.warn(e); + } + + return headOrBodyRemoveChild.call(this, child) as T; + }; +} + +export function patchHTMLDynamicAppendPrototypeFunctions( + isInvokedByMicroApp: (element: HTMLElement) => boolean, + containerConfigGetter: (element: HTMLElement) => ContainerConfig, +) { + // Just overwrite it while it have not been overwrite + if ( + HTMLHeadElement.prototype.appendChild === rawHeadAppendChild && + HTMLBodyElement.prototype.appendChild === rawBodyAppendChild && + HTMLHeadElement.prototype.insertBefore === rawHeadInsertBefore + ) { + HTMLHeadElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({ + rawDOMAppendOrInsertBefore: rawHeadAppendChild, + containerConfigGetter, + isInvokedByMicroApp, + }) as typeof rawHeadAppendChild; + HTMLBodyElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({ + rawDOMAppendOrInsertBefore: rawBodyAppendChild, + containerConfigGetter, + isInvokedByMicroApp, + }) as typeof rawBodyAppendChild; + + HTMLHeadElement.prototype.insertBefore = getOverwrittenAppendChildOrInsertBefore({ + rawDOMAppendOrInsertBefore: rawHeadInsertBefore as any, + containerConfigGetter, + isInvokedByMicroApp, + }) as typeof rawHeadInsertBefore; + } + + // Just overwrite it while it have not been overwrite + if ( + HTMLHeadElement.prototype.removeChild === rawHeadRemoveChild && + HTMLBodyElement.prototype.removeChild === rawBodyRemoveChild + ) { + HTMLHeadElement.prototype.removeChild = getNewRemoveChild( + rawHeadRemoveChild, + element => containerConfigGetter(element).appWrapperGetter, + ); + HTMLBodyElement.prototype.removeChild = getNewRemoveChild( + rawBodyRemoveChild, + element => containerConfigGetter(element).appWrapperGetter, + ); + } + + return function unpatch() { + HTMLHeadElement.prototype.appendChild = rawHeadAppendChild; + HTMLHeadElement.prototype.removeChild = rawHeadRemoveChild; + HTMLBodyElement.prototype.appendChild = rawBodyAppendChild; + HTMLBodyElement.prototype.removeChild = rawBodyRemoveChild; + + HTMLHeadElement.prototype.insertBefore = rawHeadInsertBefore; + }; +} + +export function rebuildCSSRules( + styleSheetElements: HTMLStyleElement[], + reAppendElement: (stylesheetElement: HTMLStyleElement) => void, +) { + styleSheetElements.forEach(stylesheetElement => { + // re-append the dynamic stylesheet to sub-app container + // Using document.head.appendChild ensures that appendChild invocation can also directly use the HTMLHeadElement.prototype.appendChild method which is overwritten at mounting phase + reAppendElement(stylesheetElement); + + /* + get the stored css rules from styled-components generated element, and the re-insert rules for them. + note that we must do this after style element had been added to document, which stylesheet would be associated to the document automatically. + check the spec https://www.w3.org/TR/cssom-1/#associated-css-style-sheet + */ + if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) { + const cssRules = getStyledElementCSSRules(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); + } + } + } + }); +} diff --git a/src/sandbox/patchers/dynamicAppend/forLooseSandbox.ts b/src/sandbox/patchers/dynamicAppend/forLooseSandbox.ts new file mode 100644 index 0000000..6cdc99f --- /dev/null +++ b/src/sandbox/patchers/dynamicAppend/forLooseSandbox.ts @@ -0,0 +1,84 @@ +/** + * @author Kuitos + * @since 2020-10-13 + */ + +import { checkActivityFunctions } from 'single-spa'; +import { Freer } from '../../../interfaces'; +import { patchHTMLDynamicAppendPrototypeFunctions, rebuildCSSRules, recordStyledComponentsCSSRules } from './common'; + +let bootstrappingPatchCount = 0; +let mountingPatchCount = 0; + +/** + * Just hijack dynamic head append, that could avoid accidentally hijacking the insertion of elements except in head. + * Such a case: ReactDOM.createPortal(, container), + * this could made we append the style element into app wrapper but it will cause an error while the react portal unmounting, as ReactDOM could not find the style in body children list. + * @param appName + * @param appWrapperGetter + * @param proxy + * @param mounting + * @param scopedCSS + * @param excludeAssetFilter + */ +export function patchLooseSandbox( + appName: string, + appWrapperGetter: () => HTMLElement | ShadowRoot, + proxy: Window, + mounting = true, + scopedCSS = false, + excludeAssetFilter?: CallableFunction, +): Freer { + let dynamicStyleSheetElements: Array = []; + + const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions( + /* + check if the currently specified application is active + While we switch page from qiankun app to a normal react routing page, the normal one may load stylesheet dynamically while page rendering, + but the url change listener must to wait until the current call stack is flushed. + This scenario may cause we record the stylesheet from react routing page dynamic injection, + and remove them after the url change triggered and qiankun app is unmouting + see https://github.com/ReactTraining/history/blob/master/modules/createHashHistory.js#L222-L230 + */ + () => checkActivityFunctions(window.location).some(name => name === appName), + () => ({ + appName, + appWrapperGetter, + proxy, + strictGlobal: false, + scopedCSS, + dynamicStyleSheetElements, + excludeAssetFilter, + }), + ); + + if (!mounting) bootstrappingPatchCount++; + if (mounting) mountingPatchCount++; + + return function free() { + // bootstrap patch just called once but its freer will be called multiple times + if (!mounting && bootstrappingPatchCount !== 0) bootstrappingPatchCount--; + if (mounting) mountingPatchCount--; + + const allMicroAppUnmounted = mountingPatchCount === 0 && bootstrappingPatchCount === 0; + // release the overwrite prototype after all the micro apps unmounted + if (allMicroAppUnmounted) unpatchDynamicAppendPrototypeFunctions(); + + recordStyledComponentsCSSRules(dynamicStyleSheetElements); + + // As now the sub app content all wrapped with a special id container, + // the dynamic style sheet would be removed automatically while unmoutting + + return function rebuild() { + rebuildCSSRules(dynamicStyleSheetElements, stylesheetElement => + // Using document.head.appendChild ensures that appendChild invocation can also directly use the HTMLHeadElement.prototype.appendChild method which is overwritten at mounting phase + document.head.appendChild.call(appWrapperGetter(), stylesheetElement), + ); + + // As the patcher will be invoked every mounting phase, we could release the cache for gc after rebuilding + if (mounting) { + dynamicStyleSheetElements = []; + } + }; + }; +} diff --git a/src/sandbox/patchers/dynamicAppend/forStrictSandbox.ts b/src/sandbox/patchers/dynamicAppend/forStrictSandbox.ts new file mode 100644 index 0000000..ec2e082 --- /dev/null +++ b/src/sandbox/patchers/dynamicAppend/forStrictSandbox.ts @@ -0,0 +1,107 @@ +/** + * @author Kuitos + * @since 2020-10-13 + */ + +import { Freer } from '../../../interfaces'; +import { attachDocProxySymbol } from '../../common'; +import { + ContainerConfig, + isHijackingTag, + patchHTMLDynamicAppendPrototypeFunctions, + rawHeadAppendChild, + rebuildCSSRules, + recordStyledComponentsCSSRules, +} from './common'; + +const rawDocumentCreateElement = Document.prototype.createElement; +const proxyAttachContainerConfigMap = new WeakMap(); + +const elementAttachContainerConfigMap = new WeakMap(); +function patchDocumentCreateElement() { + if (Document.prototype.createElement === rawDocumentCreateElement) { + Document.prototype.createElement = function createElement( + this: Document, + tagName: K, + options?: ElementCreationOptions, + ): HTMLElement { + const element = rawDocumentCreateElement.call(this, tagName, options); + if (isHijackingTag(tagName)) { + const proxyContainerConfig = proxyAttachContainerConfigMap.get(this[attachDocProxySymbol]); + if (proxyContainerConfig) { + elementAttachContainerConfigMap.set(element, proxyContainerConfig); + } + } + + return element; + }; + } + + return function unpatch() { + Document.prototype.createElement = rawDocumentCreateElement; + }; +} + +let bootstrappingPatchCount = 0; +let mountingPatchCount = 0; + +export function patchStrictSandbox( + appName: string, + appWrapperGetter: () => HTMLElement | ShadowRoot, + proxy: Window, + mounting = true, + scopedCSS = false, + excludeAssetFilter?: CallableFunction, +): Freer { + let containerConfig = proxyAttachContainerConfigMap.get(proxy); + if (!containerConfig) { + containerConfig = { + appName, + proxy, + appWrapperGetter, + dynamicStyleSheetElements: [], + strictGlobal: true, + excludeAssetFilter, + scopedCSS, + }; + proxyAttachContainerConfigMap.set(proxy, containerConfig); + } + // all dynamic style sheets are stored in proxy container + const { dynamicStyleSheetElements } = containerConfig; + + const unpatchDocumentCreate = patchDocumentCreateElement(); + + const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions( + element => elementAttachContainerConfigMap.has(element), + element => elementAttachContainerConfigMap.get(element)!, + ); + + if (!mounting) bootstrappingPatchCount++; + if (mounting) mountingPatchCount++; + + return function free() { + // bootstrap patch just called once but its freer will be called multiple times + if (!mounting && bootstrappingPatchCount !== 0) bootstrappingPatchCount--; + if (mounting) mountingPatchCount--; + + const allMicroAppUnmounted = mountingPatchCount === 0 && bootstrappingPatchCount === 0; + // release the overwrite prototype after all the micro apps unmounted + if (allMicroAppUnmounted) { + unpatchDynamicAppendPrototypeFunctions(); + unpatchDocumentCreate(); + } + + proxyAttachContainerConfigMap.delete(proxy); + + recordStyledComponentsCSSRules(dynamicStyleSheetElements); + + // As now the sub app content all wrapped with a special id container, + // the dynamic style sheet would be removed automatically while unmoutting + + return function rebuild() { + rebuildCSSRules(dynamicStyleSheetElements, stylesheetElement => + rawHeadAppendChild.call(appWrapperGetter(), stylesheetElement), + ); + }; + }; +} diff --git a/src/sandbox/patchers/dynamicAppend/index.ts b/src/sandbox/patchers/dynamicAppend/index.ts new file mode 100644 index 0000000..e279b06 --- /dev/null +++ b/src/sandbox/patchers/dynamicAppend/index.ts @@ -0,0 +1,7 @@ +/** + * @author Kuitos + * @since 2020-10-13 + */ + +export { patchLooseSandbox } from './forLooseSandbox'; +export { patchStrictSandbox } from './forStrictSandbox'; diff --git a/src/sandbox/patchers/index.ts b/src/sandbox/patchers/index.ts index 67337ee..fa41f5b 100644 --- a/src/sandbox/patchers/index.ts +++ b/src/sandbox/patchers/index.ts @@ -4,18 +4,16 @@ */ import { Freer, SandBox, SandBoxType } from '../../interfaces'; -import patchDynamicAppend from './dynamicAppend'; +import * as css from './css'; +import { patchLooseSandbox, patchStrictSandbox } from './dynamicAppend'; import patchHistoryListener from './historyListener'; import patchInterval from './interval'; import patchWindowListener from './windowListener'; -import * as css from './css'; - export function patchAtMounting( appName: string, elementGetter: () => HTMLElement | ShadowRoot, sandbox: SandBox, - singular: boolean, scopedCSS: boolean, excludeAssetFilter?: Function, ): Freer[] { @@ -23,13 +21,21 @@ export function patchAtMounting( () => patchInterval(sandbox.proxy), () => patchWindowListener(sandbox.proxy), () => patchHistoryListener(), - () => patchDynamicAppend(appName, elementGetter, sandbox.proxy, true, singular, scopedCSS, excludeAssetFilter), ]; const patchersInSandbox = { - [SandBoxType.LegacyProxy]: [...basePatchers], - [SandBoxType.Proxy]: [...basePatchers], - [SandBoxType.Snapshot]: basePatchers, + [SandBoxType.LegacyProxy]: [ + ...basePatchers, + () => patchLooseSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter), + ], + [SandBoxType.Proxy]: [ + ...basePatchers, + () => patchStrictSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter), + ], + [SandBoxType.Snapshot]: [ + ...basePatchers, + () => patchLooseSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter), + ], }; return patchersInSandbox[sandbox.type]?.map(patch => patch()); @@ -39,18 +45,19 @@ export function patchAtBootstrapping( appName: string, elementGetter: () => HTMLElement | ShadowRoot, sandbox: SandBox, - singular: boolean, scopedCSS: boolean, excludeAssetFilter?: Function, ): Freer[] { - const basePatchers = [ - () => patchDynamicAppend(appName, elementGetter, sandbox.proxy, false, singular, scopedCSS, excludeAssetFilter), - ]; - const patchersInSandbox = { - [SandBoxType.LegacyProxy]: basePatchers, - [SandBoxType.Proxy]: basePatchers, - [SandBoxType.Snapshot]: basePatchers, + [SandBoxType.LegacyProxy]: [ + () => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter), + ], + [SandBoxType.Proxy]: [ + () => patchStrictSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter), + ], + [SandBoxType.Snapshot]: [ + () => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter), + ], }; return patchersInSandbox[sandbox.type]?.map(patch => patch()); diff --git a/src/sandbox/proxySandbox.ts b/src/sandbox/proxySandbox.ts index 223c34b..19e30d2 100644 --- a/src/sandbox/proxySandbox.ts +++ b/src/sandbox/proxySandbox.ts @@ -4,10 +4,20 @@ * @since 2020-3-31 */ import { SandBox, SandBoxType } from '../interfaces'; -import { nextTick, uniq } from '../utils'; +import { nextTick } from '../utils'; import { attachDocProxySymbol, getTargetValue } from './common'; import { clearSystemJsProps, interceptSystemJsProps } from './noise/systemjs'; +/** + * fastest(at most time) unique array method + * @see https://jsperf.com/array-filter-unique/30 + */ +function uniq(array: PropertyKey[]) { + return array.filter(function filter(this: PropertyKey[], element) { + return element in this ? false : ((this as any)[element] = true); + }, {}); +} + // zone.js will overwrite Object.defineProperty const rawObjectDefineProperty = Object.defineProperty; diff --git a/src/utils.ts b/src/utils.ts index 5e10349..f02e2e9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -65,16 +65,6 @@ export function isBoundedFunction(fn: CallableFunction) { return bounded; } -/** - * fastest(at most time) unique array method - * @see https://jsperf.com/array-filter-unique/30 - */ -export function uniq(array: PropertyKey[]) { - return array.filter(function filter(this: string[], element) { - return element in this ? false : ((this as any)[element] = true); - }, {}); -} - export function getDefaultTplWrapper(id: string, name: string) { return (tpl: string) => `
${tpl}
`; } @@ -120,23 +110,23 @@ export function performanceMark(markName: string) { } export function performanceMeasure(measureName: string, markName: string) { - if (supportsUserTiming) { + if (supportsUserTiming && performance.getEntriesByName(markName, 'mark').length) { performance.measure(measureName, markName); performance.clearMarks(markName); performance.clearMeasures(measureName); } } -export function isEnableScopedCSS(opt: FrameworkConfiguration) { - if (typeof opt.sandbox !== 'object') { +export function isEnableScopedCSS(sandbox: FrameworkConfiguration['sandbox']) { + if (typeof sandbox !== 'object') { return false; } - if (opt.sandbox.strictStyleIsolation) { + if (sandbox.strictStyleIsolation) { return false; } - return !!opt.sandbox.experimentalStyleIsolation; + return !!sandbox.experimentalStyleIsolation; } /**