自学前端13:vue如何使用mitt事件总线,实现组件之间的通信

社区Vue前端框架

前言

关于弹出框,前几篇主要讲了如何渲染弹出框标签、实现弹出框的弹出位置、触发弹出框以及弹出框组件和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()

picture.image

通过全局变量,将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的跳转,来加载不同的页面。

picture.image

从上图看,main就一个div来展示页面,当我们切换路由/tab时,当前组件默认被销毁,然后新建跳转的组件展示。

picture.image

我修改了控制台页面的值,然后切换到其他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缓存起来。

缓存实现

picture.image

keep-alive中的 :include属性,指向的是组件缓存的一个集合,也就是state.keepAliveComponentNameList

picture.image

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是从后台请求的菜单中包含的字段,回填的就是组件名称。

picture.image

还要一种需要添加组件缓存的情况,就是首次访问应用,第一个tab(控制台)的渲染,没有触发路由变化,也就不会触发添加缓存。所以需要在main首次初始化时,调用addKeepAliveComponentName将firstRoute对应的组件添加到缓存中。

onMounted(() => {
    nextTick(() => {
        if (typeof navTabs.state.activeRoute?.meta.keepalive == 'string') {
            addKeepAliveComponentName(navTabs.state.activeRoute?.meta.keepalive)
        }
    })
})

回想:在tabs中,将activeRoute=firstRoute,完成了初次赋值。然后才是在路由导航守卫赋值。

picture.image

这个具体可以看之前讲的tabs的实现。

至此,就完成了组件缓存,在页面的修改也不会随着tab的切换而消失。

picture.image

删除缓存

那么,重新加载就是从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添加到缓存中,至此完美结束。

picture.image

结语

重新加载的实现,学到了mitt、全局配置、keep-alive等知识点,是需要多个组件联动的一个功能实现,也是比较有意思的。下一篇将写关闭所有标签、关闭其他标签的功能实现。

0
0
0
0
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论