前言
关于弹出框,前几篇主要讲了如何渲染弹出框标签、实现弹出框的弹出位置、触发弹出框以及弹出框组件和tabs组件的数据交互。
接下来的几篇文章,将围绕着如何实现弹出框的五个标签。本篇文章讲的是第一个标签:重新加载。
refresh
在上一篇中讲了tabs定义了onContextmenuItem方法,根据点击的标签name来实现对应的标签功能。重新加载对应的是refresh,我们看看是如何实现的。
case 'refresh':
proxy.eventBus.emit('onTabViewRefresh', menu)
break
可以看到就一行代码,这里的emit是什么呢?是上一篇讲的子组件调用父组件方法的那个emit吗?我们接着往下看。
mitt:事件总线
eventBus,事件总线。mitt是一个事件总线库,基于发布订阅事件在不同组件内进行通信。那么,如何使用mitt?为什么要使用mitt?
发布
mitt使用emit来发布事件。
this.$mitt.emit('eventName', args)
第一个参数是事件名称,第二个是参数。订阅方通过事件名称来接受到事件,然后就可以收到传过来的参数。
订阅
mitt通过on来订阅事件,然后定义一个方法,来接收处理事件传过来的参数。
this.$mitt.on('eventName', function(args))
BuildAdmin和mitt
从上面样例看到,调用emit和on需要使用this.$mitt,即mitt实例。在BuildAdmin中,我们使用的proxy.eventBus来调用的emit和on,也就是说proxy.eventBus代表的就是mitt实例,我们看看两者之间是如何关联的。
全局变量
在vue3中,config.globalProperties是一个全局配置选项,用于设置全局属性或方法,这些属性或方法会被注入到每个组件的实例中。
通常通过应用实例设置全局属性,main.ts中createApp创建的就是应用实例。
app.config.globalProperties.eventBus = mitt()
通过全局变量,将mitt实例绑定在了eventBus变量上,接下来就看如何获取这个变量。
获取变量
通过globalProperties设置的变量,在每个组件中都能访问到,所以我们就定义一个获取当前组件实例的方法。
import {ComponentInternalInstance, getCurrentInstance} from 'vue'
export default function useCurrentInstance() {
if (!getCurrentInstance()) {
throw new Error('useCurrentInstance() can only be used inside setup() or functional components!')
}
const {appContext} = getCurrentInstance() as ComponentInternalInstance
const proxy = appContext.config.globalProperties
return {proxy}
}
定义了useCurrentInstance方法。vue3中,getCurrentInstance就是获取当前组件实例的方法,这里将通过config.globalProperties获取到全局变量,然后赋值给proxy,这样通过proxy.eventBus就能获取到mitt实例了。
const {proxy} = useCurrentInstance()
然后在tabs中,就发布了一个名为onTabViewRefresh的事件,并传递了一个menu,即路由参数。然后就是接收这个事件重新加载页面,页面展示在layout布局中的main中,所以还要去main来了解重新加载的原理。
main
在第二篇的布局中,或者说在ElementPlus的布局组件中,main是路由中展示页面的地方,router-view根据router的跳转,来加载不同的页面。
从上图看,main就一个div来展示页面,当我们切换路由/tab时,当前组件默认被销毁,然后新建跳转的组件展示。
我修改了控制台页面的值,然后切换到其他tab再切换回来时,修改的值就没了。也就意味着,我之前的控制台的页面组件在切换时就被销毁了,在切换过来时又重新创建了一个组件。
如果切换tab就会删除我之前所有的修改,那tab栏的存在将毫无意义,这明显不是我们想要的结果,同时,我们根本也不需要重新加载的功能。
所以,我们需要一个缓存组件的功能,在切换tab时,页面不销毁重建,而是缓存起来直接引用即可。
keep-alive:组件缓存
在vue中,keep-alive功能是在多个组件间动态切换时,缓存原本要被移除的组件实例。在man中,添加keep-alive标签实现缓存。
<el-main class="layout-main">
<div class="main-div">
<router-view v-slot="{ Component }">
<transition :name="config.layout.mainAnimation" mode="out-in">
<keep-alive :include="state.keepAliveComponentNameList">
<component :is="Component" :key="state.componentKey"/>
</keep-alive>
</transition>
</router-view>
</div>
</el-main>
router-view中使用了v-slot,用来接收点击跳转路由是渲染在main区域的组件实例。然后这个组件实例,即Component就会被绑定在动态组件component上,然后被keep-alive缓存起来。
缓存实现
keep-alive中的 :include属性,指向的是组件缓存的一个集合,也就是state.keepAliveComponentNameList。
addKeepAliveComponentName
接着就是向keepAliveComponentNameList中,放入需要缓存组件的名称。定义一个addKeepAliveComponentName方法。
const addKeepAliveComponentName = function (keepAliveName: string | undefined) {
if (keepAliveName) {
let exist = state.keepAliveComponentNameList.find((name: string) => {
return name === keepAliveName
})
if (exist) return
state.keepAliveComponentNameList.push(keepAliveName)
}
}
很简单的逻辑,遍历keepAliveComponentNameList,组件存在就不放入,不存在就push。那什么时候调用这个方法来添加缓存呢。想想之前讲的tab切换是如何实现的,以及tab切换改变了什么?答案是watch和路由。
watch添加缓存
利用watch监控路由的变化,如果路由变化,就调用addKeepAliveComponentName将路由meta的keepalive放到keepAliveComponentNameList中。
watch(
() => route.path,
() => {
state.componentKey = route.path
if (typeof navTabs.state.activeRoute?.meta.keepalive == 'string') {
addKeepAliveComponentName(navTabs.state.activeRoute?.meta.keepalive)
}
}
)
其中,keepalive是从后台请求的菜单中包含的字段,回填的就是组件名称。
还要一种需要添加组件缓存的情况,就是首次访问应用,第一个tab(控制台)的渲染,没有触发路由变化,也就不会触发添加缓存。所以需要在main首次初始化时,调用addKeepAliveComponentName将firstRoute对应的组件添加到缓存中。
onMounted(() => {
nextTick(() => {
if (typeof navTabs.state.activeRoute?.meta.keepalive == 'string') {
addKeepAliveComponentName(navTabs.state.activeRoute?.meta.keepalive)
}
})
})
回想:在tabs中,将activeRoute=firstRoute,完成了初次赋值。然后才是在路由导航守卫赋值。
这个具体可以看之前讲的tabs的实现。
至此,就完成了组件缓存,在页面的修改也不会随着tab的切换而消失。
删除缓存
那么,重新加载就是从keepAliveComponentNameList中删除掉这个组件缓存,这时候就会触发这个组件的重新渲染,即组件的新建。
当tabs中通过mitt发布了onTabViewRefresh事件,在main中通过on接收到了事件,然后触发定义的回调函数。
onBeforeMount(() => {
proxy.eventBus.on('onTabViewRefresh', (menu: RouteLocationNormalized) => {
state.keepAliveComponentNameList = state.keepAliveComponentNameList.filter((name: string) => menu.meta.keepalive !== name)
state.componentKey = ''
nextTick(() => {
state.componentKey = menu.path
addKeepAliveComponentName(menu.meta.keepalive as string)
})
})
})
回调函数的逻辑就是:遍历keepAliveComponentNameList,最后通过filter过滤掉与onTabViewRefresh传入的menu相同的组件,这样就完成了页面的重新加载。
最后,再将加载后的新建的组件,通过addKeepAliveComponentName添加到缓存中,至此完美结束。
结语
重新加载的实现,学到了mitt、全局配置、keep-alive等知识点,是需要多个组件联动的一个功能实现,也是比较有意思的。下一篇将写关闭所有标签、关闭其他标签的功能实现。