前言
一直比较好奇vue-element-admin 与vban 这些中台集成方案中Tap标签页重新加载(刷新当前页)功能是怎么实现的,经过长期搜索以及查看实现代码再此做出总结,方便日后,直接实现
实现方案
重点讲解第3与第4点
- 使用 location. reload()与this.$router.go(0)缺点会整个页面刷新,页签也会丢失(不建议)
- 使用 provide / inject,其实就是在主入口 route-view 中增加v-if 判断,provide 注入事件, 当子页面刷新调用 触发入口文件事件(不建议)子页面要调用方法 不智能| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 
 | <router-view if="isRouterAlive" />
 <srtipt>
 export default {
 name: 'app',
 provide (){
 return {
 reload:this.reload
 }
 },
 data(){
 return {
 isRouterAlive:true,
 }
 },
 methods:{
 reload (){
 this.isRouterAlive = false
 this.$nextTick(()=>{
 this.isRouterAlive = true
 })
 }
 }
 }
 </script>
 
 |  
 
- route-view 绑定Key, 当触发事件,让key++,实现页面刷新(有点不推荐,只要一个页面刷新,会影响其他页面keep-alive)
- 使用中间页进行重定向(推荐)
route-view 绑定Key 方法
原理其实很简单,就是为入口main.js 的 标签绑定Key值,这个key值在vuex中维护,当触发vuex中指定mutations 方法,使key++ 或者 key = Date.now()。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 
 | <template>
 <div :key="refreshTime">
 <keep-alive>
 <router-view />
 </keep-alive>
 </div>
 </template>
 
 <script>
 export default {
 data() {
 return {
 refreshTime: ''
 }
 },
 
 watch: {
 '$store.state.tagsView.refreshTime': function () {
 this.refreshTime = this.$store.state.tagsView.refreshTime
 }
 },
 }
 </script>
 
 
 
 const tagsView = {
 state: {
 refreshTime: '',
 },
 mutations: {
 REFRESH_TIME: (state) => {
 state.refreshTime = (new Date()).valueOf()
 },
 },
 }
 
 export default tagsView
 
 | 
但是此做法会导致其他页面keep-alive 缓存失效,既已打开标签,在某个已打开页面中执行刷新方法,在切换其他页签页面,其他页面也会重新刷新
使用中间页进行重定向
该方法也很简单,先携带上一个页面的路由地址以及参数跳转到一个中间页面,然后在根据参数使用router.replace重定向回去。下面通过vban与vue-element-admin两个中台示例进行分析
在分析之前要说下,两个中台的vue/pinia都有存储
cacheTabList: 缓存标签(用于keep-alive :include的值)
tabList:展示标签(用于tap标签遍历展示)
vban 实现刷新当前页
![vban 实现刷新当前页 vban 实现刷新当前页]()
我们直接看到/src/layouts目录下,这使存储公共的布局文件,我们直接看向/src/layouts/default/tabs/components/TabContent.vue
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 
 | 
 
 
 <template>
 <Dropdown
 :dropMenuList="getDropMenuList"
 :trigger="getTrigger"
 placement="bottom"
 overlayClassName="multiple-tabs__dropdown"
 @menu-event="handleMenuEvent"
 >
 ...
 </span>
 </Dropdown>
 </template>
 <script lang="ts">
 import { useTabDropdown } from '../useTabDropdown';
 export default defineComponent({
 setup(props) {
 const { getDropMenuList } = useTabDropdown(
 props as TabContentProps,
 getIsTabs,
 );
 ...
 return {
 getDropMenuList,
 };
 },
 });
 </script>
 
 | 
