From 7a0dc3e5c213363b57e49339c3059eef7277953d Mon Sep 17 00:00:00 2001 From: Kuitos Date: Thu, 23 Nov 2023 19:38:48 +0800 Subject: [PATCH 1/2] feat: add unloadMicroApp api --- packages/qiankun/src/apis/loadMicroApp.ts | 23 ++++ packages/qiankun/src/apis/unloadMicroApp.ts | 11 ++ packages/qiankun/src/index.ts | 1 + .../sandbox/src/core/compartment/index.ts | 7 +- packages/sandbox/src/core/membrane/index.ts | 111 +++--------------- packages/sandbox/src/core/membrane/utils.ts | 93 ++++++++++++++- .../src/core/sandbox/StandardSandbox.ts | 15 +-- packages/sandbox/src/core/sandbox/index.ts | 7 +- packages/sandbox/src/core/sandbox/types.ts | 4 + .../src/patchers/dynamicAppend/common.ts | 7 +- .../dynamicAppend/forStandardSandbox.ts | 16 ++- .../src/patchers/dynamicAppend/index.ts | 2 +- .../src/patchers/dynamicAppend/types.ts | 8 +- packages/sandbox/src/patchers/index.ts | 8 +- 14 files changed, 185 insertions(+), 128 deletions(-) create mode 100644 packages/qiankun/src/apis/unloadMicroApp.ts diff --git a/packages/qiankun/src/apis/loadMicroApp.ts b/packages/qiankun/src/apis/loadMicroApp.ts index 26b075258..179c4a006 100644 --- a/packages/qiankun/src/apis/loadMicroApp.ts +++ b/packages/qiankun/src/apis/loadMicroApp.ts @@ -110,3 +110,26 @@ export function loadMicroApp( return microApp; } + +export function unload(appName: string): Promise { + const appConfigCaches = Array.from(appConfigPromiseGetterMap.entries()).filter(([key]) => key.startsWith(appName)); + if (appConfigCaches.length) { + return Promise.all( + appConfigCaches.map(([key]) => { + appConfigPromiseGetterMap.delete(key); + const microApps = containerMicroAppsMap.get(key); + if (microApps?.length) { + containerMicroAppsMap.delete(key); + return microApps.map(async (microApp) => { + await microApp.unmount(); + // todo microApp.unload + }); + } + + return; + }), + ); + } + + return Promise.resolve(); +} diff --git a/packages/qiankun/src/apis/unloadMicroApp.ts b/packages/qiankun/src/apis/unloadMicroApp.ts new file mode 100644 index 000000000..a4d0bde75 --- /dev/null +++ b/packages/qiankun/src/apis/unloadMicroApp.ts @@ -0,0 +1,11 @@ +import { unloadApplication } from 'single-spa'; +import { unload } from './loadMicroApp'; +import { microApps } from './registerMicroApps'; + +export async function unloadMicroApp(appName: string): Promise { + if (microApps.some((app) => app.name === appName)) { + return unloadApplication(appName, { waitForUnmount: true }); + } + + return unload(appName); +} diff --git a/packages/qiankun/src/index.ts b/packages/qiankun/src/index.ts index 68d8a01e1..8a8a498dd 100644 --- a/packages/qiankun/src/index.ts +++ b/packages/qiankun/src/index.ts @@ -1,4 +1,5 @@ export * from './apis/loadMicroApp'; +export * from './apis/unloadMicroApp'; export * from './apis/registerMicroApps'; export * from './apis/isRuntimeCompatible'; export * from './types'; diff --git a/packages/sandbox/src/core/compartment/index.ts b/packages/sandbox/src/core/compartment/index.ts index 5cff17bcd..bd802423e 100644 --- a/packages/sandbox/src/core/compartment/index.ts +++ b/packages/sandbox/src/core/compartment/index.ts @@ -6,6 +6,7 @@ // } import { nativeGlobal } from '../../consts'; +import type { Disposable } from '../sandbox/types'; const compartmentGlobalIdPrefix = '__compartment_globalThis__'; const compartmentGlobalIdSuffix = '__'; @@ -21,7 +22,7 @@ declare global { let compartmentCounter = 0; -export class Compartment { +export class Compartment implements Disposable { /** * Since the time of execution of the code in Compartment is determined by the browser, a unique compartmentSpecifier should be generated in Compartment */ @@ -61,6 +62,10 @@ export class Compartment { return `;(function(){with(this){${globalObjectOptimizer}${source}\n${sourceMapURL}}}).bind(window.${this.id})();`; } + dispose() { + delete nativeGlobal[this.id]; + } + // TODO add return value // evaluate(code: string, options?: CompartmentOptions): void { // const { transforms } = options || {}; diff --git a/packages/sandbox/src/core/membrane/index.ts b/packages/sandbox/src/core/membrane/index.ts index a5e3f2307..3f281983d 100644 --- a/packages/sandbox/src/core/membrane/index.ts +++ b/packages/sandbox/src/core/membrane/index.ts @@ -1,18 +1,9 @@ /* eslint-disable no-param-reassign */ -import { - create, - defineProperty, - freeze, - getOwnPropertyDescriptor, - getOwnPropertyNames, - hasOwnProperty, - keys, -} from '@qiankunjs/shared'; +import { defineProperty, getOwnPropertyDescriptor, hasOwnProperty, keys } from '@qiankunjs/shared'; import { nativeGlobal } from '../../consts'; import { isPropertyFrozen } from '../../utils'; -import { globalsInBrowser } from '../globals'; -import { array2TruthyObject } from '../utils'; -import { rebindTarget2Fn } from './utils'; +import type { Disposable } from '../sandbox/types'; +import { createMembraneTarget, isNativeGlobalProp, rebindTarget2Fn, uniq } from './utils'; declare global { interface Window { @@ -52,31 +43,17 @@ const useNativeWindowForBindingsProps = new Map([ ['mockDomAPIInBlackList', process.env.NODE_ENV === 'test'], ]); -const isPropertyDescriptor = (v: unknown): boolean => { - return ( - typeof v === 'object' && - v !== null && - ['value', 'writable', 'get', 'set', 'configurable', 'enumerable'].some((p) => p in v) - ); -}; - -const cachedGlobalsInBrowser = array2TruthyObject( - globalsInBrowser.concat(process.env.NODE_ENV === 'test' ? ['mockNativeWindowFunction'] : []), -); -const isNativeGlobalProp = (prop: string): boolean => { - return prop in cachedGlobalsInBrowser; -}; - -export class Membrane { +export class Membrane implements Disposable { private locking = false; - modifications = new Set(); + private readonly realmContextHandler: ProxyHandler; + realmContext: WindowProxy; - realmGlobal: WindowProxy; + modifications = new Set(); target: MembraneTarget; - latestSetProp: PropertyKey | undefined; + latestSetProp?: PropertyKey; constructor( incubatorContext: WindowProxy, @@ -93,8 +70,7 @@ export class Membrane { const { target, propertiesWithGetter } = createMembraneTarget(endowments, incubatorContext); this.target = target; - - this.realmGlobal = new Proxy(this.target, { + this.realmContextHandler = { set: (membraneTarget, p, value: never) => { if (!this.locking) { // sync the property to incubatorContext @@ -240,7 +216,8 @@ export class Membrane { getPrototypeOf() { return Reflect.getPrototypeOf(incubatorContext); }, - }) as unknown as WindowProxy; + }; + this.realmContext = new Proxy(target, this.realmContextHandler) as unknown as WindowProxy; } addIntrinsics( @@ -261,68 +238,8 @@ export class Membrane { unlock() { this.locking = false; } -} - -function createMembraneTarget( - endowments: Endowments = {}, - incubatorContext: WindowProxy, -): { - target: MembraneTarget; - propertiesWithGetter: Map; -} { - // map always has the best performance in `has` check scenario - // see https://jsperf.com/array-indexof-vs-set-has/23 - const propertiesWithGetter = new Map(); - const target: MembraneTarget = keys(endowments).reduce((acc, key) => { - const value = endowments[key]; - if (isPropertyDescriptor(value)) { - defineProperty(acc, key, value); - } else { - acc[key] = value; - } - return acc; - }, {} as MembraneTarget); - - /* - copy the non-configurable property of incubatorContext to membrane target to avoid TypeError - see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor - > A property cannot be reported as non-configurable, if it does not exist as an own property of the target object or if it exists as a configurable own property of the target object. - */ - getOwnPropertyNames(incubatorContext) - .filter((p) => { - const descriptor = getOwnPropertyDescriptor(incubatorContext, p); - return !hasOwnProperty(endowments, p) && !descriptor?.configurable; - }) - .forEach((p) => { - const descriptor = getOwnPropertyDescriptor(incubatorContext, p); - if (descriptor) { - const hasGetter = hasOwnProperty(descriptor, 'get'); - if (hasGetter) { - propertiesWithGetter.set(p, true); - } - - defineProperty( - target, - p, - // freeze the descriptor to avoid being modified by zone.js - // see https://github.com/angular/zone.js/blob/a5fe09b0fac27ac5df1fa746042f96f05ccb6a00/lib/browser/define-property.ts#L71 - freeze(descriptor), - ); - } - }); - return { - target, - propertiesWithGetter, - }; -} - -/** - * fastest(at most time) unique array method - * @see https://jsperf.com/array-filter-unique/30 - */ -function uniq(array: Array) { - return array.filter(function (this: Record, element) { - return element in this ? false : (this[element] = true); - }, create(null)); + dispose() { + Proxy.revocable(this.realmContext, this.realmContextHandler as unknown as ProxyHandler); + } } diff --git a/packages/sandbox/src/core/membrane/utils.ts b/packages/sandbox/src/core/membrane/utils.ts index 3139b76c2..cb89270dd 100644 --- a/packages/sandbox/src/core/membrane/utils.ts +++ b/packages/sandbox/src/core/membrane/utils.ts @@ -1,8 +1,97 @@ -import { defineProperty, getOwnPropertyDescriptor, getOwnPropertyNames, hasOwnProperty } from '@qiankunjs/shared'; +import { + create, + defineProperty, + freeze, + getOwnPropertyDescriptor, + getOwnPropertyNames, + hasOwnProperty, + keys, +} from '@qiankunjs/shared'; import { isBoundedFunction, isCallable, isConstructable } from '../../utils'; +import { globalsInBrowser } from '../globals'; +import { array2TruthyObject } from '../utils'; +import type { Endowments, MembraneTarget } from './index'; -const functionBoundedValueMap = new WeakMap(); +export const isPropertyDescriptor = (v: unknown): boolean => { + return ( + typeof v === 'object' && + v !== null && + ['value', 'writable', 'get', 'set', 'configurable', 'enumerable'].some((p) => p in v) + ); +}; + +const cachedGlobalsInBrowser = array2TruthyObject( + globalsInBrowser.concat(process.env.NODE_ENV === 'test' ? ['mockNativeWindowFunction'] : []), +); +export const isNativeGlobalProp = (prop: string): boolean => { + return prop in cachedGlobalsInBrowser; +}; + +export function createMembraneTarget( + endowments: Endowments = {}, + incubatorContext: WindowProxy, +): { + target: MembraneTarget; + propertiesWithGetter: Map; +} { + // map always has the best performance in `has` check scenario + // see https://jsperf.com/array-indexof-vs-set-has/23 + const propertiesWithGetter = new Map(); + const target: MembraneTarget = keys(endowments).reduce((acc, key) => { + const value = endowments[key]; + if (isPropertyDescriptor(value)) { + defineProperty(acc, key, value); + } else { + acc[key] = value; + } + return acc; + }, {} as MembraneTarget); + + /* + copy the non-configurable property of incubatorContext to membrane target to avoid TypeError + see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor + > A property cannot be reported as non-configurable, if it does not exist as an own property of the target object or if it exists as a configurable own property of the target object. + */ + getOwnPropertyNames(incubatorContext) + .filter((p) => { + const descriptor = getOwnPropertyDescriptor(incubatorContext, p); + return !hasOwnProperty(endowments, p) && !descriptor?.configurable; + }) + .forEach((p) => { + const descriptor = getOwnPropertyDescriptor(incubatorContext, p); + if (descriptor) { + const hasGetter = hasOwnProperty(descriptor, 'get'); + if (hasGetter) { + propertiesWithGetter.set(p, true); + } + + defineProperty( + target, + p, + // freeze the descriptor to avoid being modified by zone.js + // see https://github.com/angular/zone.js/blob/a5fe09b0fac27ac5df1fa746042f96f05ccb6a00/lib/browser/define-property.ts#L71 + freeze(descriptor), + ); + } + }); + + return { + target, + propertiesWithGetter, + }; +} + +/** + * fastest(at most time) unique array method + * @see https://jsperf.com/array-filter-unique/30 + */ +export function uniq(array: Array) { + return array.filter(function (this: Record, element) { + return element in this ? false : (this[element] = true); + }, create(null)); +} +const functionBoundedValueMap = new WeakMap(); export function rebindTarget2Fn(target: unknown, fn: T, receiver: unknown): T { /* 仅绑定 isCallable && !isBoundedFunction && !isConstructable 的函数对象,如 window.console、window.atob 这类,不然微应用中调用时会抛出 Illegal invocation 异常 diff --git a/packages/sandbox/src/core/sandbox/StandardSandbox.ts b/packages/sandbox/src/core/sandbox/StandardSandbox.ts index 4972c7ce9..2d335a7c9 100644 --- a/packages/sandbox/src/core/sandbox/StandardSandbox.ts +++ b/packages/sandbox/src/core/sandbox/StandardSandbox.ts @@ -18,11 +18,11 @@ export class StandardSandbox extends Compartment implements Sandbox { readonly name: string; constructor(name: string, globals: Endowments, incubatorContext: WindowProxy = window) { - const getRealmGlobal = () => realmGlobal; + const getRealmGlobal = () => realmContext; const getTopValue = (p: 'top' | 'parent'): WindowProxy => { // if your master app in an iframe context, allow these props escape the sandbox if (incubatorContext === incubatorContext.parent) { - return realmGlobal; + return realmContext; } return incubatorContext[p]!; }; @@ -38,7 +38,7 @@ export class StandardSandbox extends Compartment implements Sandbox { hasOwnProperty: { value: function hasOwnPropertyImpl(this: unknown, key: PropertyKey): boolean { // calling from hasOwnProperty.call(obj, key) - if (this !== realmGlobal && this !== null && typeof this === 'object') { + if (this !== realmContext && this !== null && typeof this === 'object') { return hasOwnProperty(this, key); } @@ -84,9 +84,9 @@ export class StandardSandbox extends Compartment implements Sandbox { endowments: { ...intrinsics, ...globals }, }); - const { realmGlobal, target } = membrane; + const { realmContext, target } = membrane; - super(realmGlobal); + super(realmContext); this.name = name; this.membrane = membrane; @@ -115,9 +115,4 @@ export class StandardSandbox extends Compartment implements Sandbox { this.membrane.lock(); } - - // TODO - // destroy() { - // - // } } diff --git a/packages/sandbox/src/core/sandbox/index.ts b/packages/sandbox/src/core/sandbox/index.ts index 6f3dabdab..974cd4544 100644 --- a/packages/sandbox/src/core/sandbox/index.ts +++ b/packages/sandbox/src/core/sandbox/index.ts @@ -2,7 +2,7 @@ * @author Kuitos * @since 2019-04-11 */ -import { patchAtBootstrapping, patchAtMounting } from '../../patchers'; +import { disposePatcher, patchAtBootstrapping, patchAtMounting } from '../../patchers'; import type { SandboxConfig } from '../../patchers/dynamicAppend/types'; import type { Free, Rebuild } from '../../patchers/types'; import type { Endowments } from '../membrane'; @@ -91,5 +91,10 @@ export function createSandboxContainer( sandbox.inactive(); }, + + async unload() { + sandbox.dispose(); + disposePatcher(sandbox); + }, }; } diff --git a/packages/sandbox/src/core/sandbox/types.ts b/packages/sandbox/src/core/sandbox/types.ts index 27256a6d6..f4ef088c4 100644 --- a/packages/sandbox/src/core/sandbox/types.ts +++ b/packages/sandbox/src/core/sandbox/types.ts @@ -10,6 +10,10 @@ export enum SandboxType { Snapshot = 'Snapshot', } +export interface Disposable { + dispose(): void; +} + export interface Sandbox extends Compartment { name: string; type: SandboxType; diff --git a/packages/sandbox/src/patchers/dynamicAppend/common.ts b/packages/sandbox/src/patchers/dynamicAppend/common.ts index ff633910f..e4108dde7 100644 --- a/packages/sandbox/src/patchers/dynamicAppend/common.ts +++ b/packages/sandbox/src/patchers/dynamicAppend/common.ts @@ -6,7 +6,7 @@ import type { AssetsTranspilerOpts, ScriptTranspilerOpts } from '@qiankunjs/shar */ import { prepareDeferredQueue } from '@qiankunjs/shared'; import { qiankunHeadTagName } from '../../consts'; -import type { SandboxConfig } from './types'; +import type { SandboxAndSandboxConfig, SandboxConfig } from './types'; const SCRIPT_TAG_NAME = 'SCRIPT'; const LINK_TAG_NAME = 'LINK'; @@ -121,7 +121,7 @@ export function getStyledElementCSSRules(styledElement: HTMLStyleElement): CSSRu export function getOverwrittenAppendChildOrInsertBefore( nativeFn: typeof HTMLElement.prototype.appendChild | typeof HTMLElement.prototype.insertBefore, - getSandboxConfig: (element: HTMLElement) => SandboxConfig | undefined, + getSandboxConfig: (element: HTMLElement) => SandboxAndSandboxConfig | undefined, target: DynamicDomMutationTarget = 'body', ) { function appendChildInSandbox( @@ -156,7 +156,7 @@ export function getOverwrittenAppendChildOrInsertBefore( refNo = Array.from(this.childNodes).indexOf(referenceNode as ChildNode); } - const { sandbox, nodeTransformer, fetch } = sandboxConfig; + const { sandbox, nodeTransformer, fetch, dynamicStyleSheetElements } = sandboxConfig; const transpiledStyleSheetElement = nodeTransformer(stylesheetElement, { fetch, sandbox, @@ -169,7 +169,6 @@ export function getOverwrittenAppendChildOrInsertBefore( if (typeof refNo === 'number' && refNo !== -1) { defineNonEnumerableProperty(transpiledStyleSheetElement, styleElementRefNodeNo, refNo); } - const { dynamicStyleSheetElements } = sandboxConfig; // record dynamic style elements after insert succeed dynamicStyleSheetElements.push(transpiledStyleSheetElement); diff --git a/packages/sandbox/src/patchers/dynamicAppend/forStandardSandbox.ts b/packages/sandbox/src/patchers/dynamicAppend/forStandardSandbox.ts index 4175c8c67..0a929c53b 100644 --- a/packages/sandbox/src/patchers/dynamicAppend/forStandardSandbox.ts +++ b/packages/sandbox/src/patchers/dynamicAppend/forStandardSandbox.ts @@ -22,7 +22,7 @@ import { styleElementRefNodeNo, styleElementTargetSymbol, } from './common'; -import type { SandboxConfig } from './types'; +import type { SandboxAndSandboxConfig, SandboxConfig } from './types'; const elementAttachedSymbol = Symbol('attachedApp'); declare global { @@ -50,10 +50,10 @@ Object.defineProperty(nativeGlobal, '__currentLockingSandbox__', { const sandboxConfigWeakMap = new WeakMap(); -const elementAttachSandboxConfigMap = new WeakMap(); +const elementAttachSandboxAndConfigMap = new WeakMap(); const patchCacheWeakMap = new WeakMap(); -const getSandboxConfig = (element: HTMLElement) => elementAttachSandboxConfigMap.get(element); +const getSandboxConfig = (element: HTMLElement) => elementAttachSandboxAndConfigMap.get(element); function patchDocument(sandbox: Sandbox, getContainer: () => HTMLElement): CallableFunction { const container = getContainer(); @@ -68,7 +68,7 @@ function patchDocument(sandbox: Sandbox, getContainer: () => HTMLElement): Calla const attachElementToSandbox = (element: HTMLElement) => { const sandboxConfig = sandboxConfigWeakMap.get(sandbox); if (sandboxConfig) { - elementAttachSandboxConfigMap.set(element, sandboxConfig); + elementAttachSandboxAndConfigMap.set(element, { ...sandboxConfig, sandbox }); } }; const getDocumentHeadElement = () => { @@ -320,7 +320,6 @@ export function patchStandardSandbox( if (!sandboxConfig) { sandboxConfig = { appName, - sandbox, fetch, nodeTransformer, dynamicStyleSheetElements: [], @@ -409,3 +408,10 @@ export function patchStandardSandbox( }; }; } + +export function disposeStandardSandbox(sandbox: Sandbox) { + const sandboxConfig = sandboxConfigWeakMap.get(sandbox); + sandboxConfig?.dynamicStyleSheetElements.splice(0); + sandboxConfig?.dynamicExternalSyncScriptDeferredList.splice(0); + sandboxConfigWeakMap.delete(sandbox); +} diff --git a/packages/sandbox/src/patchers/dynamicAppend/index.ts b/packages/sandbox/src/patchers/dynamicAppend/index.ts index 8b1b9862a..b1570a47a 100644 --- a/packages/sandbox/src/patchers/dynamicAppend/index.ts +++ b/packages/sandbox/src/patchers/dynamicAppend/index.ts @@ -3,4 +3,4 @@ * @since 2020-10-13 */ -export { patchStandardSandbox } from './forStandardSandbox'; +export { patchStandardSandbox, disposeStandardSandbox } from './forStandardSandbox'; diff --git a/packages/sandbox/src/patchers/dynamicAppend/types.ts b/packages/sandbox/src/patchers/dynamicAppend/types.ts index 03f8cf9e5..ac460447f 100644 --- a/packages/sandbox/src/patchers/dynamicAppend/types.ts +++ b/packages/sandbox/src/patchers/dynamicAppend/types.ts @@ -2,14 +2,14 @@ * @author Kuitos * @since 2023-05-04 */ -import type { BaseLoaderOpts, NodeTransformer } from '@qiankunjs/shared'; -import type { Deferred } from '@qiankunjs/shared'; -import type { Sandbox } from '../../core/sandbox'; +import type { Sandbox } from '@qiankunjs/sandbox'; +import type { BaseLoaderOpts, Deferred, NodeTransformer } from '@qiankunjs/shared'; export type SandboxConfig = { appName: string; - sandbox: Sandbox; dynamicStyleSheetElements: Array; dynamicExternalSyncScriptDeferredList: Array>; nodeTransformer: NodeTransformer; } & BaseLoaderOpts; + +export type SandboxAndSandboxConfig = { sandbox: Sandbox } & SandboxConfig; diff --git a/packages/sandbox/src/patchers/index.ts b/packages/sandbox/src/patchers/index.ts index 7e14fae3f..4ded9d863 100644 --- a/packages/sandbox/src/patchers/index.ts +++ b/packages/sandbox/src/patchers/index.ts @@ -5,7 +5,7 @@ import { SandboxType } from '../core/sandbox/types'; import { patchStandardSandbox } from './dynamicAppend'; -import type { SandboxConfig } from './dynamicAppend/types'; +import type { SandboxAndSandboxConfig } from './dynamicAppend/types'; import patchHistoryListener from './historyListener'; import patchInterval from './interval'; import type { Free } from './types'; @@ -14,7 +14,7 @@ import patchWindowListener from './windowListener'; export function patchAtBootstrapping( appName: string, getContainer: () => HTMLElement, - opts: Pick, + opts: Pick, ): Free[] { const patchersInSandbox = { [SandboxType.Standard]: [() => patchStandardSandbox(appName, getContainer, { mounting: false, ...opts })], @@ -28,7 +28,7 @@ export function patchAtBootstrapping( export function patchAtMounting( appName: string, getContainer: () => HTMLElement, - opts: Pick, + opts: Pick, ): Free[] { const { sandbox } = opts; const basePatchers = [ @@ -47,3 +47,5 @@ export function patchAtMounting( return patchersInSandbox[sandbox.type].map((patch) => patch()); } + +export { disposeStandardSandbox as disposePatcher } from './dynamicAppend'; From e90f20a87be103be801a90f9747aa7e42c64edfc Mon Sep 17 00:00:00 2001 From: Kuitos Date: Thu, 23 Nov 2023 19:42:34 +0800 Subject: [PATCH 2/2] Create wet-socks-behave.md --- .changeset/wet-socks-behave.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/wet-socks-behave.md diff --git a/.changeset/wet-socks-behave.md b/.changeset/wet-socks-behave.md new file mode 100644 index 000000000..686d7303b --- /dev/null +++ b/.changeset/wet-socks-behave.md @@ -0,0 +1,6 @@ +--- +"qiankun": patch +"@qiankunjs/sandbox": patch +--- + +feat: add unloadMicroApp api