🐛 fix the simulated head issues (#2121)

This commit is contained in:
Kuitos 2022-05-29 22:41:10 +08:00
parent a885c4ab8a
commit 9c77055535
7 changed files with 57 additions and 41 deletions

View File

@ -1,12 +1,12 @@
import 'zone.js'; // for angular subapp import 'zone.js'; // for angular subapp
import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from '../../es'; import { initGlobalState, registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start } from '../../es';
import './index.less'; import './index.less';
/** /**
* 主应用 **可以使用任意技术栈** * 主应用 **可以使用任意技术栈**
* 以下分别是 React Vue 的示例可切换尝试 * 以下分别是 React Vue 的示例可切换尝试
*/ */
import render from './render/ReactRender'; import render from './render/ReactRender';
// import render from './render/VueRender'; // import render from './render/VueRender';
/** /**
@ -14,7 +14,7 @@ import render from './render/ReactRender';
*/ */
render({ loading: true }); render({ loading: true });
const loader = loading => render({ loading }); const loader = (loading) => render({ loading });
/** /**
* Step2 注册子应用 * Step2 注册子应用
@ -67,17 +67,17 @@ registerMicroApps(
], ],
{ {
beforeLoad: [ beforeLoad: [
app => { (app) => {
console.log('[LifeCycle] before load %c%s', 'color: green;', app.name); console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
}, },
], ],
beforeMount: [ beforeMount: [
app => { (app) => {
console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name); console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
}, },
], ],
afterUnmount: [ afterUnmount: [
app => { (app) => {
console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name); console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
}, },
], ],

View File

@ -2,13 +2,12 @@
* @author Kuitos * @author Kuitos
* @since 2019-05-16 * @since 2019-05-16
*/ */
import './public-path'; import 'antd/dist/antd.min.css';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import App from './App'; import App from './App';
import 'antd/dist/antd.min.css';
import './index.css'; import './index.css';
import './public-path';
export async function bootstrap() { export async function bootstrap() {
console.log('[react15] react app bootstraped'); console.log('[react15] react app bootstraped');
@ -24,6 +23,14 @@ export async function mount(props = {}) {
import('./dynamic.css').then(() => { import('./dynamic.css').then(() => {
console.log('[react15] dynamic style load'); console.log('[react15] dynamic style load');
}); });
const styleElement = document.createElement('style');
styleElement.innerText = '.react15-icon { height: 400px }';
document.head.appendChild(styleElement);
setTimeout(() => {
document.head.removeChild(styleElement);
}, 2000);
} }
export async function unmount(props) { export async function unmount(props) {

View File

@ -29,7 +29,8 @@ test('should wrap string with div', () => {
const ret = factory(tpl); const ret = factory(tpl);
expect(ret).toBe( expect(ret).toBe(
`<div id="__qiankun_microapp_wrapper_for_react_16__" data-name="react16" data-version="${version}">${tpl}</div>`, // eslint-disable-next-line max-len
`<div id="__qiankun_microapp_wrapper_for_react_16__" data-name="react16" data-version="${version}"><qiankun-head></qiankun-head>${tpl}</div>`,
); );
}); });

View File

@ -21,19 +21,19 @@ const STYLE_TAG_NAME = 'STYLE';
export const styleElementTargetSymbol = Symbol('target'); export const styleElementTargetSymbol = Symbol('target');
type DynamicAppendTarget = 'head' | 'body'; type DynamicDomMutationTarget = 'head' | 'body';
declare global { declare global {
interface HTMLLinkElement { interface HTMLLinkElement {
[styleElementTargetSymbol]: DynamicAppendTarget; [styleElementTargetSymbol]: DynamicDomMutationTarget;
} }
interface HTMLStyleElement { interface HTMLStyleElement {
[styleElementTargetSymbol]: DynamicAppendTarget; [styleElementTargetSymbol]: DynamicDomMutationTarget;
} }
} }
export const getAppWrapperHeadElement = (appWrapper: Element | ShadowRoot) => { export const getAppWrapperHeadElement = (appWrapper: Element | ShadowRoot): Element => {
const rootElement = 'host' in appWrapper ? appWrapper.host : appWrapper; const rootElement = 'host' in appWrapper ? appWrapper.host : appWrapper;
return rootElement.getElementsByTagName(qiankunHeadTagName)[0]; return rootElement.getElementsByTagName(qiankunHeadTagName)[0];
}; };
@ -158,7 +158,7 @@ export type ContainerConfig = {
appName: string; appName: string;
proxy: WindowProxy; proxy: WindowProxy;
strictGlobal: boolean; strictGlobal: boolean;
dynamicStyleSheetElements: HTMLStyleElement[]; dynamicStyleSheetElements: Array<HTMLStyleElement | HTMLLinkElement>;
appWrapperGetter: CallableFunction; appWrapperGetter: CallableFunction;
scopedCSS: boolean; scopedCSS: boolean;
excludeAssetFilter?: CallableFunction; excludeAssetFilter?: CallableFunction;
@ -168,7 +168,7 @@ function getOverwrittenAppendChildOrInsertBefore(opts: {
rawDOMAppendOrInsertBefore: <T extends Node>(newChild: T, refChild?: Node | null) => T; rawDOMAppendOrInsertBefore: <T extends Node>(newChild: T, refChild?: Node | null) => T;
isInvokedByMicroApp: (element: HTMLElement) => boolean; isInvokedByMicroApp: (element: HTMLElement) => boolean;
containerConfigGetter: (element: HTMLElement) => ContainerConfig; containerConfigGetter: (element: HTMLElement) => ContainerConfig;
target: DynamicAppendTarget; target: DynamicDomMutationTarget;
}) { }) {
return function appendChildOrInsertBefore<T extends Node>( return function appendChildOrInsertBefore<T extends Node>(
this: HTMLHeadElement | HTMLBodyElement, this: HTMLHeadElement | HTMLBodyElement,
@ -209,7 +209,6 @@ function getOverwrittenAppendChildOrInsertBefore(opts: {
}); });
const appWrapper = appWrapperGetter(); const appWrapper = appWrapperGetter();
const mountDOM = target === 'head' ? getAppWrapperHeadElement(appWrapper) : appWrapper;
if (scopedCSS) { if (scopedCSS) {
// exclude link elements like <link rel="icon" href="favicon.ico"> // exclude link elements like <link rel="icon" href="favicon.ico">
@ -224,16 +223,17 @@ function getOverwrittenAppendChildOrInsertBefore(opts: {
: frameworkConfiguration.fetch?.fn; : frameworkConfiguration.fetch?.fn;
stylesheetElement = convertLinkAsStyle( stylesheetElement = convertLinkAsStyle(
element, element,
(styleElement) => css.process(mountDOM, styleElement, appName), (styleElement) => css.process(appWrapper, styleElement, appName),
fetch, fetch,
); );
dynamicLinkAttachedInlineStyleMap.set(element, stylesheetElement); dynamicLinkAttachedInlineStyleMap.set(element, stylesheetElement);
} else { } else {
css.process(mountDOM, stylesheetElement, appName); css.process(appWrapper, stylesheetElement, appName);
} }
} }
// eslint-disable-next-line no-shadow const mountDOM = target === 'head' ? getAppWrapperHeadElement(appWrapper) : appWrapper;
dynamicStyleSheetElements.push(stylesheetElement); dynamicStyleSheetElements.push(stylesheetElement);
const referenceNode = mountDOM.contains(refChild) ? refChild : null; const referenceNode = mountDOM.contains(refChild) ? refChild : null;
return rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode); return rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode);
@ -301,7 +301,8 @@ function getOverwrittenAppendChildOrInsertBefore(opts: {
function getNewRemoveChild( function getNewRemoveChild(
headOrBodyRemoveChild: typeof HTMLElement.prototype.removeChild, headOrBodyRemoveChild: typeof HTMLElement.prototype.removeChild,
appWrapperGetterGetter: (element: HTMLElement) => ContainerConfig['appWrapperGetter'], containerConfigGetter: (element: HTMLElement) => ContainerConfig,
target: DynamicDomMutationTarget,
) { ) {
return function removeChild<T extends Node>(this: HTMLHeadElement | HTMLBodyElement, child: T) { return function removeChild<T extends Node>(this: HTMLHeadElement | HTMLBodyElement, child: T) {
const { tagName } = child as any; const { tagName } = child as any;
@ -309,14 +310,19 @@ function getNewRemoveChild(
try { try {
let attachedElement: Node; let attachedElement: Node;
const { appWrapperGetter, dynamicStyleSheetElements } = containerConfigGetter(child as any);
switch (tagName) { switch (tagName) {
case STYLE_TAG_NAME:
case LINK_TAG_NAME: { case LINK_TAG_NAME: {
attachedElement = (dynamicLinkAttachedInlineStyleMap.get(child as any) as Node) || child; attachedElement = dynamicLinkAttachedInlineStyleMap.get(child as any) || child;
// try to remove the dynamic style sheet
dynamicStyleSheetElements.splice(dynamicStyleSheetElements.indexOf(attachedElement as any), 1);
break; break;
} }
case SCRIPT_TAG_NAME: { case SCRIPT_TAG_NAME: {
attachedElement = (dynamicScriptAttachedCommentMap.get(child as any) as Node) || child; attachedElement = dynamicScriptAttachedCommentMap.get(child as any) || child;
break; break;
} }
@ -325,11 +331,11 @@ function getNewRemoveChild(
} }
} }
// container may had been removed while app unmounting if the removeChild action was async const appWrapper = appWrapperGetter();
const appWrapperGetter = appWrapperGetterGetter(child as any); const container = target === 'head' ? getAppWrapperHeadElement(appWrapper) : appWrapper;
const container = appWrapperGetter(); // container might have been removed while app unmounting if the removeChild action was async
if (container.contains(attachedElement)) { if (container.contains(attachedElement)) {
return rawRemoveChild.call(container, attachedElement) as T; return rawRemoveChild.call(attachedElement.parentNode, attachedElement) as T;
} }
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
@ -375,14 +381,8 @@ export function patchHTMLDynamicAppendPrototypeFunctions(
HTMLHeadElement.prototype.removeChild === rawHeadRemoveChild && HTMLHeadElement.prototype.removeChild === rawHeadRemoveChild &&
HTMLBodyElement.prototype.removeChild === rawBodyRemoveChild HTMLBodyElement.prototype.removeChild === rawBodyRemoveChild
) { ) {
HTMLHeadElement.prototype.removeChild = getNewRemoveChild( HTMLHeadElement.prototype.removeChild = getNewRemoveChild(rawHeadRemoveChild, containerConfigGetter, 'head');
rawHeadRemoveChild, HTMLBodyElement.prototype.removeChild = getNewRemoveChild(rawBodyRemoveChild, containerConfigGetter, 'body');
(element) => containerConfigGetter(element).appWrapperGetter,
);
HTMLBodyElement.prototype.removeChild = getNewRemoveChild(
rawBodyRemoveChild,
(element) => containerConfigGetter(element).appWrapperGetter,
);
} }
return function unpatch() { return function unpatch() {

View File

@ -117,7 +117,7 @@ export function patchStrictSandbox(
if (mounting) mountingPatchCount--; if (mounting) mountingPatchCount--;
const allMicroAppUnmounted = mountingPatchCount === 0 && bootstrappingPatchCount === 0; const allMicroAppUnmounted = mountingPatchCount === 0 && bootstrappingPatchCount === 0;
// release the overwrite prototype after all the micro apps unmounted // release the overwritten prototype after all the micro apps unmounted
if (allMicroAppUnmounted) { if (allMicroAppUnmounted) {
unpatchDynamicAppendPrototypeFunctions(); unpatchDynamicAppendPrototypeFunctions();
unpatchDocumentCreate(); unpatchDocumentCreate();

View File

@ -107,10 +107,18 @@ export const qiankunHeadTagName = 'qiankun-head';
export function getDefaultTplWrapper(name: string) { export function getDefaultTplWrapper(name: string) {
return (tpl: string) => { return (tpl: string) => {
// We need to mock a head placeholder as native head element will be erased by browser in micro app let tplWithSimulatedHead: string;
const tplWithSimulatedHead = tpl
.replace('<head>', `<${qiankunHeadTagName}>`) if (tpl.indexOf('<head>') !== -1) {
.replace('</head>', `</${qiankunHeadTagName}>`); // We need to mock a head placeholder as native head element will be erased by browser in micro app
tplWithSimulatedHead = tpl
.replace('<head>', `<${qiankunHeadTagName}>`)
.replace('</head>', `</${qiankunHeadTagName}>`);
} else {
// Some template might not be a standard html document, thus we need to add a simulated head tag for them
tplWithSimulatedHead = `<${qiankunHeadTagName}></${qiankunHeadTagName}>${tpl}`;
}
return `<div id="${getWrapperId( return `<div id="${getWrapperId(
name, name,
)}" data-name="${name}" data-version="${version}">${tplWithSimulatedHead}</div>`; )}" data-name="${name}" data-version="${version}">${tplWithSimulatedHead}</div>`;

View File

@ -1 +1 @@
export { version } from '../package.json'; export const version = '2.7.1';