学学吧

大家好,我是张浔

本篇文章讲述的是在 Vue2 中使用 Vue Router 的知识,阅读时间大概3分钟

路由进阶你确定不学吗?

前言

经过前几篇的学习,小伙伴学会了如何实现对元素和组件之间过渡的动画效果,是不是感觉挺好玩的,哈哈,发现炫酷的东西都比较吸引人,可是只是这些就满足了吗?经常逛一些教程网站的小伙伴可能发现过在路由跳转之间会有一个过渡动画的效果,比如慢慢消失再慢慢出现的效果,也许还会有一个进度条、loading等等好多好玩的东西,但是当时的你只是去查找资料去了,就没太在意这个东西,也就过去了,那么今天既然看到这篇文章了,如果你对我刚刚所说的感兴趣,请看完这篇文章,一定让你搞明白它具体是怎么实现的,搞不明白找我。

太多的字,小伙伴可能觉得啰嗦,可以直接忽略上面文字看下面动图的效果,这是本篇文章的主题。

路由过渡+进度条效果

GIF 2022-5-15 18-06-14

路由过渡+loading效果

GIF 2022-5-15 18-08-47

看过我这篇 《用一用Vant组件库》文章的小伙伴会觉得上面的动图演示很熟悉,今天的这个案例我进行了改造,不过是自己原生实现的,并没有使用第三方组件库,因为组件库有很多内置的动态效果,不便于演示。

路由过渡动效

那我们就来搞一搞吧,首先去官网 过渡动效 看看是怎么介绍的

image-20220515173701689

看了官网的介绍和示例代码,发现就是用 transition 组件把 router-view 动态组件包裹起来就好了,原来如此的简单,还说 transition 组件的所有功能都可以使用,并没有什么冲突。

对于 router-view 动态组件,小伙伴们肯定知道吧,意思就是路由的出口,路由匹配到的组件将会被渲染在这里,这时,小伙伴看到动态组件,是不是想到了之前我的那一篇 《学一学在Vue2中实现过渡&动画的初始渲染和多个元素and组件过渡的效果》文章,里面讲到多个组件的过渡时,也是使用了动态组件 component ,这时候你想它们有什么不同吗?

我来告诉你,它们俩个最大的不同点就是在于 router-view 是根据路由地址变化,component 而是根据已注册的组件名字变化,听完我的解释,是不是豁然开朗呢?要不眼见为实一下吧,我把讲述多个组件的过渡那篇文章中展示的案例全屏给你们看一下,看看路由地址有没有变化,看下图。

GIF 2022-5-15 17-54-20

看完动图,你会发现我点击微信登录与密码登录的时候,地址栏并没有变化,上面的 Fade、Multiple、Switch、MultipleDemo是通过 router-link 跳转的路由导航,相当于 a 超链接标签,所以路由地址发生了变化,懂了吧。

接下来,就来写路由之间的过渡效果吧,用 transition 组件把 router-view 组件包裹起来,加动画效果就可以了。

代码

通过前言中演示的动图效果,小伙伴们知道我们要实现的是什么效果了,为了后面更好的讲述,我把初步的代码先都放上来,然后再一步一步去修改,这样你能搞明白全部流程。

数据来源均来自后台接口,我先是将 axios 请求进行了二次封装,再引入需要的接口进行请求。

src/api/index.js 文件

// 引入 axios 模块
import axios from 'axios'

// 设置基准地址
axios.defaults.baseURL = 'http://127.0.0.1:3000/api'

// 获取导航列表接口
export const getNavListAPI = () => {
    return axios.get('/cate')
}

// 重置数据接口
export const resetAPI = () => {
    return axios.get('/reset')
}

/**
 * 获取文章列表
 * 根据 cateId 类别id值获取不同的文章列表
 * 1: 表示获取热点文章列表
 * 2: 表示获取本地文章列表
 * 3: 表示获取财经文章列表
 */
export const getArticleListAPI = cateId => {
    return axios.get(`/blog/list/${cateId}`)
}

Header.vue 头部导航组件

<template>
    <nav>
      <!-- 路由跳转 -->
        <a
            :class="{ active: activeIndex === item.id }"
            href="#"
            v-for="item in navList"
            :key="item.id"
            v-on:click.prevent="handleClick(item.id)"
            >{{ item.title }}</a
        >
        <!-- 重置按钮 -->
        <span v-on:click="reset">重置</span>
    </nav>
