Fix qiankun 内存泄漏问题 (#126)

* fix: 修复乾坤插件内存问题

* fix: 多页签时关闭页面需要清除缓存

* feat: 乾坤增加entry配置,用于区别同一name不同位置用法

* docs: 去掉文档

* fix: preset-build-in的route api 兼容

* fix: 修复问题

* docs: 更新w文档

* fix: 变更api
This commit is contained in:
harrywan 2022-06-07 17:29:24 +08:00 committed by GitHub
parent d37d6b480b
commit c09dfeadde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 91 additions and 38 deletions

View File

@ -74,7 +74,6 @@ Fes.js 路由基于 [Vue Router 4.0](https://next.router.vuejs.org/introduction.
返回当前 `router` 实例。 返回当前 `router` 实例。
```js ```js
import { getRouter } from "@fesjs/fes"; import { getRouter } from "@fesjs/fes";
const router = getRouter(); const router = getRouter();
router.push(); router.push();
``` ```

View File

@ -301,7 +301,7 @@ import { access, useAccess } from '@fesjs/fes';
```js ```js
api.addCoreExports(() => [ api.addCoreExports(() => [
{ {
specifiers: ['getRoutes', 'getRouter', 'getHistory', 'destroyRouter'], specifiers: ['getRoutes'],
source: absCoreFilePath source: absCoreFilePath
} }
]); ]);

View File

@ -269,3 +269,24 @@ export default {
- 主应用使用 props 的模式传递数据(参考主应用装载子应用配置一节) - 主应用使用 props 的模式传递数据(参考主应用装载子应用配置一节)
- 子应用在生命周期钩子中获取 props 消费数据(参考子应用运行时配置一节) - 子应用在生命周期钩子中获取 props 消费数据(参考子应用运行时配置一节)
### MicroApp
| 属性 | 说明 | 类型 | 默认值 |
| ---- | ----------- | ------------- | ---------- |
| name | 子应用名称,传入`qiankun.main.apps`配置中的`name` | String | - |
| settings | 子应用配置信息 | Object | {} |
| props | 传入子应用的参数 | Object | {} |
| lifeCycles | 子应用生命周期钩子 | Object | {} |
| cacheName | 子应用缓存名称,配置后根据`name`+`cacheName`缓存子应用实例 | Object | - |
### MicroAppWithMemoHistory
| 属性 | 说明 | 类型 | 默认值 |
| ---- | ----------- | ------------- | ---------- |
| name | 子应用名称,传入`qiankun.main.apps`配置中的`name` | String | - |
| settings | 子应用配置信息 | Object | {} |
| props | 传入子应用的参数 | Object | {} |
| lifeCycles | 子应用生命周期钩子 | Object | {} |
| cacheName | 子应用缓存名称,配置后根据`name`+`cacheName`缓存子应用实例 | Object | - |
| url | 子应用的路由地址 | String | - |

View File

@ -31,8 +31,11 @@
</template> </template>
</FTabs> </FTabs>
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<keep-alive> <keep-alive :include="keepAlivePages">
<component :is="Component" :key="getPageKey(route)" /> <component
:is="getComponent(Component, route, true)"
:key="getPageKey(route)"
/>
</keep-alive> </keep-alive>
</router-view> </router-view>
</template> </template>
@ -68,11 +71,12 @@ export default {
return { return {
path: _route.path, path: _route.path,
route: _route, route: _route,
name: _route.meta.name, name: _route.meta.name ?? _route.name,
title: computed(() => transTitle(title)), title: computed(() => transTitle(title)),
key: getKey() key: getKey()
}; };
}; };
const keepAlivePages = ref([]);
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -122,6 +126,12 @@ export default {
} }
list.splice(index, 1); list.splice(index, 1);
pageList.value = list; pageList.value = list;
const _keepAlivePages = [...keepAlivePages.value];
const keepIndex = _keepAlivePages.indexOf(selectedPage.name);
if (keepIndex !== -1) {
_keepAlivePages.splice(keepIndex, 1);
}
keepAlivePages.value = _keepAlivePages;
}; };
const reloadPage = (path) => { const reloadPage = (path) => {
const selectedPage = findPage(path || unref(route.path)); const selectedPage = findPage(path || unref(route.path));
@ -132,6 +142,7 @@ export default {
const closeOtherPage = (path) => { const closeOtherPage = (path) => {
const selectedPage = findPage(path || unref(route.path)); const selectedPage = findPage(path || unref(route.path));
pageList.value = [selectedPage]; pageList.value = [selectedPage];
keepAlivePages.value = [selectedPage.name];
}; };
const getPageKey = (_route) => { const getPageKey = (_route) => {
const selectedPage = findPage(_route.path); const selectedPage = findPage(_route.path);
@ -151,10 +162,10 @@ export default {
default: default:
} }
}; };
const keepAlivePages = ref([]);
const getComponent = (Component, _route) => { const getComponent = (Component, _route, isKeep = false) => {
if (_route.meta['keep-alive']) { if (isKeep || _route.meta['keep-alive']) {
const name = _route.meta?.name || _route.name; const name = _route.meta?.name ?? _route.name;
if (name) { if (name) {
// name // name
Component.type.name = name; Component.type.name = name;

View File

@ -1,6 +1,7 @@
<template> <template>
<div class="haizekuo"> <div class="haizekuo">
app1 - index app1 - index
<input />
</div> </div>
</template> </template>
<config> <config>

View File

@ -1,6 +1,7 @@
<template> <template>
<div class="haizekuo"> <div class="haizekuo">
app1 - test app1 - test
<input />
</div> </div>
</template> </template>
<config> <config>

View File

@ -1,9 +1,10 @@
<template> <template>
<div> <div>
main main
<input />
<FTabs v-model="activeKey"> <FTabs v-model="activeKey">
<FTabPane name="Tab 1" value="1"> <FTabPane name="Tab 1" value="1">
<MicroAppWithMemoHistory key="1" name="app1" url="/app1" a="1" /> <MicroAppWithMemoHistory key="1" style="background: red" name="app1" url="/app1" a="1" />
</FTabPane> </FTabPane>
<FTabPane name="Tab 2" value="2"> <FTabPane name="Tab 2" value="2">
<MicroAppWithMemoHistory key="2" name="app1" url="/app1/test" /> <MicroAppWithMemoHistory key="2" name="app1" url="/app1/test" />

View File

@ -2,11 +2,11 @@ import { defaultHistoryType } from '../constants';
function getMicroApp(options) { function getMicroApp(options) {
const { const {
key, microAppName, masterHistoryType, base, namespace, ...normalizedRouteProps key, microAppName, cacheName, masterHistoryType, base, namespace, ...normalizedRouteProps
} = options; } = options;
return `(() => { return `(() => {
const { getMicroAppRouteComponent } = require('@@/${namespace}/getMicroAppRouteComponent'); const { getMicroAppRouteComponent } = require('@@/${namespace}/getMicroAppRouteComponent');
return getMicroAppRouteComponent({key: '${key}', appName: '${microAppName}', base: '${base}', masterHistoryType: '${masterHistoryType}', routeProps: ${JSON.stringify(normalizedRouteProps)} }) return getMicroAppRouteComponent({key: '${key}', appName: '${microAppName}', cacheName: '${cacheName}', base: '${base}', masterHistoryType: '${masterHistoryType}', routeProps: ${JSON.stringify(normalizedRouteProps)} })
})()`; })()`;
} }
@ -19,6 +19,7 @@ function modifyRoutesWithAttachMode({
if (route.meta && route.meta.microApp) { if (route.meta && route.meta.microApp) {
route.component = getMicroApp({ route.component = getMicroApp({
key: route.path, key: route.path,
cacheName: route.meta.cacheName ?? route.path,
microAppName: route.meta.microApp, microAppName: route.meta.microApp,
masterHistoryType, masterHistoryType,
base, base,

View File

@ -12,13 +12,13 @@ import {mergeWith} from "{{{LODASH_ES}}}";
// eslint-disable-next-line import/extensions // eslint-disable-next-line import/extensions
import { getMasterOptions } from "./masterOptions"; import { getMasterOptions } from "./masterOptions";
function unmountMicroApp(microApp) { async function unmountMicroApp(microApp) {
if (!microApp) { if (!microApp) {
return; return;
} }
const status = microApp.getStatus(); const status = microApp.getStatus();
if (status === 'MOUNTED') { if (status === 'MOUNTED') {
microApp.unmount(); await microApp.unmount();
} }
} }
@ -28,6 +28,7 @@ export const MicroApp = defineComponent({
type: String, type: String,
required: true required: true
}, },
cacheName: String,
settings: Object, settings: Object,
props: Object, props: Object,
lifeCycles: Object lifeCycles: Object
@ -64,26 +65,26 @@ export const MicroApp = defineComponent({
return {}; return {};
}); });
const propsConfigRef = computed(() => { const propsConfigRef = computed(() => {
return { return {
...propsFromConfigRef.value, ...propsFromConfigRef.value,
...props.props, ...props.props,
...attrs
}; };
}); });
// 只有当name变化时才重新加载新的子应用 // 只有当name变化时才重新加载新的子应用
const loadApp = () => { const loadApp = () => {
const appConfig = appConfigRef.value; const appConfig = appConfigRef.value;
const { name, entry } = appConfig; const { name, cacheName } = appConfig;
// 加载新的 // 加载新的
microAppRef.value = loadMicroApp( const app = loadMicroApp(
{ {
// 保证唯一 // 保证唯一
name: `${name}_${Date.now()}`, name: `${name}_${props.cacheName || ''}`,
entry: entry, entry: entry,
container: containerRef.value, container: containerRef.value,
props: {...propsConfigRef.value} props: {...propsConfigRef.value, ...attrs}
}, },
{ {
...globalSettings, ...globalSettings,
@ -96,6 +97,12 @@ export const MicroApp = defineComponent({
(v1, v2) => concat(v1 ?? [], v2 ?? []) (v1, v2) => concat(v1 ?? [], v2 ?? [])
) )
); );
['loadPromise', 'bootstrapPromise', 'mountPromise', 'unmountPromise'].forEach((key)=>{
app[key].catch((e)=>{
console.warn("[@fesjs/plugin-qiankun]", e)
})
})
microAppRef.value = app;
}; };
// 当参数变化时update子应用 // 当参数变化时update子应用
@ -131,7 +138,7 @@ export const MicroApp = defineComponent({
} }
// 返回 microApp.update 形成链式调用 // 返回 microApp.update 形成链式调用
return microApp.update({...propsConfigRef.value}); return microApp.update({...propsConfigRef.value, ...attrs});
} }
} }
); );

View File

@ -14,6 +14,7 @@ export const MicroAppWithMemoHistory = defineComponent({
type: String, type: String,
required: true required: true
}, },
cacheName: String,
settings: Object, settings: Object,
props: Object, props: Object,
lifeCycles: Object, lifeCycles: Object,

View File

@ -3,10 +3,11 @@ import { MicroApp } from './MicroApp';
export function getMicroAppRouteComponent({ export function getMicroAppRouteComponent({
key, key,
appName, appName,
cacheName,
base, base,
masterHistoryType, masterHistoryType,
routeProps routeProps
}) { }) {
return <MicroApp key={key} base={base} masterHistoryType={masterHistoryType} name={appName} {...routeProps} />; return <MicroApp key={key} base={base} masterHistoryType={masterHistoryType} name={appName} cacheName={cacheName} {...routeProps} />;
} }

View File

@ -1,4 +1,4 @@
import { plugin, ApplyPluginsType, getRouter, getHistory, destroyRouter } from '@@/core/coreExports'; import { plugin, ApplyPluginsType } from '@@/core/coreExports';
{{#HAS_PLUGIN_MODEL}} {{#HAS_PLUGIN_MODEL}}
import { setModelState } from './qiankunModel'; import { setModelState } from './qiankunModel';
{{/HAS_PLUGIN_MODEL}} {{/HAS_PLUGIN_MODEL}}
@ -18,6 +18,7 @@ function isPromise(obj) {
let render = () => {}; let render = () => {};
let cacheAppPromise = null; let cacheAppPromise = null;
let hasMountedAtLeastOnce = false; let hasMountedAtLeastOnce = false;
const cache = {};
export default () => defer.promise; export default () => defer.promise;
@ -91,6 +92,11 @@ export function genMount(mountElementId) {
defer.resolve(); defer.resolve();
} }
hasMountedAtLeastOnce = true; hasMountedAtLeastOnce = true;
cacheAppPromise.then((app)=>{
if(!cache[props.name]) {
cache[props.name] = app;
}
})
}; };
} }
@ -109,9 +115,15 @@ export function genUpdate() {
// 子应用生命周期钩子Unmount // 子应用生命周期钩子Unmount
export function genUnmount() { export function genUnmount() {
return async (props) => { return async (props) => {
const history = getHistory(); if (cache[props.name]) {
history.destroy(); // 会触发app.unmount setTimeout(() => {
destroyRouter(); const app = cache[props.name];
app.unmount();
app._container.innerHTML = '';
delete cache[props.name];
cacheAppPromise = null;
}, 0);
}
const slaveRuntime = getSlaveRuntime(); const slaveRuntime = getSlaveRuntime();
if (slaveRuntime.unmount) { if (slaveRuntime.unmount) {
await slaveRuntime.unmount(props); await slaveRuntime.unmount(props);

View File

@ -1,5 +1,5 @@
import { createMemoryHistory, getHistory } from '@@/core/coreExports'; import { createMemoryHistory } from '@@/core/coreExports';
import qiankunRender, { clientRenderOptsStack, history } from './lifecycles'; import qiankunRender, { clientRenderOptsStack, history as historyConfig } from './lifecycles';
export const render = oldRender => qiankunRender().then(oldRender); export const render = oldRender => qiankunRender().then(oldRender);
@ -15,19 +15,18 @@ export function modifyClientRenderOpts(memo) {
} }
export function modifyCreateHistory(memo) { export function modifyCreateHistory(memo) {
if (history.url) { if (historyConfig.url) {
return createMemoryHistory return createMemoryHistory
} }
return memo; return memo;
} }
export function onRouterCreated({ router }) { export function onRouterCreated({ router, history }) {
if(history.url) { if(historyConfig.url) {
const memoryHistory = getHistory(); history.push(historyConfig.url)
memoryHistory.push(history.url)
} }
if(history.onRouterInit){ if(historyConfig.onRouterInit){
history.onRouterInit(router) historyConfig.onRouterInit(router)
} }
} }

View File

@ -61,6 +61,7 @@ const beforeRender = async ({rootElement}) => {
console.error(e); console.error(e);
} }
app.unmount(); app.unmount();
app._container.innerHTML = '';
} }
return initialState; return initialState;
}; };

View File

@ -10,9 +10,6 @@ const ROUTER_BASE = '{{{ routerBase }}}';
let router = null; let router = null;
let history = null; let history = null;
export const createRouter = (routes) => { export const createRouter = (routes) => {
if (router) {
return router;
}
const createHistory = plugin.applyPlugins({ const createHistory = plugin.applyPlugins({
key: 'modifyCreateHistory', key: 'modifyCreateHistory',
type: ApplyPluginsType.modify, type: ApplyPluginsType.modify,
@ -36,7 +33,7 @@ export const createRouter = (routes) => {
plugin.applyPlugins({ plugin.applyPlugins({
key: 'onRouterCreated', key: 'onRouterCreated',
type: ApplyPluginsType.event, type: ApplyPluginsType.event,
args: { router }, args: { router, history },
}); });
return router; return router;