useTabDropdown.ts 配置了右键 tap展开的选项配置,配置中有icon、event触发事件,text:下拉名称,disabled是否展示,最重要的还是event,其中event为MenuEventEnum.REFRESH_PAGE 就是我们的重新加载的事件了,然后通过handleMenuEvent事件执行对应key 的事件
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 
 | import { useTabs } from '/@/hooks/web/useTabs';const { refreshPage, closeAll, close, closeLeft, closeOther, closeRight } = useTabs();
 const dropMenuList: DropMenu[] = [
 {
 icon: 'ion:reload-sharp',
 
 
 event: MenuEventEnum.REFRESH_PAGE,
 text: t('layout.multipleTab.reload'),
 disabled: refreshDisabled,
 },
 {
 icon: 'clarity:close-line',
 
 event: MenuEventEnum.CLOSE_CURRENT,
 text: t('layout.multipleTab.close'),
 disabled: !!meta?.affix || disabled,
 divider: true,
 },
 {
 icon: 'line-md:arrow-close-left',
 
 event: MenuEventEnum.CLOSE_LEFT,
 text: t('layout.multipleTab.closeLeft'),
 disabled: closeLeftDisabled,
 divider: false,
 },
 {
 icon: 'line-md:arrow-close-right',
 
 event: MenuEventEnum.CLOSE_RIGHT,
 text: t('layout.multipleTab.closeRight'),
 disabled: closeRightDisabled,
 divider: true,
 },
 {
 icon: 'dashicons:align-center',
 
 event: MenuEventEnum.CLOSE_OTHER,
 text: t('layout.multipleTab.closeOther'),
 disabled: disabled || !isCurItem,
 },
 {
 icon: 'clarity:minus-line',
 
 event: MenuEventEnum.CLOSE_ALL,
 text: t('layout.multipleTab.closeAll'),
 disabled: disabled,
 },
 ];
 return dropMenuList;
 });
 <!--事件触发-->
 function handleMenuEvent(menu: DropMenu): void {
 const { event } = menu;
 switch (event) {
 case MenuEventEnum.REFRESH_PAGE:
 
 
 refreshPage();
 break;
 
 case MenuEventEnum.CLOSE_CURRENT:
 close(tabContentProps.tabItem);
 break;
 
 case MenuEventEnum.CLOSE_LEFT:
 closeLeft();
 break;
 
 case MenuEventEnum.CLOSE_RIGHT:
 closeRight();
 break;
 
 case MenuEventEnum.CLOSE_OTHER:
 closeOther();
 break;
 
 case MenuEventEnum.CLOSE_ALL:
 closeAll();
 break;
 }
 
 | 
refreshPage 事件是由/src/hooks/web/useTabs.ts 路径下 导出了useTabs 方法,看向导出执行事件为refreshPage()
![vban useTabs vban useTabs]()
tabStore.refreshPage(router);
调用则为pinia中位于
src/store/modules/multipleTab.ts
中抛出来的
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
const tabStore = useMultipleTabStore();
调用了它的refreshPage方法,我们下面直接看pinia 实现
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 
 | import type { Router } from 'vue-router';import { unref } from 'vue';
 import { useRedo } from '/@/hooks/web/usePage';
 
 export const useMultipleTabStore = defineStore({
 id: 'app-multiple-tab',
 state: (): MultipleTabState => ({
 cacheTabList: [],
 tabList:  [],
 }),
 getters: {
 getCachedTabList(): string[] {
 return Array.from(this.cacheTabList);
 },
 },
 actions: {
 
 
 
 
 async refreshPage(router: Router) {
 
 const { currentRoute } = router;
 
 const route = unref(currentRoute);
 
 const name = route.name;
 
 const findTab = this.getCachedTabList.find((item) => item === name);
 if (findTab) {
 this.cacheTabList.delete(findTab);
 }
 
 
 const redo = useRedo(router);
 await redo();
 }
 },
 });
 
 | 
方法中获取了我们当前要刷新的router 对象获取到router 对象首先要判断舒服在缓存数组中,如果在缓存数组中则删除缓存,然后执行useRedo 抛出来的方法redo();
useRedo所在位置为src/hooks/web/usePage.ts, 直接看redo 实现
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 
 | 
 
 export const useRedo = (_router?: Router) => {
 
 const { replace, currentRoute } = _router || useRouter();
 
 const { query, params = {}, name, fullPath } = unref(currentRoute.value);
 
 function redo(): Promise<boolean> {
 return new Promise((resolve) => {
 
 if (name === REDIRECT_NAME) {
 resolve(false);
 return;
 }
 
 if (name && Object.keys(params).length > 0) {
 params['_redirect_type'] = 'name';
 params['path'] = String(name);
 } else {
 
 params['_redirect_type'] = 'path';
 params['path'] = fullPath;
 }
 
 
 replace({ name: REDIRECT_NAME, params, query }).then(() => resolve(true));
 });
 }
 return redo;
 };
 
 | 
- 获取到传入router 对象,如果没传入,则获取当前路由route对象
- 将取消响应式
- 创建redo方法,判断当前页面是否是重置页面,如果是重置页面,不执行任何操作直接resolve(false);
- 如果router.name存在 _redirect_type 跳转模式为name, 跳转path 为 name,如果router.name不存在_redirect_type 跳转模式为path,path 为 fullPath
- 执行router. Replace 放啊进行跳转至中间页面
- 中间页为/redirect,页面存放地方为src/views/sys/redirect/index.vue
![vban redirect中间页实现 vban redirect中间页实现]()
中间页将在beforCreate 生命周期直接跳转到 传入信息的path.
vue-element-admin 实现刷新当前页
由于vue-element-admin没使用typeScript,并没有过度封装,直接看src/layout/components/TagsView/index.vue路径文件代码
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 
 | <template><div id="tags-view-container" class="tags-view-container">
 <scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
 ...
 </scroll-pane>
 <ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
 
 <li @click="refreshSelectedTag(selectedTag)">Refresh</li>
 <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">Close</li>
 <li @click="closeOthersTags">Close Others</li>
 <li @click="closeAllTags(selectedTag)">Close All</li>
 </ul>
 </div>
 </template>
 
 <script>
 import ScrollPane from './ScrollPane'
 import path from 'path'
 
 export default {
 methods: {
 
 refreshSelectedTag(view) {
 
 this.$store.dispatch('tagsView/delCachedView', view).then(() => {
 const { fullPath } = view
 this.$nextTick(() => {
 this.$router.replace({
 path: '/redirect' + fullPath
 })
 })
 })
 },
 }
 }
 </script>
 
 | 
以上代码主要在vuex tagsView/delCachedView 事件执行,/redirect路由 定义
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 
 | export const constantRoutes = [
 {
 path: '/redirect',
 component: Layout,
 hidden: true,
 children: [
 {
 path: '/redirect/:path(.*)',
 component: () => import('@/views/redirect/index')
 }
 ]
 }
 ]
 
 
 
 <script>
 export default {
 created() {
 const { params, query } = this.$route
 const { path } = params
 this.$router.replace({ path: '/' + path, query })
 },
 render: function(h) {
 return h()
 }
 }
 </script>
 
 | 
vuex tagsView/delCachedView
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 
 | const state = {visitedViews: [],
 cachedViews: []
 }
 
 const mutations = {
 DEL_CACHED_VIEW: (state, view) => {
 <!-- 查看当前页面是否在缓存队列中 -->
 const index = state.cachedViews.indexOf(view.name)
 <!-- 如果由则删除 -->
 index > -1 && state.cachedViews.splice(index, 1)
 },
 }
 
 const actions = {
 delCachedView({ commit, state }, view) {
 return new Promise(resolve => {
 commit('DEL_CACHED_VIEW', view)
 resolve([...state.cachedViews])
 })
 },
 
 }
 
 export default {
 namespaced: true,
 state,
 mutations,
 actions
 }
 
 |