背景
微前端 qiankun 有两种运作模式:
1、使用 registerMicroApps + start,这是挂自动档,路由改变,重新load子应用。
2、使用 loadMicroApp,每次路由匹配上,手动load子应用。
以上这两点 官方文档 也有详细的 API 说明。
registerMicroApps + start
API
注册微应用的基础配置信息。当浏览器 url 发生变化时,会自动检查每一个微应用注册的 activeRule 规则,符合规则的应用将会被自动激活。
import { registerMicroApps } from 'qiankun';
const apps = [
{
// 微应用的名称,微应用之间必须确保唯一。
name: 'app1',
// 微应用的入口。
entry: '//localhost:8080',
// 微应用的容器节点的选择器或者 Element 实例。
container: '#container',
// 浏览器 url 发生变化会调用 activeRule 里的规则
activeRule: '/react',
// 主应用需要传递给微应用的数据。
props: {
name: 'kuitos',
},
},
]
registerMicroApps(apps, {
// 加载子应用前,可以用来加载进度条
beforeLoad: (app) => console.log('before load', app.name),
beforeMount: (app) => console.log('before load', app.name),
// 加载子应用后,可以用来关闭进度条
afterMount: (app) => console.log('after load', app.name),
beforeUnmount: [(app) => console.log('before mount', app.name)],
afterUnmount: [(app) => console.log('before mount', app.name)],
},
);
import { start } from 'qiankun';
const options = {
// 是否预加载
prefetch: true,
// 是否开启沙箱样式隔离
sandbox: true,
// 是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 true。
singular: true
}
start(options)
singular: false 的使用场景:
例如:有一个父页面,左边是菜单列表,右边上半部分是子项目A的展示区域,右边下半部分是子项目B的展示区域。而且A、B是同时呈现在父页面中的。
办法1:registerMicroApps 注册时两个子应用使用相同的activeRule,但使用不同的 container,并且设置 singular 值为 false
办法2:直接使用两次 loadMicroApp ,使用不同的 container 分别加载两个子应用
注意事项
自动档下,是这样运作的:
1、首次load应用,创建子应用实例,渲染。
2、当切到其他子应用后切回,会重新创建新的子应用实例并渲染。之前的子应用实例 qiankun 直接不要了,即使你没有手动销毁实例。
所以说,采用这种模式的话一定要在子应用暴露的unmount钩子里手动销毁实例,不然就内存泄漏了。
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
router = null;
}
所以在 qiankun 中,如果子应用使用 keep-alive 来保存状态,那么从子应用1切到子应用2,再切回子应用1,是不会保存状态的,因为整个子应用实例都被弃之不用了造成了重新加载。
loadMicroApp
通常这种场景下微应用是一个不带路由的可独立运行的业务组件,所以可以看到下面的 api 中没有路由相关的。
API
手动加载一个微应用。
如果需要能支持主应用手动 update 微应用,需要微应用 entry 再多导出一个 update 钩子:
微应用:
export async function mount(props) {
renderApp(props);
}
// 微应用中增加 update 钩子以便主应用手动更新微应用
export async function update(props) {
renderPatch(props);
}
主应用:
import { loadMicroApp } from 'qiankun';
import React from 'react';
const app = {
// 微应用的名称,微应用之间必须确保唯一。
name: 'app1',
// 微应用的入口。
entry: '//localhost:8080',
// 微应用的容器节点的选择器或者 Element 实例。
container: '#container',
// 主应用需要传递给微应用的数据。
props: {
name: 'kuitos',
},
}
class App extends React.Component {
containerRef = React.createRef();
microApp = null;
componentDidMount() {
// 返回值为微应用的示例,有对应的一些方法可以调用
// mount、unmount、update、getStatus、loadPromise、bootstrapPromise、mountPromise、unmountPromise
this.microApp = loadMicroApp(app, {
// 是否开启沙箱样式隔离
sandbox: true,
// 是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 false。
singular: false
});
}
componentWillUnmount() {
this.microApp.unmount();
}
componentDidUpdate() {
this.microApp.update({ name: 'kuitos' });
}
render() {
return <div ref={this.containerRef}></div>;
}
}
注意事项
对于自动挡模式下会丢失应用的问题,loadMicroApp 模式下不会有,loadMicroApp 的策略是每个子应用都有一个唯一的实例ID,reload时会复用之前的实例。剔除我们不关心的细节,抽象出来的代码是这样的:
https://codesandbox.io/s/great-leakey-rv03i?file=/src/index.js
当然这个例子中没有路由切换,所以可以没有keep-alive也成功存住状态了。
看这段代码要关注的是ReactDOM.render的行为,现在是往wrapper0里render了两次App0,这种case下App0的状态是保留了首次的。
但是如果往wrapper里render null或者其他App之后再次render App0,那么App0第一次渲染时的状态是不会被保留的。
而我阅读了qiankun的源码,得出的结论是qiankun中的行为就是我例子中这样的,所以App0的状态可以被保留。
那么有人会问,子应用很可能是Vue的啊,那么情况是什么样的呢?如果有这个疑问,说明可能这对你而言是一个很好的动手机会,真实的用qiankun作为基座,切换子应用时keep-alive的代码已经在这个仓库了,你可以自行增加一个Vue子应用
下面总结下最简实现子应用keep-alive的操作,其实只需两步:
1、基座中监听路由变化,变化后通过 loadMicroApp 加载对应子应用。
2、子应用中 keep-alive。
React中可以这样来keep-alive:
import CacheRoute, {CacheSwitch} from 'react-router-cache-route';
// 在路由配置处使用CacheRoute缓存希望keep-alive的组件,注意when要配置为always
<CacheSwitch>
<CacheRoute when="always" exact path="/list" component={List} />
<CacheRoute when="always" exact path="/item" component={Item} />
</CacheSwitch>
更进一步:
我们的基座中展示子应用和浏览器展示标签页是的表现是一样的,但是最简方案下,关闭标签页也没有卸载子应用实例,仍然占据着内存,并且下次重新打开也不是全新的子应用,这理论上是不符合预期的行为。
所以我们的解决方案是:
1、在子应用暴露的 unmount 钩子中写好卸载子应用的逻辑,如调用ReactDOM.unmountComponentAtNode
2、在基座中,关闭标签页时,手动调用app的unmount钩子
总结
一般情况下使用自动挡就可以了,如果微应用是一个不带路由的可独立运行的业务组件,可以用手动挡来加载这个子应用。