</template>

<script>
// 引入封装好的 获取导航列表 接口、重置数据接口
import { getNavListAPI, resetAPI } from '@/api'

export default {
    name: 'HeaderWrap',
    data () {
        return {
            // 导航列表
            navList: [],
            // 当前导航激活的索引
            activeIndex: 1
        }
    },
    async created () {
        // 获取导航列表
        const res = await getNavListAPI()
        // 如果获取失败停止执行
        if (res.data.code !== 200) return
        // 为导航列表属性赋值
        this.navList = res.data.data
    },
    methods: {
        // 点击导航跳转路由
        handleClick (id) {
            /**
             * 判断
             * 如果点击导航还是当前的路由地址不进行跳转
             * id 为 1 跳转到热点 /hot 路由
             * id 为 2 跳转到本地 /local 路由
             * id 为 3 跳转到财经 /money 路由
             */
            this.activeIndex = id
            if (id === 1 && this.$route.path !== '/hot') {
                this.$router.push('/hot')
            } else if (id === 2 && this.$route.path !== '/local') {
                this.$router.push('/local')
            } else if (id === 3 && this.$route.path !== '/money') {
                this.$router.push('/money')
            }
        },
        // 点击重置按钮恢复数据
        async reset () {
            // 向父组件传值
            const res = await resetAPI()
            // 如果获取失败停止执行
            if (res.data.code !== 200) return
            // 刷新当前页面
            this.$router.go()
        }
    }
}
</script>

<style lang="less" scoped>
nav {
    padding: 0.1333rem 0px;
    border: 1px solid #ccc;
    text-align: center;
    a {
        color: #000;
        text-decoration: none;
        padding: 0px 0.32rem;
        font-size: 0.48rem;
    }
    // 导航被激活的样式
    .active {
        color: red;
    }
    // 重置
    span {
        font-size: .32rem;
        cursor: pointer;
    }
}
</style>

ArticleList.vue 文章列表组件

<template>
  <div class="article-container">
    <!-- 文章列表 -->
    <div class="article" v-for="item in article" :key="item.id">
      <!-- 标题 -->
      <h1 v-on:click="handleClick(item.id)">{{ item.title }}</h1>
      <!-- 底部 -->
      <footer>
        <!-- 信息 -->
        <div class="info">
          <!-- 来源 -->
          <span>{{ item.resource }}</span>
          <!-- 评论 -->
          <span>{{ item.comment_count }}评论</span>
          <!-- 发布时间 -->
          <span>{{ item.create_time }}</span>
        </div>
        <!-- x 图标 -->
        <span v-on:click="delArticle(item.id)">X</span>
      </footer>
    </div>
  </div>
</template>

