Author: vivo Internet front-end team - Tang Xiao
This article sorts out the solutions for implementing multi-tab and sub-application caching based on Alibaba's open-source micro-frontend framework qiankun, and also compares the differences, advantages and disadvantages between multiple different solutions. For students who use micro-frontends for multi-tab development, Provide some references.
1. What is multi-tab?
Our common browser multi-tab, editor multi-tab, from the product point of view, is to enable users to access records, quickly locate the workspace, etc.; for single-page applications, you can achieve multi-tab, Cache the user's access records to provide a better user experience.
The front-end can implement multiple tabs in a variety of ways. There are two common solutions:
- Use the CSS style display:none to control the display of the page and the content of the hidden module;
- Serialize and cache modules and render them through cached content (similar to vue's keep-alive principle, which is widely used in single-page applications).
Compared with the first method, the second method stores the DOM format in the serialized JS object, and only renders the DOM elements that need to be displayed, which reduces the number of DOM nodes and improves the rendering performance. It is the current mainstream implementation of multiple pages. way of signing.
So compared with the traditional single-page application, what is the difference between the front-end application after the transformation through the micro-frontend qiankun and the implementation of multi-page labels?
1.1 Single-page application realizes multiple tabs
The single-page application technology stack before the transformation is the Vue family bucket (vue2.6.10 + element2.15.1 + webpack4.0.0+vue-cli4.2.0).
The vue framework provides keep-alive to support cache-related requirements. Use keep-alive to implement the basic functions of multiple tabs. However, in order to support more functions, we repackage the vue-keep-alive component based on it. .
Compared with keep-alive, which controls the cache through include and exclude, vue-keep-alive uses a more native publish-subscribe method to delete the cache, which can achieve a more complete multi-tab function. For example, the same route can be based on different parameters. Derive multiple routing instances (such as opening multiple detail page tabs) and dynamically delete cache instances.
The following is an extended implementation of vue-keep-alive customization:
created() {
// 动态删除缓存实例监听
this.cache = Object.create(null);
breadCompBus.$on('removeTabByKey', this.removeCacheByKey);
breadCompBus.$on('removeTabByKeys', (data) => {
data.forEach((item) => {
this.removeCacheByKey(item);
});
});
}
The vue-keep-alive component can pass in a custom method for custom vnode.key, which supports multiple instances derived from the same matching route.
// 传入`vue-keep-alive`的自定义方法
function updateComponentsKey(key, name, vnode) {
const match = this.$route.matched[1];
if (match && match.meta.multiNodeKey) {
vnode.key = match.meta.multiNodeKey(key, this.$route);
return vnode.key;
}
return key;
}
1.2 After using qiankun for micro-frontend transformation, what is the difference between multi-tab cache
qiankun is a front-end microservice framework based on Single-Spa implementation launched by Ant Financial. It is essentially a routing-distributed service framework. Unlike the original Single-Spa solution that uses JS Entry, qiankun uses HTML Entry instead. optimization.
After using qiankun for micro-frontend transformation, the page is split into a base application and multiple sub-applications, each of which runs in an independent sandbox environment.
Compared with the way of controlling component instances through keep-alive in a single-page application, the keep-alive of each sub-application after splitting cannot control the instances of other sub-applications. We need the cache to take effect for all applications, then only the The cache is placed in the dock application.
There are several problems with this:
- Loading: When and how does the main application need to load sub-application instances?
- Rendering: When rendering sub-applications through cached instances, are sub-applications rendered through DOM explicit and hidden or are there other ways?
- Communication: When closing the tab, how to judge whether to completely uninstall the sub-app, and what communication method should the main application use to tell the sub-app?
2. Program selection
After searching and comparing a series of data on Github issues and Nuggets and other platforms, there are two main ways to realize how to implement multiple tabs under the qiankun framework without modifying the qiankun source code.
2.1 Option 1: Multiple sub-applications exist at the same time
Implementation ideas:
On the dom, v-show controls which sub-application is displayed, and display:none; controls the display and hiding of the dom of different sub-applications.
When the url changes, manually control which sub-application is loaded through loadMicroApp, and when the tab is closed, manually call the unmount method to unload the sub-application.
Example:
<template>
<div id="app">
<header>
<router-link to="/app-vue-hash/">app-vue-hash</router-link>
<router-link to="/app-vue-history/">app-vue-history</router-link>
<router-link to="/about">about</router-link>
</header>
<div id="appContainer1" v-show="$route.path.startsWith('/app-vue-hash/')"></div>
<div id="appContainer2" v-show="$route.path.startsWith('/app-vue-history/')"></div>
<router-view></router-view>
</div>
</template>
<script>
import { loadMicroApp } from 'qiankun';
const apps = [
{
name: 'app-vue-hash',
entry: 'http://localhost:1111',
container: '#appContainer1',
props: { data : { store, router } }
},
{
name: 'app-vue-history',
entry: 'http://localhost:2222',
container: '#appContainer2',
props: { data : store }
}
]
export default {
mounted() {
// 优先加载当前的子项目
const path = this.$route.path;
const currentAppIndex = apps.findIndex(item => path.includes(item.name));
if(currentAppIndex !== -1){
const currApp = apps.splice(currentAppIndex, 1)[0];
apps.unshift(currApp);
}
// loadMicroApp 返回值是 app 的生命周期函数数组
const loadApps = apps.map(item => loadMicroApp(item))
// 当 tab 页关闭时,调用 loadApps 中 app 的 unmount 函数即可
},
}
</script>
Specific DOM display (through display:none; to control the display and hide of the DOM of different sub-applications):
Program advantages:
- loadMicroApp is an API provided by qiankun, which can be easily and quickly accessed;
- This method does not uninstall sub-applications, and the tab switching speed is faster.
Insufficient solution:
- If the DOM is not destroyed when the sub-application is switched, it will cause too many DOM nodes and events to be monitored, and in severe cases, the page will be stuck;
- The sub-application is not uninstalled when it is switched, and the routing event monitoring is also not uninstalled. Special processing is required for the monitoring of routing changes.
2.2 Option 2: Load only one sub-application at the same time, and save the state of other applications at the same time
Implementation ideas:
- Register the sub-application through registerMicroApps, and qiankun will automatically load the matching sub-application;
- Referring to the implementation of keep-alive, each sub-application caches the vnode of its own instance, and the cached vnode can be directly rendered into the real DOM when entering the sub-application next time.
Program advantages:
- At the same time, only the active page of a sub-application is displayed, which can reduce the number of DOM nodes;
- When the non-active sub-application is uninstalled, the DOM and unnecessary event monitoring will be uninstalled at the same time, which can release a certain amount of memory.
Insufficient solution:
- There is no existing API that can be implemented quickly, and it is necessary to manage the sub-application cache by itself, which is more complicated to implement;
- DOM rendering adds a process of converting from virtual DOM to real DOM, and the rendering time will be slightly longer than the first solution.
Introduction to the instantiation process of vue components
Here is a brief review of several key rendering nodes of Vue:
vue key rendering node (source: Nuggets community)
compile: Compile the template and convert the AST to generate a render function;
render: Generate VNODE virtual DOM;
patch : convert virtual DOM to real DOM;
Therefore, the second scheme is more than the last patch process compared to the scheme one.
2.3 Final selection
According to the evaluation of the advantages and disadvantages of the two schemes, and according to the specific situation of our project, the second scheme was finally selected for implementation. The specific reasons are as follows:
- Excessive DOM and event monitoring will cause unnecessary memory waste. At the same time, our project mainly focuses on editor display and data display. There is a lot of content in a single tab, so we tend to pay more attention to memory usage;
- Scheme 2 adds a patch process to the secondary rendering of the sub-application, and the rendering speed will not be much slower, which is within an acceptable range.
Third, the specific implementation
In the above part, we briefly described an implementation idea of the second solution. The core idea is to cache the vnode of the sub-application instance. In this part, let's take a look at a specific implementation process.
3.1 From component-level caching to application-level caching
In vue, the keep-alive component implements component-level caching by caching vnodes. For sub-applications implemented through the vue framework, it is actually a vue instance, so we can also do the same by caching vnodes. way to implement application-level caching.
By analyzing the keep-alive source code, we learned that keep-alive returns the vnode of the corresponding component by performing a cache hit in the render, and adds the cache of the sub-component vnode to the mounted and updated life cycle hooks.
// keep-alive核心代码
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// 更多代码...
// 缓存命中
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
// delay setting the cache until update
this.vnodeToCache = vnode
this.keyToCache = key
}
// 设置keep-alive,防止再次触发created等生命周期
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
// mounted和updated时缓存当前组件的vnode
mounted() {
this.cacheVNode()
}
updated() {
this.cacheVNode()
}
Compared with keep-alive, which needs to update the vnode cache in the two life cycles of mounted and updated, in the application-level cache, we only need to actively cache the vnode of the entire instance when the sub-application is uninstalled.
// 父应用提供unmountCache方法
function unmountCache() {
// 此处永远只会保存首次加载生成的实例
const needCached = this.instance?.cachedInstance || this.instance;
const cachedInstance = {};
cachedInstance._vnode = needCached._vnode;
// keepalive设置为必须 防止进入时再次created,同keep-alive实现
if (!cachedInstance._vnode.data.keepAlive) cachedInstance._vnode.data.keepAlive = true;
// 省略其他代码...
// loadedApplicationMap用于是key-value形式,用于保存当前应用的实例
loadedApplicationMap[this.cacheKey] = cachedInstance;
// 省略其他代码...
// 卸载实例
this.instance.$destroy();
// 设置为null后可进行垃圾回收
this.instance = null;
}
// 子应用在qiankun框架提供的卸载方法中,调用unmountCache
export async function unmount() {
console.log('[vue] system app unmount');
mainService.unmountCache();
}
3.2 Relocation - remounting the vnode to a new instance
After the vnode is cached in memory, the original instance is unloaded, and when the sub-application is re-entered, the cached vnode can be used for rendering.
// 创建子应用实例,有缓存的vnode则使用缓存的vnode
function newVueInstance(cachedNode) {
const config = {
router: this.router,
store: this.store,
render: cachedNode ? () => cachedNode : instance.render, // 优先使用缓存vnode
});
return new Vue(config);
}
// 实例化子应用实例,根据是否有缓存vnode确定是否传入cachedNode
this.instance = newVueInstance(cachedNode);
this.instance.$mount('#app');
So, here are some questions:
- If we re-create an instance every time we enter the sub-application, then why uninstall it, can it be just not uninstalled?
- Wouldn't it be a problem to use the cached vnode on a new instance?
First, let's answer the first question. Why should the original sub-application instance be uninstalled when switching sub-applications? There are two considerations:
- The first is the consideration of memory. What we need is only the vnode, not the entire instance. Cache the entire instance is the implementation of solution 1, so we only need to cache the objects we need;
- Second, uninstalling the sub-application instance can remove unnecessary event monitoring. For example, vue-router monitors the popstate event. When we operate other sub-applications, we do not want the original sub-application to respond to these events. Then when the sub-application is uninstalled, these listeners can be removed.
For the second question, the situation will be a little more complicated. In the next part, we will mainly look at the main problems encountered and how to solve them.
3.3 Solve the problem of application-level caching scheme
3.3.1 vue-router related issues
- After the instance is uninstalled, the monitoring of route changes is invalid;
- The new vue-router fails to record parameters such as the original router params.
First, we need to clarify the reasons for these two problems:
- The first is because the listener for the popstate event is removed when the sub-application is uninstalled, then all we need to do is to re-register the listener for the popstate event, which can be solved by re-instantiating a vue-router;
- The second problem is that after solving the first problem by re-instantiating vue-router, it is actually a new vue-router. What we need to do is to cache not only vnode, but also router-related information.
The general solution is as follows:
// 实例化子应用vue-router
function initRouter() {
const { router: originRouter } = this.baseConfig;
const config = Object.assign(originRouter, {
base: `app-kafka/`,
});
Vue.use(VueRouter);
this.router = new VueRouter(config);
}
// 创建子应用实例,有缓存的vnode则使用缓存的vnode
function newVueInstance(cachedNode) {
const config = {
router: this.router, // 在vue init过程中,会重新调用vue-router的init方法,重新启动对popstate事件监听
store: this.store,
render: cachedNode ? () => cachedNode : instance.render, // 优先使用缓存vnode
});
return new Vue(config);
}
function render() {
if(isCache) {
// 场景一、重新进入应用(有缓存)
const cachedInstance = loadedApplicationMap[this.cacheKey];
// router使用缓存命中
this.router = cachedInstance.$router;
// 让当前路由在最初的Vue实例上可用
this.router.apps = cachedInstance.catchRoute.apps;
// 使用缓存vnode重新实例化子应用
const cachedNode = cachedInstance._vnode;
this.instance = this.newVueInstance(cachedNode);
} else {
// 场景二、首次加载子应用/重新进入应用(无缓存)
this.initRouter();
// 正常实例化
this.instance = this.newVueInstance();
}
}
function unmountCache() {
// 省略其他代码...
cachedInstance.$router = this.instance.$router;
cachedInstance.$router.app = null;
// 省略其他代码...
}
3.3.2 Parent-child component communication
The multi-tab method increases the frequency of communication between parent and child components. qiankun provides the setGlobalState communication method, but in single application mode, only one child application is supported at the same time. Application communication, therefore, for different scenarios, we need more flexible communication methods.
Child application - parent application: use qiankun's own communication method;
The communication scenario from the child to the parent is relatively simple. Generally, it is reported only when the route changes, and only the child application in the active state will report. You can directly use the communication method of qiankun;
Parent application - child application: use custom event communication;
From the parent application to the child application, it not only needs to communicate with the child application in the active state, but also needs to communicate with the child application currently in the cache;
Therefore, from the parent application to the child application, the communication between the parent application and multiple child applications can be realized by means of custom events.
// 自定义事件发布
const evt = new CustomEvent('microServiceEvent', {
detail: {
action: { name: action, data },
basePath, // 用于子应用唯一标识
},
});
document.dispatchEvent(evt);
// 自定义事件监听
document.addEventListener('microServiceEvent', this.listener);
3.3.3 Cache management to prevent memory leaks
The most important thing in using cache is to manage the cache and clean it up in time when it is not needed. This is a very important but easily overlooked item in JS.
application level cache
Attributes such as sub-application vnode, router, etc., are cached when the sub-application is switched;
page level cache
- The vnode of the component is cached through vue-keep-alive;
- When deleting a tab, listen to the remove event and delete the vnode corresponding to the page;
- When all caches in the vue-keep-alive component are deleted, notify to delete the entire sub-application cache;
3.4 Overall Framework
Finally, let's take a look at the implementation of the multi-tab cache from an overall perspective.
Because it is not only necessary to manage the cache of the sub-application, but also to register the vue-keep-alive component to each sub-application, etc., we will manage these services in the mainService of the main application, and register the sub-application through registerMicroApps. props are passed into sub-applications, so that the same set of code can be implemented and reused in multiple places.
// 子应用main.js
let mainService = null;
export async function mount(props) {
mainService = null;
const { MainService } = props;
// 注册主应用服务
mainService = new MainService({
// 传入对应参数
});
// 实例化vue并渲染
mainService.render(props);
}
export async function unmount() {
mainService.unmountCache();
}
Finally, the key processes are sorted out:
4. Existing problems
4.1 Temporarily only supports the instance cache of the vue framework
This solution is also supported by the existing features of vue. There is no unified implementation solution for multi-tab implementation in the react community, and the author has not explored too much. Considering that the existing project is mainly based on the vue technology stack, the later stage The upgrade will only be upgraded to vue3.0, which can be fully supported for a period of time.
V. Summary
Compared with most implementations in the community through scheme 1, this article provides another way of implementing multi-tab caching, mainly because the sub-application cache processing is slightly different. The general ideas and communication methods are the same. are interoperable.
In addition, this article does not make too many divergent summaries on the use of the qiankun framework. There are already many summaries of related issues and experience of stepping on pits for reference on the official website and Github.
Finally, if there is any problem or error in the article, please point it out, thank you.
reference reading
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。