diff --git a/src/create_app.ts b/src/create_app.ts index 3803d47c..91504010 100644 --- a/src/create_app.ts +++ b/src/create_app.ts @@ -39,6 +39,7 @@ import sourceCenter from './source/source_center' // micro app instances export const appInstanceMap = new Map() +export const appCurrentScriptMap = new Map() // params of CreateApp export interface CreateAppParam { diff --git a/src/sandbox/with/document.ts b/src/sandbox/with/document.ts index 211c106b..e5df19b1 100644 --- a/src/sandbox/with/document.ts +++ b/src/sandbox/with/document.ts @@ -14,6 +14,7 @@ import { rawDefineProperties, } from '../../libs/utils' import { + appCurrentScriptMap, appInstanceMap, } from '../../create_app' import { @@ -232,6 +233,8 @@ function createProxyDocument ( if (key === 'removeEventListener') return removeEventListener if (key === 'microAppElement') return appInstanceMap.get(appName)?.container if (key === '__MICRO_APP_NAME__') return appName + // mini-css-extract-plugin in hmr mode depends on document.currentScript to map module-id to chunk file href + if (key === 'currentScript') return appCurrentScriptMap.get(appName) return bindFunctionToRawTarget(Reflect.get(target, key), rawDocument, 'DOCUMENT') }, set: (target: Document, key: PropertyKey, value: unknown): boolean => { diff --git a/src/source/links.ts b/src/source/links.ts index 057a00db..102bac5c 100644 --- a/src/source/links.ts +++ b/src/source/links.ts @@ -161,6 +161,60 @@ export function fetchLinksFromHtml ( }) } +/** + * Get a MutationObserver that will remove the convertStyle and disabledLink when the the other is removed + * @param convertStyle converted style + * @param disabledLink disabled link + * @returns MutationObserver + */ +export function getAutoRemoveObserver (convertStyle: HTMLStyleElement, disabledLink: HTMLLinkElement): MutationObserver { + const observer = new MutationObserver((mutations, obs) => { + mutations.forEach((mutation) => { + const removedNodes = mutation.removedNodes + const removedLength = removedNodes.length + let needRemoveElement: Element | null = null + for (let i = 0; i < removedLength; i++) { + const removedNode = removedNodes[i] + if (removedNode === disabledLink) { + needRemoveElement = convertStyle + break + } + if (removedNode === convertStyle) { + needRemoveElement = disabledLink + break + } + } + if (needRemoveElement) { + obs.disconnect() + needRemoveElement.remove() + } + }) + }) + + return observer +} + +function getDisabledLink (convertStyle: HTMLStyleElement, linkInfo: LinkSourceInfo, app: AppInterface) { + const disabledLink = pureCreateElement('link') + const appSpaceData = linkInfo.appSpace[app.name] + appSpaceData.attrs?.forEach((value, key) => { + if (key === 'href') { + globalEnv.rawSetAttribute.call(disabledLink, 'data-origin-href', value) + globalEnv.rawSetAttribute.call(disabledLink, 'href', CompletionPath(value, app.url)) + } else { + globalEnv.rawSetAttribute.call(disabledLink, key, value) + } + }) + globalEnv.rawSetAttribute.call(disabledLink, 'disabled', 'true') + + const observer = getAutoRemoveObserver(convertStyle, disabledLink) + + return { + disabledLink, + observer, + } +} + /** * Fetch link succeeded, replace placeholder with style tag * NOTE: @@ -198,6 +252,10 @@ export function fetchLinkSuccess ( if (placeholder) { const convertStyle = pureCreateElement('style') + // mini-css-extract-plugin in hmr mode updates css by finding the element and replacing it. + // so we need to create a disabled link for the hmr to work + const { disabledLink, observer } = getDisabledLink(convertStyle, linkInfo, app) + handleConvertStyle( app, address, @@ -207,9 +265,15 @@ export function fetchLinkSuccess ( ) if (placeholder.parentNode) { - placeholder.parentNode.replaceChild(convertStyle, placeholder) + const parentNode = placeholder.parentNode + parentNode.insertBefore(convertStyle, placeholder) + parentNode.insertBefore(disabledLink, placeholder) + parentNode.removeChild(placeholder) + observer.observe(parentNode, { childList: true }) } else { microAppHead.appendChild(convertStyle) + microAppHead.appendChild(disabledLink) + observer.observe(microAppHead, { childList: true }) } // clear placeholder diff --git a/src/source/patch.ts b/src/source/patch.ts index a37fc900..a1bfd189 100644 --- a/src/source/patch.ts +++ b/src/source/patch.ts @@ -32,11 +32,13 @@ import { isImageElement, isVideoElement, isAudioElement, + defer, } from '../libs/utils' import scopedCSS from '../sandbox/scoped_css' import { extractLinkFromHtml, formatDynamicLink, + getAutoRemoveObserver, } from './links' import { extractScriptElement, @@ -104,6 +106,17 @@ function handleNewNode(child: Node, app: AppInterface): Node { if (address && linkInfo) { const replaceStyle = formatDynamicLink(address, app, linkInfo, child) dynamicElementInMicroAppMap.set(child, replaceStyle) + + // mini-css-extract-plugin in hmr mode updates css by finding the element and replacing it. + // so we need to insert the disabled link after the convertStyle + defer(() => { + globalEnv.rawSetAttribute.call(child, 'disabled', 'true') + globalEnv.rawInsertAdjacentElement.call(replaceStyle, 'afterend', child) + const observer = getAutoRemoveObserver(replaceStyle, child) + if (replaceStyle.parentElement) { + observer.observe(replaceStyle.parentElement, { childList: true }) + } + }) return replaceStyle } else if (replaceComment) { dynamicElementInMicroAppMap.set(child, replaceComment) diff --git a/src/source/scripts.ts b/src/source/scripts.ts index 5fe38efd..51b623ac 100644 --- a/src/source/scripts.ts +++ b/src/source/scripts.ts @@ -34,6 +34,7 @@ import microApp from '../micro_app' import globalEnv from '../libs/global_env' import { GLOBAL_CACHED_KEY } from '../constants' import sourceCenter from './source_center' +import { appCurrentScriptMap } from '../create_app' export type moduleCallBack = Func & { moduleCount?: number, errorCount?: number } @@ -665,7 +666,17 @@ function runParsedFunction (app: AppInterface, scriptInfo: ScriptSourceInfo) { if (!appSpaceData.parsedFunction) { appSpaceData.parsedFunction = getParsedFunction(app, scriptInfo, appSpaceData.parsedCode!) } - appSpaceData.parsedFunction.call(getEffectWindow(app)) + const targetWindow = getEffectWindow(app) + const dummyScriptTag = document.createElement('script') + dummyScriptTag.src = scriptInfo.appSpace[app.name].attrs.get('src') || '' + appCurrentScriptMap.set(app.name, dummyScriptTag) + try { + appSpaceData.parsedFunction.call(targetWindow) + } finally { + if (appCurrentScriptMap.get(app.name) === dummyScriptTag) { + appCurrentScriptMap.delete(app.name) + } + } } /**