<script>
export default {
    name: 'ArticleList',
    props: ['article']
</script>

<style lang="less" scoped>
.article-container {
  padding: 0.2667rem;
  // 文章
  .article {
    padding: 0.2667rem 0px;
    border-bottom: 1px solid #cccccc;
    // 标题
    h1 {
      font-size: 0.3733rem;
      margin-bottom: 0.2667rem;
      color: #0a0a0a;
      // 点击标题时效果
      &:active {
        color: red;
      }
    }
    // 底部
    footer {
      display: flex;
      justify-content: space-between;
      color: #6e6e6e;
      font-size: 0.32rem;
      // 信息
      .info {
        span {
          margin-right: 20px;
          // 来源
          &:nth-child(1) {
            color: red;
          }
        }
      }
      // x 图标
      span {
        cursor: pointer;
      }
    }
  }
}
</style>

Hot.vue 热点新闻组件

<template>
  <div class="hot-container">
    <!-- 文章列表组件 -->
    <ArticleList
      :article="List"
      @del="delArticle"
      ref="ArticleList"
    ></ArticleList>
  </div>
</template>

<script>
// 引入文章列表 ArticleList 组件
import ArticleList from '../components/ArticleList.vue'
// 引入封装好的 获取文章列表接口
import { getArticleListAPI } from '@/api'

export default {
    name: 'HotView',
    // 注册组件
    components: {
        ArticleList
    },
    data () {
        return {
            // 热点文章列表
            List: [],
        }
    },
    created () {
    // 获取热点文章列表
        this.getList()
    },
    methods: {
        // 获取热点文章列表
        async getList () {
            // 请求数据
            const res = await getArticleListAPI(1)
            // 如果获取失败停止执行
            if (res.data.code !== 200) return
            // 为热点文章列表赋值
            this.List = res.data.data
        }
    }
}
</script>

<style lang="less" scoped>
.hot-container {
  cursor: pointer;
}
</style>

Local.vue 本地新闻和 Money.vue 财经新闻组件的代码就不放了,因为它们的结构和热点新闻组件结构一模一样,只需改变第 38 行请求数据时传递的分类 id 就可以了。

App.vue 组件

<template>
  <div id="app">
    <!-- 头部导航组件 -->
    <Header></Header>
    <!-- 过渡&动画组件 -->
    <transition
      appear
      mode="out-in"
      enter-active-class="animate__animated animate__fadeInLeft animate__fast"
      leave-active-class="animate__animated animate__fadeOutRight animate__fast"
    >
      <!-- 路由出口 -->
      <router-view />
    </transition>
  </div>
</template>

<script>
// 引入头部导航组件
import Header from './views/Header/Header.vue'
export default {
    // 注册组件
    components: {
        Header
    }
}
</script>

<style>
</style>

如果小伙伴对于 transition 组件中的 appear、mode、enter-active-class、leave-active-class 属性不理解的话,需要去看《 教你用Vue2如何做过渡&动画效果+著名动画库 》这篇文章。

写完如上的代码后,效果如下

GIF 2022-5-15 19-10-07

看动图效果,随着路由地址发生变化,展示过渡动画的效果。此时发现所有的路由都是设置一样的过渡效果,难道不能设置不同的过渡效果吗?

Vue 官网早已想到了这一点,也有对应的解决方案 单个路由的过渡 ,看下图。

image-20220515191748802

官网文档说的很清楚,我就不一一展示了,感兴趣的小伙伴自行根据示例中的代码设置就好了。

数据获取

此时,路由之间的过渡动画效果实现了,那么博主开头所说的加进度条、loading 效果该怎么实现呢?

实现的方案有很多,比如:

  • 在每次请求数据时进行拦截,请求前可以让它展示进度条或 loading,请求成功后隐藏进度条或 loading
  • 使用 Vue Router 的路由导航守卫的方式实现

我们这里使用后者,因为讲述的是 Vue Router 的数据获取知识嘛,具体如何实现呢?

先看看官网 Vue Router—数据获取 文档中的介绍

image-20220515193457324

看完官网的介绍,了解到分为两种方式,下面我们来详细的介绍一下吧

1. 导航完成之后获取

image-20220515193858310

官网说的也是非常的清楚,在 created 钩子函数中获取数据,在获取期间展示一个 loading。

经常用 Vue 做开发的小伙伴一定非常熟悉这种获取数据的方式了,基本是形影不离的,不了解钩子函数的小伙伴,可以去看官网 Vue2—实例生命周期钩子 文档,介绍的很明白,看不懂的小伙伴,等我后面出一篇详解 Vue 生命周期的文章分享给大家。

下面我们就来实现一下吧

代码

首先我们需要一个 loading 组件,这里,我引用了菜鸟教程中的 HTML5+CSS3 最酷的 loading 效果收集 这篇文章里的演示效果代码,感觉挺炫酷的,当然了,我们也可以自己实现一个。

src/components/Loading.vue

<template>
  <div class="loader">
    <div class="loader-inner" v-if="isLoading">
      <div class="loader-line-wrap">
        <div class="loader-line"></div>
      </div>
      <div class="loader-line-wrap">
        <div class="loader-line"></div>
      </div>
      <div class="loader-line-wrap">
        <div class="loader-line"></div>
      </div>
      <div class="loader-line-wrap">
        <div class="loader-line"></div>
      </div>
      <div class="loader-line-wrap">
        <div class="loader-line"></div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
    name: 'LoadingWrap',
    // isLoading 属性用来接收父组件的传值
    props: ['isLoading']
}
</script>

<style>
.loader-inner {
  bottom: 0;
  height: 60px;
  left: 0;
  margin: auto;
  position: absolute;
  right: 0;
  top: 40px;
  width: 100px;
}

