🐛 simulate micro app head element for dynamic appending styles (#2097)

This commit is contained in:
Kuitos 2022-05-23 12:18:22 +08:00 committed by GitHub
parent 7fa3eb28f3
commit 526a3f610c
10 changed files with 61 additions and 16 deletions

View File

@ -3,7 +3,7 @@
* @since 2020-04-10
*/
import { initGlobalState, getMicroAppStateActions } from '../globalState';
import { getMicroAppStateActions, initGlobalState } from '../globalState';
const master = initGlobalState({ user: 'qiankun' });

View File

@ -1,3 +1,4 @@
import { version } from '../../package.json';
import {
Deferred,
genAppInstanceIdByName,
@ -8,7 +9,6 @@ import {
sleep,
validateExportLifecycle,
} from '../utils';
import { version } from '../../package.json';
test('should wrap the id [1]', () => {
const id = 'REACT16';

View File

@ -5,9 +5,8 @@
import { concat, mergeWith } from 'lodash';
import type { FrameworkLifeCycles, ObjectType } from '../interfaces';
import getRuntimePublicPathAddOn from './runtimePublicPath';
import getEngineFlagAddon from './engineFlag';
import getRuntimePublicPathAddOn from './runtimePublicPath';
export default function getAddOns<T extends ObjectType>(global: Window, publicPath: string): FrameworkLifeCycles<T> {
return mergeWith({}, getEngineFlagAddon(global), getRuntimePublicPathAddOn(global, publicPath), (v1, v2) =>

View File

@ -8,8 +8,8 @@ import { patchAtBootstrapping, patchAtMounting } from './patchers';
import ProxySandbox from './proxySandbox';
import SnapshotSandbox from './snapshotSandbox';
export { css } from './patchers';
export { getCurrentRunningApp } from './common';
export { css } from './patchers';
/**
*

View File

@ -3,8 +3,8 @@
* @since 2020-04-19
*/
import { ScopedCSS } from '../css';
import { sleep } from '../../../utils';
import { ScopedCSS } from '../css';
let CSSProcessor: ScopedCSS;
beforeAll(() => {

View File

@ -1,4 +1,4 @@
import { rebuildCSSRules, recordStyledComponentsCSSRules, getStyledElementCSSRules } from '../common';
import { getStyledElementCSSRules, rebuildCSSRules, recordStyledComponentsCSSRules } from '../common';
jest.mock('import-html-entry', () => ({
execScripts: jest.fn(),

View File

@ -5,7 +5,7 @@
import { execScripts } from 'import-html-entry';
import { isFunction } from 'lodash';
import { frameworkConfiguration } from '../../../apis';
import { qiankunHeadTagName } from '../../../utils';
import * as css from '../css';
export const rawHeadAppendChild = HTMLHeadElement.prototype.appendChild;
@ -19,6 +19,25 @@ const SCRIPT_TAG_NAME = 'SCRIPT';
const LINK_TAG_NAME = 'LINK';
const STYLE_TAG_NAME = 'STYLE';
export const styleElementTargetSymbol = Symbol('target');
type DynamicAppendTarget = 'head' | 'body';
declare global {
interface HTMLLinkElement {
[styleElementTargetSymbol]: DynamicAppendTarget;
}
interface HTMLStyleElement {
[styleElementTargetSymbol]: DynamicAppendTarget;
}
}
export const getAppWrapperHeadElement = (appWrapper: Element | ShadowRoot) => {
const rootElement = 'host' in appWrapper ? appWrapper.host : appWrapper;
return rootElement.getElementsByTagName(qiankunHeadTagName)[0];
};
export function isExecutableScriptType(script: HTMLScriptElement) {
return (
!script.type ||
@ -149,6 +168,7 @@ function getOverwrittenAppendChildOrInsertBefore(opts: {
rawDOMAppendOrInsertBefore: <T extends Node>(newChild: T, refChild?: Node | null) => T;
isInvokedByMicroApp: (element: HTMLElement) => boolean;
containerConfigGetter: (element: HTMLElement) => ContainerConfig;
target: DynamicAppendTarget;
}) {
return function appendChildOrInsertBefore<T extends Node>(
this: HTMLHeadElement | HTMLBodyElement,
@ -156,7 +176,7 @@ function getOverwrittenAppendChildOrInsertBefore(opts: {
refChild: Node | null = null,
) {
let element = newChild as any;
const { rawDOMAppendOrInsertBefore, isInvokedByMicroApp, containerConfigGetter } = opts;
const { rawDOMAppendOrInsertBefore, isInvokedByMicroApp, containerConfigGetter, target = 'body' } = opts;
if (!isHijackingTag(element.tagName) || !isInvokedByMicroApp(element)) {
return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T;
}
@ -182,7 +202,14 @@ function getOverwrittenAppendChildOrInsertBefore(opts: {
return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T;
}
const mountDOM = appWrapperGetter();
Object.defineProperty(stylesheetElement, styleElementTargetSymbol, {
value: target,
writable: true,
configurable: true,
});
const appWrapper = appWrapperGetter();
const mountDOM = target === 'head' ? getAppWrapperHeadElement(appWrapper) : appWrapper;
if (scopedCSS) {
// exclude link elements like <link rel="icon" href="favicon.ico">
@ -326,17 +353,20 @@ export function patchHTMLDynamicAppendPrototypeFunctions(
rawDOMAppendOrInsertBefore: rawHeadAppendChild,
containerConfigGetter,
isInvokedByMicroApp,
target: 'head',
}) as typeof rawHeadAppendChild;
HTMLBodyElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({
rawDOMAppendOrInsertBefore: rawBodyAppendChild,
containerConfigGetter,
isInvokedByMicroApp,
target: 'body',
}) as typeof rawBodyAppendChild;
HTMLHeadElement.prototype.insertBefore = getOverwrittenAppendChildOrInsertBefore({
rawDOMAppendOrInsertBefore: rawHeadInsertBefore as any,
containerConfigGetter,
isInvokedByMicroApp,
target: 'head',
}) as typeof rawHeadInsertBefore;
}

View File

@ -8,11 +8,13 @@ import { nativeGlobal } from '../../../utils';
import { getCurrentRunningApp } from '../../common';
import type { ContainerConfig } from './common';
import {
getAppWrapperHeadElement,
isHijackingTag,
patchHTMLDynamicAppendPrototypeFunctions,
rawHeadAppendChild,
rebuildCSSRules,
recordStyledComponentsCSSRules,
styleElementTargetSymbol,
} from './common';
declare global {
@ -130,7 +132,9 @@ export function patchStrictSandbox(
rebuildCSSRules(dynamicStyleSheetElements, (stylesheetElement) => {
const appWrapper = appWrapperGetter();
if (!appWrapper.contains(stylesheetElement)) {
rawHeadAppendChild.call(appWrapper, stylesheetElement);
const mountDom =
stylesheetElement[styleElementTargetSymbol] === 'head' ? getAppWrapperHeadElement(appWrapper) : appWrapper;
rawHeadAppendChild.call(mountDom, stylesheetElement);
return true;
}

View File

@ -6,7 +6,7 @@
import type { SandBox } from '../interfaces';
import { SandBoxType } from '../interfaces';
import { nativeGlobal, nextTask } from '../utils';
import { getTargetValue, setCurrentRunningApp, getCurrentRunningApp } from './common';
import { getCurrentRunningApp, getTargetValue, setCurrentRunningApp } from './common';
type SymbolTarget = 'target' | 'globalContext';

View File

@ -3,10 +3,9 @@
* @since 2019-05-15
*/
import { isFunction, snakeCase, once } from 'lodash';
import { version } from './version';
import { isFunction, once, snakeCase } from 'lodash';
import type { FrameworkConfiguration } from './interfaces';
import { version } from './version';
export function toArray<T>(array: T | T[]): T[] {
return Array.isArray(array) ? array : [array];
@ -21,6 +20,7 @@ const nextTick: (cb: () => void) => void =
typeof window.Zone === 'function' ? setTimeout : (cb) => Promise.resolve().then(cb);
let globalTaskPending = false;
/**
* Run a callback before next task executing, and the invocation is idempotent in every singular task
* That means even we called nextTask multi times in one task, only the first callback will be pushed to nextTick to be invoked.
@ -37,6 +37,7 @@ export function nextTask(cb: () => void): void {
}
const fnRegexCheckCacheMap = new WeakMap<any | FunctionConstructor, boolean>();
export function isConstructable(fn: () => any | FunctionConstructor) {
// prototype methods might be changed while code running, so we need check it every time
const hasPrototypeMethods =
@ -88,6 +89,7 @@ export const isCallable = (fn: any) => {
};
const boundedMap = new WeakMap<CallableFunction, boolean>();
export function isBoundedFunction(fn: CallableFunction) {
if (boundedMap.has(fn)) {
return boundedMap.get(fn);
@ -101,8 +103,18 @@ export function isBoundedFunction(fn: CallableFunction) {
return bounded;
}
export const qiankunHeadTagName = 'qiankun-head';
export function getDefaultTplWrapper(name: string) {
return (tpl: string) => `<div id="${getWrapperId(name)}" data-name="${name}" data-version="${version}">${tpl}</div>`;
return (tpl: string) => {
// We need to mock a head placeholder as native head element will be erased by browser in micro app
const tplWithSimulatedHead = tpl
.replace('<head>', `<${qiankunHeadTagName}>`)
.replace('</head>', `</${qiankunHeadTagName}>`);
return `<div id="${getWrapperId(
name,
)}" data-name="${name}" data-version="${version}">${tplWithSimulatedHead}</div>`;
};
}
export function getWrapperId(name: string) {