Vuevue

七、撸vue/cli 3.+ 的正确姿势(快捷导航/keep-

2019-09-18  本文已影响0人  Baby_ed6e

1、我们的目标,缓存所有访问页面,包括路由嵌套

vue是一个单页应用,意味着无法多页面打开,但是有些用户需要几个页面同时打开操作,所以我们的目标是用快捷导航解决这个问题。
这个快捷导航不单单有快速跳转到对应页面的功能,还必须有缓存功能缓存功能缓存功能

image.png

2、首先吐槽一下网上各种鸡肋的缓存方法。

第一种:根据路由mete里的变量,切换<router-view>
<keep-alive>
  <router-view v-if="this.$route.meat.keepAlive"></router-view>
  <!--这里是会被缓存的组件-->
</keep-alive>
<keep-alive v-if="!this.$router.meta.keepAlive"></keep-alive>
<!--这里是不会被缓存的组件-->

弊端:
(1)切换完了想手动刷新?不可能的!毕竟某些页面需要根据业务逻辑更新。
(2)无法在路由嵌套的情况下使用,缓存不了!(参考 vue-element-admin,也有此问题)
(3)你需要在view下每个组件都写一个name,太可怕了....
(4)虽然有max属性控制缓存个数,但是这里非常消耗内存。

第二种:路由守卫A->B刷新、 B->A 不刷新....

可能我描述不清楚,总之看到这个方法我就放弃了,所以也懒得粘贴代码了。
弊端:
(1)麻烦,全站那么多跳转页面,一个一个写得累哭,还捋不顺逻辑。
(2)需要每个页面写单独路由守卫,如果100个vue文件,那就写100多个
(3)也需要在view下每个组件都写一个name

第三种:管理<keep-alive>的include/exclude,和第一种接近
<keep-alive :include="[a,b]" :exclude="[c,d]">
  <router-view v-if="this.$route.meat.keepAlive"></router-view>
  <!--ab缓存,cd不缓存-->
</keep-alive>

弊端:
(1)无法在路由嵌套的情况下使用,缓存不了!这一点足够致命。
(2)不太容易页面根据业务逻辑更新。

这些方法都避免不了路由嵌套无法缓存问题

3、解决思路

1.我不想每个文件添加name,所以最好动态的给view下所有的文件添加name!
2.不要影响我用路由嵌套,要保证路由层级还是清晰明了的,绝对不因为要搞缓存把路由搞成一层的,最好有个办法自动搞定!
3.不写复杂的跳转刷新逻辑,页面标签作用就是快速跳转至缓存页。所以业务逻辑可以简化为:只有点快捷导航,页面才是上一次缓存的,其它页面任何位置点击都是刷新。

关于路由嵌套不能缓存,简单理解为,以前我们公司的员工网线都集中在一个路由转换器里,给网通公司接一跟网线,但是网通公司就想停我一个人的网,怎么办?没法操作啊!!!因为操作层面不同,路由嵌套做缓存就是这个道理,一个是多级嵌套,一个是只能用在一层的!


网通公司和我们公司的网线关系 顶层<router-view>和我们需要缓存的底层视图关系

假如我们每个人都有网线,都通过一个管子链接给网通公司,那么网通想停我一个人的网,只需要把我的那根网线减掉即可。对应我们这里就是把路由嵌套带来的壁垒给破了。


单独网线连接网通公司 我们需要的页面结构

这样我们只需要根据<router-view>对应的编号,来切换是否缓存对应后面的页面即可。如何实现这种结构,往下看!

4、解决方法,制作成“包裹组件”

为了让每一个页面独立分开,我们可以复制顶层的<router-view>,制作成"包裹组件",并根据路由别名有规律的起组件名。

components 下建立一个tagNav文件夹。
文件夹下创建一个config文件,用来管理“包裹组件”的前缀

const config = {
    componentsPrefix: "tagNav-"
};
export default config;

创建一个"包裹"view.vue。

<template>
    <router-view />
</template>

创建一个index.js,递归循环出所有会在快捷导航里出现的页面,它们的router.name,用来动态创建一个以 "tagNav-"开头的组件组,注册到全局。完成所有页面对应一个单独的组件,这里我们用的是异步组件,只在调用的时候触发函数。所以不必担心性能。

import Vue from "vue";
// 按模块划分的路由文件数组,这里就是导航会出现的几个页面
import router from "@/router/module/index"; 
// config里取得组件前缀
import config from "./config";

const allRouterName = ["home"];
const getName = arr => {
    arr.forEach(item => {
        if (item.name) {
            allRouterName.push(item.name);
        }
        if (item.children && item.children.length > 0) {
            getName(item.children);
        }
    });
};
getName(router);

// 以下注释来自官网:
// <keep-alive>匹配首先检查组件自身的 name 选项
// 如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值)。
// 匿名组件不能被匹配。
allRouterName.forEach(i => {
    Vue.component(config.componentsPrefix + i, () =>
        import("@/components/tagNav/view.vue")
    );
});

在main里引入这个index.js,因为在我们访问页面之前,应该保证这些“包裹组件”可用

// main.js里的文件
import "./components/tagNav/index";

这里所有的工作,只为了一件事:一个路由对应一个“包裹组件”。

最终达成的目标:


目标结构

到这里其实已经实现了每个路由对应的组件在同一个层级下,如果你已经理解了,下面不用再看了。接下来,我们需要在包裹组价外层,添加<keep-alive> !如何添加?往下看!

5、对“包裹组件”嵌套缓存<keep-alive >

1、在tagNav下创建一个layoutView.vue文件,这个文件用来替换之前页面里的<router-view>(结构图中顶层的那个)。
2、原理就是,根据$route.name 来动态展示每个路由对应“包裹组件”。


目标结构图

3、具体的代码如下,根据路由切换,自动匹配到对应的“包裹组件”。
4、这里我们只需要管理include数组,就可以动态管理缓存哪些“包裹组件”,实现刷新某个页面。

<template>
    <keep-alive :include="include">
        <component :is="componentsPrefix + $route.name"> </component>
    </keep-alive>
</template>

<script>
import config from "./config";
export default {
    name: "layoutView",
    data() {
        return {
            componentsPrefix: config.componentsPrefix
        };
    },
    computed: {
        // 从所有快捷导航标签里获得需要缓存的数组
        // 过滤掉我们需要刷新的那个页面的 router.name,即可刷新
        include() {
            let exclude = this.$store.getters.refreshRouterName;
            return this.$store.getters.tagNavList
                .map(i => i.name)
                .filter(i => i != exclude)
                .map(i => this.componentsPrefix + i);
        }
    }
};
</script>

6、用带有缓存的视图组件(layoutView)替换<route-view>

这里<route-view>是所有缓存页面最顶端的那个,也就是嵌套路由的顶层。

<div class="content">
      <layout-view></layout-view>
      <!--<router-view/>-->
</div>

7、(略看)如何制作快捷导航标签,以及如何管理缓存数组

原理就是利用路由变化管理一个个快捷导航,都是针对数组的增删,所以不详细介绍,下面只做参考

导航标签组件

1.tagNav文件下建立 listNav.vue
2.VUEX管理快捷导航列表相关数据。

(1)页面点击 ---> 路由跳转 ---> 新页面,需要刷新

跳转前,需要从include中排除即将访问的页面所对应的"包裹组件",来达到刷新。由于包裹组件注册的key值与路由name有规律,所以不难排除。
首先建立store中管理的数据

const tag = {
    state: {
        isKeepAlive: false, // 跳转前,可修改这个开关变量,在路由拦截里判断是否记录router.name
        refreshRouterName: "", // 记录需要刷新的router.name
        tagNavList: [      //  记录标签里的信息。
            {
                label: "首页",
                path: "/",
                name: "home",
                lastTime: 0 //上一次打开时间戳
            }
        ]
    },
    // 一些对上面几个状态的管理基本操作
    // 对数组的增删改查,关闭某个标签,增加一个标签,关闭以后根据lastTime时间戳定位上一个页面等等
    ...... 
}

其次路由拦截里,判断本次访问是否需要刷新,记录刷新的页面to.name
这里访问前我用了全局状态管理了一个开关变量 isKeepAlive,只需要访问前改变这个变量,就可以在路由拦截里得知将要去的页面,是不是需要刷新。
如果需要刷新,那么我们就把将要去的页面的router.name 记录下来,存放在refreshRouterName里。

router.beforeEach((to, from, next) => {
        if (store.getters.isKeepAlive) {
            store.dispatch("setRefreshRouterName", "");
        } else {
            store.dispatch("setRefreshRouterName", to.name);
        }
        store.dispatch("setIsKeepAlive", false);
        next();
});

然后利用全局状态 refreshRouterName,来改变layoutView.vue里的include
这样我们就达到,在访问页面前,从缓存数组里清除了该页面的“包裹数组”。达到了刷新目的。

页面刷新以后,还需要重启页面的缓存(加入include),为下次可能是标签切换做好缓存准备
所以需要完善layoutView.vue 文件,监听路由改变以后,页面重新渲染后,也就是$nextTick()以后,重现加入缓存的队伍当中。整个过程完成页面重启(刷新)
完善后的代码:

<template>
    <keep-alive :include="include">
        <component :is="componentsPrefix + $route.name"> </component>
    </keep-alive>
</template>

<script>
import config from "./config";
export default {
    name: "layoutView",
    data() {
        return {
            componentsPrefix: config.componentsPrefix
        };
    },
    computed: {
        include() {
            let exclude = this.$store.getters.refreshRouterName;
            return this.$store.getters.tagNavList
                .map(i => i.name)
                .filter(i => i != exclude)
                .map(i => this.componentsPrefix + i);
        }
    },
    watch: {
        // 刷新后,再次切换为缓存状态,为下次跳转做好缓存准备。
        $route: {
            handler() {
                this.$nextTick(() => {
                    this.$store.dispatch("setRefreshRouterName", "");
                });
            },
            deep: true
        }
    }
};
</script>
(2)快捷导航 ---> 路由跳转 ---> 新页面,不需要从include中排除,访问的是缓存。

根据以上代码,在访问前,我们需要告诉路由拦截,我们即将访问的页面,你不需要刷新,所以你不必记录 refreshRouterName了,跳转前需要改变isKeepAlive 为true。

this.$store.dispatch("setIsKeepAlive", true);
(3)以下是tagList部分代码的管理,仅做参考用:
<template>
    <div class="tagList">
        <scroll-pane class="tags-view-wrapper" ref="scrollPane">
            &nbsp;
            <font
                @click="changeOn(item)"
                :key="index"
                v-for="(item, index) in list"
                ref="tags"
                :data-path="item.path"
                :data-name="item.name"
            >
                <template v-if="$route.name === item.name">
                    <a-tag
                        v-if="item.name === 'home'"
                        color="blue"
                        class="tags"
                        :data-name="item.name"
                    >
                        {{ item.label }}
                    </a-tag>
                    <a-tag
                        v-else
                        color="blue"
                        closable
                        @close.stop.prevent="handleClose(item)"
                        class="tags"
                        :data-name="item.name"
                    >
                        {{ item.label }}
                    </a-tag>
                </template>
                <template v-else>
                    <a-tag
                        v-if="item.name === 'home'"
                        class="tags"
                        :data-name="item.name"
                    >
                        {{ item.label }}
                    </a-tag>
                    <a-tag
                        v-else
                        closable
                        @close.stop.prevent="handleClose(item)"
                        class="tags"
                        :data-name="item.name"
                    >
                        {{ item.label }}
                    </a-tag>
                </template>
            </font>
        </scroll-pane>
        <div class="close" @click="closeAllTag">关闭其它</div>
    </div>
</template>
<script>
import scrollPane from "@/components/scrollPane";
export default {
    name: "listNav",
    data() {
        return {};
    },
    computed: {
        list() {
            return this.$store.getters.tagNavList;
        }
    },
    components: {
        scrollPane
    },
    methods: {
        // 新增或者更新标签数组
        updateTag: function() {
            let nowRouter = this.$route;
            let obj = {
                label: nowRouter.meta.tagName || nowRouter.meta.title,
                path: nowRouter.path,
                name: nowRouter.name,
                lastTime: new Date().getTime()
            };
            this.$store.dispatch("pushOrUpdateTagNavList", obj);
            this.moveToCurrentTag();
        },
        // 关闭指定标签
        handleClose: function(item) {
            this.$store.dispatch("deleteTagNavList", item);
            let parentsTag = [...this.list].sort((a, b) => {
                return b.lastTime - a.lastTime;
            })[0];
            // 跳转为缓存
            this.$store.dispatch("setIsKeepAlive", true);
            this.$router.push({
                path: parentsTag.path
            });
            this.moveToCurrentTag();
        },
        // 切换至标签上
        changeOn: function(item) {
            // 跳转为缓存
            this.$store.dispatch("setIsKeepAlive", true);
            this.$router.push({
                path: item.path
            });
            this.moveToCurrentTag();
        },
        // 滚动条移动到对应的标签
        moveToCurrentTag: function() {
            this.$nextTick(() => {
                const tags = this.$refs["tags"];
                for (const tag of tags) {
                    if (tag.dataset.name === this.$route.name) {
                        this.$refs.scrollPane.moveToTarget(tag);
                        break;
                    }
                }
            });
        },
        // 关闭所有
        closeAllTag: function() {
            this.$store.dispatch("closeAllTagNavList");
            this.updateTag();
        }
    },
    watch: {
        $route: {
            handler: function() {
                this.$nextTick(() => {
                    this.updateTag();
                });
            },
            immediate: true,
            deep: true
        }
    }
};
</script>
上一篇 下一篇

猜你喜欢

热点阅读