.loader-line-wrap {
  animation: spin 2000ms cubic-bezier(0.175, 0.885, 0.32, 1.275) infinite;
  box-sizing: border-box;
  height: 50px;
  left: 0;
  overflow: hidden;
  position: absolute;
  top: 0;
  transform-origin: 50% 100%;
  width: 100px;
}
.loader-line {
  border: 4px solid transparent;
  border-radius: 100%;
  box-sizing: border-box;
  height: 100px;
  left: 0;
  margin: 0 auto;
  position: absolute;
  right: 0;
  top: 0;
  width: 100px;
}
.loader-line-wrap:nth-child(1) {
  animation-delay: -50ms;
}
.loader-line-wrap:nth-child(2) {
  animation-delay: -100ms;
}
.loader-line-wrap:nth-child(3) {
  animation-delay: -150ms;
}
.loader-line-wrap:nth-child(4) {
  animation-delay: -200ms;
}
.loader-line-wrap:nth-child(5) {
  animation-delay: -250ms;
}

.loader-line-wrap:nth-child(1) .loader-line {
  border-color: hsl(0, 80%, 60%);
  height: 90px;
  width: 90px;
  top: 7px;
}
.loader-line-wrap:nth-child(2) .loader-line {
  border-color: hsl(60, 80%, 60%);
  height: 76px;
  width: 76px;
  top: 14px;
}
.loader-line-wrap:nth-child(3) .loader-line {
  border-color: hsl(120, 80%, 60%);
  height: 62px;
  width: 62px;
  top: 21px;
}
.loader-line-wrap:nth-child(4) .loader-line {
  border-color: hsl(180, 80%, 60%);
  height: 48px;
  width: 48px;
  top: 28px;
}
.loader-line-wrap:nth-child(5) .loader-line {
  border-color: hsl(240, 80%, 60%);
  height: 34px;
  width: 34px;
  top: 35px;
}

@keyframes spin {
  0%,
  15% {
    transform: rotate(0);
  }
  100% {
    transform: rotate(360deg);
  }
}
</style>

修改 Hot.vue 、Local.vue、Money.vue 组件中的代码,如下

<template>
  <div class="hot-container">
    <!-- loading 组件 -->
    <Loading :isLoading="isLoading"></Loading>
    <!-- 文章列表组件 -->
    <ArticleList
      :article="List"
      @del="delArticle"
      ref="ArticleList"
    ></ArticleList>
  </div>
</template>

<script>
// 引入文章列表 ArticleList 组件
import ArticleList from '../components/ArticleList.vue'
// 引入封装好的 获取文章列表接口、删除文章接口
import { getArticleListAPI, delArticleAPI } from '@/api'
// 引入 Loading 组件
import Loading from '../components/Loading.vue'

export default {
    name: 'HotView',
    // 注册组件
    components: {
        ArticleList,
        Loading
    },
    data () {
        return {
            // 热点文章列表
            List: [],
            // 控制是否展示 loading
            isLoading: false
        }
    },
   	// 钩子函数
    created () {
    // 获取热点文章列表
        this.getList()
    },
    methods: {
        // 获取热点文章列表
        async getList () {
            // 显示 loading
            this.isLoading = true
            // 请求数据
            const res = await getArticleListAPI(1)
            // 如果获取失败停止执行
            if (res.data.code !== 200) return
            // 为热点文章列表赋值
            this.List = res.data.data
            // 数据获取成功时隐藏 loading
            this.isLoading = false
        },
    }
}
</script>

<style lang="less" scoped>
/**
 * 注意一定要给当前盒子加上相对定位
 * 因为 loading 是绝对定位, 不加相对定位的话默认是根据当前文档定位的
 */
.hot-container {
  position: relative;
  cursor: pointer;
}
</style>

在运行时,为了方便大家更好的看到效果,我将浏览器的网络速度调低了,怎么设置?看下图就明白了

image-20220515201324617

  • 默认值是 No throttling,意思是没有节流的意思
  • Fast 3G 意思是高速3G
  • Slow 3G 意思是低速3G
  • Offline 意思是离线,就是没有网络了

效果

GIF 2022-5-15 20-08-08

此时就已经实现了显示隐藏 loading 的效果了,是不是很好玩,加 loading 就是提升用户体验,不然在加载数据时,如果网络较慢的话,用户那里看到一片空白,多么无味啊,加了动画效果是不是很有新鲜感。

2. 导航完成前获取数据

还是先去看官网 在导航完成前获取数据 文档中是怎么介绍使用的

image-20220515202215068

