Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(vue): add unmount hook to unmount application #2849

Merged
merged 4 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rich-parents-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@qiankunjs/vue": patch
---

fix(vue): add unmount hook to unmount application
37 changes: 31 additions & 6 deletions examples/main/render/Vue3Render.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,48 @@
import { createApp, h } from 'vue';
import { createApp, h, onBeforeMount, onBeforeUnmount, ref } from 'vue';
import { MicroApp } from '../../../packages/ui-bindings/vue/dist/esm';

const sidemenu = document.querySelector('.mainapp-sidemenu');

const microApps = [
{ name: 'react15', entry: '//localhost:7102' },
{ name: 'react16', entry: '//localhost:7100' },
];

function vueRender() {
const application = createApp({
components: {
},
render() {
return h('div', [
this.message,
h(MicroApp, { name: 'react15', entry: '//localhost:7102' }),
this.message,
h(MicroApp, { name: this.name, entry: this.entry, autoCaptureError: true }),
]);
},
setup() {
const message = 'abc';
const name = ref('');
const entry = ref('');

const handleMenuClick = (e) => {
const app = microApps.find((app) => app.name === e.target.dataset.value);
if (app && app.name !== name.value) {
name.value = app.name;
entry.value = app.entry;
} else {
console.log('not found any app');
}
}

onBeforeMount(() => {
sidemenu.addEventListener('click', handleMenuClick);
});


onBeforeUnmount(() => {
sidemenu.removeEventListener('click', handleMenuClick);
});

return {
message,
name,
entry,
};
// return () => h('div', [
// message.value,
Expand Down
1 change: 1 addition & 0 deletions packages/ui-bindings/vue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import { MicroApp } from '@qiankunjs/vue';
| `autoCaptureError` | No | Automatically set error capturing for the micro-application | `boolean` | `false` |
| `className` | No | The style class for the micro-application | `string` | `undefined` |
| `wrapperClassName` | No | The style class wrapping the micro-application's loading and error components | `string` | `undefined` |
| `appProps` | No | Properties passed to the sub-application | `Record<string, any>` | `undefined` |

### Component Slots

Expand Down
1 change: 1 addition & 0 deletions packages/ui-bindings/vue/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ import { MicroApp } from '@qiankunjs/vue';
| `autoCaptureError` | 否 | 自动设置微应用的错误捕获 | `boolean` | `false` |
| `className` | 否 | 微应用的样式类 | `string` | `undefined` |
| `wrapperClassName` | 否 | 包裹微应用加载组件、错误捕获组件和微应用的样式类,仅在启用加载组件或错误捕获组件时有效 | `string` | `undefined` |
| `appProps` | 否 | 传递给子应用的属性 | `Record<string, any>` | `undefined` |

### 组件插槽

Expand Down
88 changes: 55 additions & 33 deletions packages/ui-bindings/vue/src/MicroApp.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import type { PropType } from 'vue-demi';
import { computed, defineComponent, h, onMounted, reactive, ref, toRefs, watch, isVue2 } from 'vue-demi';
import {
computed,
defineComponent,
h,
onMounted,
ref,
onBeforeUnmount,
shallowRef,
toRefs,
watch,
isVue2,
} from 'vue-demi';
import type { AppConfiguration, LifeCycles } from 'qiankun';
import type { MicroAppType } from '@qiankunjs/ui-shared';
import { mountMicroApp, omitSharedProps, unmountMicroApp, updateMicroApp } from '@qiankunjs/ui-shared';
Expand Down Expand Up @@ -43,23 +54,23 @@ export const MicroApp = defineComponent({
type: String,
default: undefined,
},
appProps: {
type: Object,
default: undefined,
},
},
setup(props, { slots }) {
const originProps = props;
const { name, wrapperClassName, className, ...propsFromParams } = toRefs(originProps);
const { name, wrapperClassName, className, appProps, autoCaptureError } = toRefs(originProps);

const loading = ref(false);
const error = ref<Error>();

const containerRef = ref(null);
const microAppRef = ref<MicroAppType>();

const reactivePropsFromParams = computed(() => {
return omitSharedProps(reactive(propsFromParams));
});
const microAppRef = shallowRef<MicroAppType>();

const isNeedShowError = computed(() => {
return slots.errorBoundary || reactivePropsFromParams.value.autoCaptureError;
return slots.errorBoundary || autoCaptureError.value;
});

// 配置了 errorBoundary 才改 error 状态,否则直接往上抛异常
Expand All @@ -77,31 +88,38 @@ export const MicroApp = defineComponent({

const rootRef = ref(null);

onMounted(() => {
console.log(rootRef.value);
const unmount = () => {
const microApp = microAppRef.value;

console.log(containerRef.value);
if (microApp) {
microApp._unmounting = true;

unmountMicroApp(microApp).catch((err: Error) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kuitos 卸载这一块要不要 await 下,不等上一个应用A卸载完就加载应用B并跳转路由。有时候会匹配到A应用的404路由

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

你是指通过同一个组件切 name 的方式切换子应用的场景的吗?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

是的, 改变name跟entry ,再push路由。

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

那需要存一下前一个的 unmountPromise

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里 await 好像没啥意义。

在你说的场景下,await 也解决不了的,切换本身也是一个异步行为。

image
目前已经通过 _unmounting 这个同步标志位来处理了。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个标志位好像作用也不大,按理来说应该是等上一个子应用完全卸载完再进行 路由跳转

setComponentError(err);
loading.value = false;
});

microAppRef.value = undefined;
}
};

onMounted(() => {
// watch name 变更切换子应用
watch(
name,
() => {
const microApp = microAppRef.value;

if (microApp) {
microApp._unmounting = true;

unmountMicroApp(microApp).catch((err: Error) => {
setComponentError(err);
loading.value = false;
});

microAppRef.value = undefined;
}
const prevApp = microAppRef.value;
// 销毁上一个子应用
unmount();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里为啥要调 unmount ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

利用 name 切换子应用时,先将前一个子应用 unmount

Copy link
Contributor

@qiYuei qiYuei Jan 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里应该可以去掉, 我看 最新的 mountMicroApp 已经有对上一个应用进行卸载的操作。 然后最好判断下 name 是否存在再调用mountMicroApp。
相关pr

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

最新的 pr 应该没有在 mountMicroApp 进行卸载,只是做了如果 上一个 应用正在卸载,使用 await 等待卸载完毕再执行 mount 操作


// 初始化下一个子应用
void mountMicroApp({
prevMicroApp: microAppRef.value,
prevMicroApp: prevApp,
container: containerRef.value!,
componentProps: originProps,
componentProps: {
...originProps,
...appProps.value,
},
setLoading: (l) => {
loading.value = l;
},
Expand All @@ -118,13 +136,17 @@ export const MicroApp = defineComponent({
);

watch(
reactivePropsFromParams,
appProps,
() => {
updateMicroApp({
microApp: microAppRef.value,
setLoading: (l) => {
loading.value = l;
},
microAppProps: {
...omitSharedProps(originProps),
...appProps.value,
},
});
},
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里不应该不用深度监听,甚至 ref 都不用, shallowRef 就好。因为可能传递各种实例,或者组件啥的下去没必要 observe 一次。

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

react 的版本现在就是 deepCompare 的

}, [useDeepCompare(omitSharedProps(componentProps))]);

vue 里有更合理的方案实现类似的效果吗?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不使用深度监听的话,以目前暴露的接口来说,是没有主动更新 vue 组件里的子应用的能力的,是否需要把子应用实例或者是更新方法暴露给使用者

此外,目前 vue 里如果使用 shallowRef 或者仅仅浅层 watch props 中的 appProps ,这种方式仅能检测到引用的变化,对于 appProps 内部的变化是无法检测到的,可能不太符合预期?

Copy link
Contributor

@qiYuei qiYuei Jan 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

一般来说 react 要改变 componentProps 里面的状态一般也是 {...componentProps} 这样吧,如果是这样 vue 这边可以用 shallowRef 然后 lodash.isEqual 比较新旧值来判断是否要更新 micro。 我是感觉 deep watch appProps 虽然简单但是有点耗性能。

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

跟react版本保持一致吧,还是深度监听吧

Expand All @@ -133,6 +155,10 @@ export const MicroApp = defineComponent({
);
});

onBeforeUnmount(() => {
unmount();
});

const microAppWrapperClassName = computed(() =>
wrapperClassName.value ? `${wrapperClassName.value} qiankun-micro-app-wrapper` : 'qiankun-micro-app-wrapper',
);
Expand All @@ -149,16 +175,12 @@ export const MicroApp = defineComponent({
microAppWrapperClassName,
microAppClassName,
rootRef,
reactivePropsFromParams,
microApp: microAppRef,
};
},

render() {
return this.reactivePropsFromParams.autoSetLoading ||
this.reactivePropsFromParams.autoCaptureError ||
this.$slots.loader ||
this.$slots.errorBoundary
return this.autoSetLoading || this.autoCaptureError || this.$slots.loader || this.$slots.errorBoundary
? h(
'div',
{
Expand All @@ -169,7 +191,7 @@ export const MicroApp = defineComponent({
? typeof this.$slots.loader === 'function'
? this.$slots.loader(this.loading)
: this.$slots.loader
: this.reactivePropsFromParams.autoSetLoading &&
: this.autoSetLoading &&
h(MicroAppLoader, {
...(isVue2
? {
Expand All @@ -186,7 +208,7 @@ export const MicroApp = defineComponent({
? typeof this.$slots.errorBoundary === 'function'
? this.$slots.errorBoundary(this.error)
: this.$slots.errorBoundary
: this.reactivePropsFromParams.autoCaptureError &&
: this.autoCaptureError &&
h(ErrorBoundary, {
...(isVue2
? {
Expand Down