diff --git a/dev/main-react16/src/pages/react16/react16.js b/dev/main-react16/src/pages/react16/react16.js index cb515071d..c3583ec21 100644 --- a/dev/main-react16/src/pages/react16/react16.js +++ b/dev/main-react16/src/pages/react16/react16.js @@ -29,6 +29,7 @@ export default class App extends React.Component { handleCreated = () => { console.log(`生命周期:created -- ${this.state.name}`) + // Promise.resolve().then(() => microApp.router.push({name: this.state.name, path: this.state.baseroute + '/page2'})) } beforemount = (e) => { diff --git a/dev/main-react16/src/pages/vite2/vite2.js b/dev/main-react16/src/pages/vite2/vite2.js index 11cfcbd86..9b33b049a 100644 --- a/dev/main-react16/src/pages/vite2/vite2.js +++ b/dev/main-react16/src/pages/vite2/vite2.js @@ -92,7 +92,7 @@ function vite2 (props) { url={`${config.vite2}micro-app/vite2/`} // url={`http://127.0.0.1:8080/micro-app/vite2/`} data={data} - onCreated={() => Promise.resolve().then(() => jumpToElementPlus())} + // onCreated={() => Promise.resolve().then(() => jumpToElementPlus())} // onBeforemount={() => jumpToElementPlus()} onMounted={handleMounted} onDataChange={handleDataChange} diff --git a/docs/1.x/zh-cn/api.md b/docs/1.x/zh-cn/api.md index 6958be38a..bbcfdd197 100644 --- a/docs/1.x/zh-cn/api.md +++ b/docs/1.x/zh-cn/api.md @@ -349,6 +349,7 @@ interface RenderAppOptions { name: string, // 应用名称,必传 url: string, // 应用地址,必传 container: string | Element, // 应用容器或选择器,必传 + iframe?: boolean, // 是否切换为iframe沙箱,可选 inline?: boolean, // 开启内联模式运行js,可选 'disable-scopecss'?: boolean, // 关闭样式隔离,可选 'disable-sandbox'?: boolean, // 关闭沙箱,可选 diff --git a/docs/1.x/zh-cn/changelog.md b/docs/1.x/zh-cn/changelog.md index bba0d5767..5289baa66 100644 --- a/docs/1.x/zh-cn/changelog.md +++ b/docs/1.x/zh-cn/changelog.md @@ -10,7 +10,7 @@ ### 1.0.0-beta.7 -`2023-09-19` +`2023-09-20` - **New** @@ -18,9 +18,11 @@ - **Bug Fix** - - 🐞 修复了在iframe模式下,子应用使用`monaco-editor`时代码输入框光标失效的问题。 + - 🐞 修复了在iframe沙箱模式下,子应用使用`monaco-editor`时代码输入框光标失效的问题。 - 🐞 修复了在`window.mount`为Promise时抛出的错误无法捕获的问题。 - - 🐞 修复了在iframe模式下,子应用加载完成之前进行导航导致报错的问题。 + - 🐞 修复了在iframe沙箱模式下,子应用加载完成之前进行导航导致报错的问题。 + - 🐞 修复了在with沙箱模式下,异步创建路由系统导致部分场景下location未定义的问题,issue [#908](https://github.com/micro-zoe/micro-app/issues/908)。 + - 🐞 修复了在micro-app子应用开始渲染到渲染完成之前通过路由API无法控制跳转的问题。 - **Update** diff --git a/src/create_app.ts b/src/create_app.ts index 21618ba33..dbc4fa6e1 100644 --- a/src/create_app.ts +++ b/src/create_app.ts @@ -80,7 +80,7 @@ export default class CreateApp implements AppInterface { public isPrerender: boolean public prefetchLevel?: number public fiber = false - public routerMode: string = DEFAULT_ROUTER_MODE + public routerMode: string constructor ({ name, @@ -93,6 +93,7 @@ export default class CreateApp implements AppInterface { ssrUrl, isPrefetch, prefetchLevel, + routerMode, }: CreateAppParam) { appInstanceMap.set(name, this) // init actions @@ -102,6 +103,11 @@ export default class CreateApp implements AppInterface { this.scopecss = this.useSandbox && scopecss this.inline = inline ?? false this.iframe = iframe ?? false + /** + * NOTE: + * 1. Navigate after micro-app created, before mount + */ + this.routerMode = routerMode || DEFAULT_ROUTER_MODE // not exist when prefetch 👇 this.container = container ?? null @@ -220,6 +226,8 @@ export default class CreateApp implements AppInterface { this.createSandbox() + this.setAppState(appStates.BEFORE_MOUNT) + const nextAction = () => { /** * Special scenes: @@ -264,8 +272,6 @@ export default class CreateApp implements AppInterface { this.fiber = fiber this.routerMode = routerMode - this.setAppState(appStates.BEFORE_MOUNT) - const dispatchBeforeMount = () => dispatchLifecyclesEvent( this.container!, this.name, @@ -654,7 +660,7 @@ export default class CreateApp implements AppInterface { } // set app state - private setAppState (state: string): void { + public setAppState (state: string): void { this.state = state } diff --git a/src/micro_app_element.ts b/src/micro_app_element.ts index 1c820c3d7..3902ea69d 100644 --- a/src/micro_app_element.ts +++ b/src/micro_app_element.ts @@ -25,6 +25,7 @@ import { import { ObservedAttrName, lifeCycles, + appStates, } from './constants' import CreateApp, { appInstanceMap, @@ -346,6 +347,7 @@ export function defineElement (tagName: string): void { inline: this.getDisposeResult('inline'), iframe: this.getDisposeResult('iframe'), ssrUrl: this.ssrUrl, + routerMode: this.getMemoryRouterMode(), }) /** @@ -377,7 +379,12 @@ export function defineElement (tagName: string): void { */ private handleMount (app: AppInterface): void { app.isPrefetch = false - // TODO: Can defer be removed? + /** + * Fix error when navigate before app.mount by microApp.router.push(...) + * Issue: https://github.com/micro-zoe/micro-app/issues/908 + */ + app.setAppState(appStates.BEFORE_MOUNT) + // exec mount async, simulate the first render scene defer(() => this.mount(app)) } diff --git a/src/sandbox/iframe/index.ts b/src/sandbox/iframe/index.ts index 3c28a6e05..0957d76b0 100644 --- a/src/sandbox/iframe/index.ts +++ b/src/sandbox/iframe/index.ts @@ -26,8 +26,8 @@ import { resetDataCenterSnapshot, } from '../../interact' import { - patchRoute, -} from './route' + patchRouter, +} from './router' import { router, initRouteStateWithURL, @@ -83,13 +83,12 @@ export default class IframeSandbox { this.microAppWindow = this.iframe!.contentWindow this.patchIframe(this.microAppWindow, (resolve: CallableFunction) => { - // TODO: 优化代码 // create new html to iframe this.createIframeTemplate(this.microAppWindow) // get escapeProperties from plugins this.getSpecialProperties(appName) // patch location & history of child app - this.proxyLocation = patchRoute(appName, url, this.microAppWindow, browserHost) + this.proxyLocation = patchRouter(appName, url, this.microAppWindow, browserHost) // patch window of child app this.windowEffect = patchWindow(appName, this.microAppWindow, this) // patch document of child app @@ -100,7 +99,7 @@ export default class IframeSandbox { * create static properties * NOTE: * 1. execute as early as possible - * 2. run after patchRoute & createProxyWindow + * 2. run after patchRouter & createProxyWindow */ this.initStaticGlobalKeys(appName, url) resolve() @@ -241,7 +240,7 @@ export default class IframeSandbox { * create static properties * NOTE: * 1. execute as early as possible - * 2. run after patchRoute & createProxyWindow + * 2. run after patchRouter & createProxyWindow */ private initStaticGlobalKeys (appName: string, url: string): void { this.microAppWindow.__MICRO_APP_ENVIRONMENT__ = true diff --git a/src/sandbox/iframe/route.ts b/src/sandbox/iframe/router.ts similarity index 97% rename from src/sandbox/iframe/route.ts rename to src/sandbox/iframe/router.ts index cc5514808..173360f53 100644 --- a/src/sandbox/iframe/route.ts +++ b/src/sandbox/iframe/router.ts @@ -13,7 +13,7 @@ import { assign, } from '../../libs/utils' -export function patchRoute ( +export function patchRouter ( appName: string, url: string, microAppWindow: microAppWindowType, diff --git a/src/sandbox/router/api.ts b/src/sandbox/router/api.ts index df604e467..cb9ed4a6d 100644 --- a/src/sandbox/router/api.ts +++ b/src/sandbox/router/api.ts @@ -9,6 +9,7 @@ import type { AccurateGuard, SetDefaultPageOptions, AttachAllToURLParam, + AppInterface, } from '@micro-app/types' import { encodeMicroPath, @@ -88,6 +89,46 @@ function createRouterApi (): RouterApi { // clear element scope after navigate removeDomScope() } + + /** + * navigation handler + * @param appName app.name + * @param app app instance + * @param to router target options + * @param replace use router.replace? + */ + function handleNavigate ( + appName: string, + app: AppInterface, + to: RouterTarget, + replace: boolean, + ): void { + const microLocation = app.sandBox!.proxyWindow.location as MicroLocation + const targetLocation = createURL(to.path, microLocation.href) + // Only get path data, even if the origin is different from microApp + const currentFullPath = microLocation.pathname + microLocation.search + microLocation.hash + const targetFullPath = targetLocation.pathname + targetLocation.search + targetLocation.hash + if (currentFullPath !== targetFullPath || getMicroPathFromURL(appName) !== targetFullPath) { + const methodName = (replace && to.replace !== false) || to.replace === true ? 'replaceState' : 'pushState' + navigateWithRawHistory(appName, methodName, targetLocation, to.state) + /** + * TODO: + * 1. 关闭虚拟路由的跳转地址不同:baseRoute + 子应用地址,文档中要说明 + * 2. 关闭虚拟路由时跳转方式不同:1、基座跳转但不发送popstate事件 2、控制子应用更新location,内部发送popstate事件。 + * 核心思路:减小对基座的影响(子应用跳转不向基座发送popstate事件,其他操作一致),但这是必要的吗,只是多了一个触发popstate的操作 + * 路由优化方案有两种: + * 1、减少对基座的影响,主要是解决vue循环刷新的问题 + * 2、全局发送popstate事件,解决主、子都是vue3的冲突问题 + * 两者选一个吧,如果选2,则下面这两行代码可以去掉 + * NOTE1: history和search模式采用2,这样可以解决vue3的问题,custom采用1,避免vue循环刷新的问题,这样在用户出现问题时各有解决方案。但反过来说,每种方案又分别导致另外的问题,不统一,导致复杂度增高 + * NOTE2: 关闭虚拟路由,同时发送popstate事件还是无法解决vue3的问题(毕竟history.state理论上还是会冲突),那么就没必要发送popstate事件了。 + */ + if (isRouterModeCustom(appName)) { + updateMicroLocationWithEvent(appName, targetFullPath) + } + } + } + /** * create method of router.push/replace * NOTE: @@ -114,30 +155,8 @@ function createRouterApi (): RouterApi { */ if (getActiveApps({ excludeHiddenApp: true, excludePreRender: true }).includes(appName)) { const app = appInstanceMap.get(appName)! - const microLocation = app.sandBox!.proxyWindow.location as MicroLocation - const targetLocation = createURL(to.path, microLocation.href) - // Only get path data, even if the origin is different from microApp - const currentFullPath = microLocation.pathname + microLocation.search + microLocation.hash - const targetFullPath = targetLocation.pathname + targetLocation.search + targetLocation.hash - if (currentFullPath !== targetFullPath || getMicroPathFromURL(appName) !== targetFullPath) { - const methodName = (replace && to.replace !== false) || to.replace === true ? 'replaceState' : 'pushState' - navigateWithRawHistory(appName, methodName, targetLocation, to.state) - /** - * TODO: - * 1. 关闭虚拟路由的跳转地址不同:baseRoute + 子应用地址,文档中要说明 - * 2. 关闭虚拟路由时跳转方式不同:1、基座跳转但不发送popstate事件 2、控制子应用更新location,内部发送popstate事件。 - * 核心思路:减小对基座的影响(子应用跳转不向基座发送popstate事件,其他操作一致),但这是必要的吗,只是多了一个触发popstate的操作 - * 路由优化方案有两种: - * 1、减少对基座的影响,主要是解决vue循环刷新的问题 - * 2、全局发送popstate事件,解决主、子都是vue3的冲突问题 - * 两者选一个吧,如果选2,则下面这两行代码可以去掉 - * NOTE1: history和search模式采用2,这样可以解决vue3的问题,custom采用1,避免vue循环刷新的问题,这样在用户出现问题时各有解决方案。但反过来说,每种方案又分别导致另外的问题,不统一,导致复杂度增高 - * NOTE2: 关闭虚拟路由,同时发送popstate事件还是无法解决vue3的问题(毕竟history.state理论上还是会冲突),那么就没必要发送popstate事件了。 - */ - if (isRouterModeCustom(appName)) { - updateMicroLocationWithEvent(appName, targetFullPath) - } - } + const navigateAction = () => handleNavigate(appName, app, to, replace) + app.iframe ? app.sandBox.sandboxReady.then(navigateAction) : navigateAction() } else { logWarn('navigation failed, app does not exist or is inactive') } diff --git a/src/sandbox/router/core.ts b/src/sandbox/router/core.ts index dc272448a..4f0c4eb26 100644 --- a/src/sandbox/router/core.ts +++ b/src/sandbox/router/core.ts @@ -241,7 +241,7 @@ export function isEffectiveApp (appName: string): boolean { const app = appInstanceMap.get(appName) /** * !!(app && !app.isPrefetch && !app.isHidden()) - * TODO: 隐藏的keep-alive应用暂时不作为无效应用,原因如下 + * NOTE: 隐藏的keep-alive应用暂时不作为无效应用,原因如下 * 1、隐藏后才执行去除浏览器上的微应用的路由信息的操作,导致微应用的路由信息无法去除 * 2、如果保持隐藏应用内部正常跳转,阻止同步路由信息到浏览器,这样理论上是好的,但是对于location跳转改如何处理?location跳转是基于修改浏览器地址后发送popstate事件实现的,所以应该是在隐藏后不支持通过location进行跳转 */ diff --git a/src/sandbox/with/index.ts b/src/sandbox/with/index.ts index e95c91fd0..71168d9dc 100644 --- a/src/sandbox/with/index.ts +++ b/src/sandbox/with/index.ts @@ -27,7 +27,6 @@ import { getEffectivePath, isArray, isPlainObject, - isUndefined, removeDomScope, throttleDeferForSetAppName, rawDefineProperty, @@ -113,6 +112,8 @@ export default class WithSandBox implements WithSandBoxInterface { this.adapter = new Adapter() // get scopeProperties and escapeProperties from plugins this.getSpecialProperties(appName) + // create location, history for child app + this.patchRouter(appName, url, this.microAppWindow) // patch window of child app this.windowEffect = patchWindow(appName, this.microAppWindow, this) // patch document of child app @@ -138,15 +139,6 @@ export default class WithSandBox implements WithSandBoxInterface { this.active = true /* --- memory router part --- start */ - // create location, history for child app - if (isUndefined(this.microAppWindow.location)) { - this.setMicroAppRouter( - this.microAppWindow.__MICRO_APP_NAME__, - this.microAppWindow.__MICRO_APP_URL__, - this.microAppWindow, - ) - } - // update microLocation, attach route info to browser url this.initRouteState(defaultPage) @@ -542,7 +534,7 @@ export default class WithSandBox implements WithSandBoxInterface { } // set location & history for memory router - private setMicroAppRouter (appName: string, url: string, microAppWindow: microAppWindowType): void { + private patchRouter (appName: string, url: string, microAppWindow: microAppWindowType): void { const { microLocation, microHistory } = createMicroRouter(appName, url) rawDefineProperties(microAppWindow, { location: { diff --git a/typings/global.d.ts b/typings/global.d.ts index f3292e53f..1e995cc9d 100644 --- a/typings/global.d.ts +++ b/typings/global.d.ts @@ -201,6 +201,9 @@ declare module '@micro-app/types' { // app rendering error onerror (e: Error): void + // set app state + setAppState (state: string): void + // get app state getAppState (): string