看完介绍,聪明的小伙伴应该知道该怎么使用了,我们得知要使用 beforeRouteEnter 守卫获取数据,获取成功后调用 next 方法,有小伙对于 next 方法可能不了解,看示例代码也是看不懂,没关系,我们去官网文档中找找,有没有详细说明 beforeRouteEnter 导航守卫的。

接触过的小伙伴知道这叫组件内的守卫,没有接触过也没有关系,我们一起来看看 组件内的守卫

image-20220515203012300

小伙伴们仔细的读我画框中的内容,我们得知这个导航守卫是在路由跳转前执行,它不能通过 this 获取组件实例,原因也说明了,因为执行的时候还没有创建组件实例。

看到这里,有小伙伴懵了,不能获取组件实例,那么我们获取数据后怎么给组件中的 data 属性赋值呢?别急,这个时候 next 方法就起作用了。

咱们先去看看官网中的示例代码是怎么写的

image-20220515204208855

看我标注的地方就可以了,我们分析一下啊,发现它在 next 方法中传了一个回调函数,回调函数中传了一个参数为 vm ,然后它使用 vm 调用了在 methods 中定义的 setData 方法,诶,不是说不能获取组件实例吗,怎么它调用组件实例中的方法了,我们试一试,这样可以成功吗?

我在 Hot.vue 组件中加上了如下代码

beforeRouteEnter (to, from, next) {
    next((vm) => console.log(vm))
},

效果

GIF 2022-5-15 20-51-00

看上面动图,你发现竟然输出了当前组件的实例对象,里面竟然还有我们在 data 中定义的 list 属性,此时我们明白可以通过 next 方法获取到当前组件的实例,那么这就好办了,我在获取数据之前显示进度条,获取完数据之后,调用一个自己定义的方法将数据赋值给 data 中的 list 属性,再隐藏进度条不就好了。

问题来了,我们怎么搞进度条呢???

哈哈,我们就不自己写了,使用一下第三方库吧,这里推荐给小伙伴的是 Nprogress ,如何使用呢?

我们去 npm 上看看文档有没有介绍

image-20220515210412364

发现周下载量 74万多,还是很多人用的诶,我划线的部分什么意思呢,我来解释下

  • 通过 npm install --save nprogress 安装
  • NProgress.start() 意思是显示进度条
  • NProgress.done() 意思是隐藏进度条

既然了解了,我们就来敲代码吧,实现我们要的效果。

代码

修改 Hot.vue 、Local.vue、Money.vue 组件中的代码,如下

<template>
  <div class="hot-container">
    <!-- 文章列表组件 -->
    <ArticleList
      :article="List"
      @del="delArticle"
      ref="ArticleList"
    ></ArticleList>
  </div>
</template>

<script>
// 引入文章列表 ArticleList 组件
import ArticleList from '../components/ArticleList.vue'
// 引入封装好的 获取文章列表接口、删除文章接口
import { getArticleListAPI, delArticleAPI } from '@/api'
// 引入 nprogress 模块,用于展示进度条
import NProgress from 'nprogress'

export default {
    name: 'HotView',
    // 注册组件
    components: {
        ArticleList,
        Loading
    },
    data () {
        return {
            // 热点文章列表
            List: [],
            // 控制是否展示 loading
            isLoading: false
        }
    },
    // 路由导航完成前获取数据
    async beforeRouteEnter (to, from, next) {
        // 显示进度条
        NProgress.start()
        // 请求数据
        const res = await getArticleListAPI(1)
        // 数据获取成功调用 next() 方法, 传一个回调方法可以访问组件实例,组件实例为回调方法的参数
        next((vm) => vm.setData(res.data.data))
    },
    methods: {
        // 为数据赋值
        setData (post) {
            // 为热点文章列表赋值
            this.List = post
            // 隐藏进度条
            NProgress.done()
        }
    }
}
</script>

<style lang="less" scoped>
.hot-container {
  cursor: pointer;
}
</style>

效果

GIF 2022-5-15 21-09-21

进度条成功的显示出来了,是不是很 Nice。

总结

今天给小伙伴分享了以下知识

  • 如何给所有路由设置一样的过渡效果
  • 如何在路由导航获取数据之前加进度条效果
  • 如何在路由导航获取数据之后加 loading 效果

看完此篇我相信小伙伴一定解决了以上疑问,同时也感谢你的阅读,如果对你有所帮助,那我会非常开心,帮助了你也是对我这篇文章的一种认可,我们下次再见吧。