Vue Router v3.x 路由进阶【路由篇】

文章目录

  • 前言
  • 一、导航守卫💂
    • [1.1 全局前置守卫](#1.1 全局前置守卫)
    • [1.2 全局解析守卫](#1.2 全局解析守卫)
    • [1.3 全局后置钩子](#1.3 全局后置钩子)
    • [1.4 路由独享的守卫](#1.4 路由独享的守卫)
    • [1.5 组件内的守卫](#1.5 组件内的守卫)
    • [1.6 完整的导航解析流程](#1.6 完整的导航解析流程)
  • 二、路由元信息✉️
    • 三、过渡动效
    • [3.1 单个路由的过渡](#3.1 单个路由的过渡)
    • [3.2 基于路由的动态过渡](#3.2 基于路由的动态过渡)
  • 四、数据获取🌐
    • [4.1 导航完成后获取数据](#4.1 导航完成后获取数据)
    • [4.2 在导航完成前获取数据](#4.2 在导航完成前获取数据)
  • 五、滚动行为⚓
  • 六、路由懒加载☕
    • [6.1 把组件按组分块](#6.1 把组件按组分块)
  • 七、导航故障🚫
    • [7.1 检测导航故障](#7.1 检测导航故障)
    • [7.2 NavigationFailureType](#7.2 NavigationFailureType)
    • [7.3 导航故障的属性](#7.3 导航故障的属性)
  • 八、总结💯

前言

Vue RouterVue.js (opens new window)官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。这里为什么要分 v3.xv4.x 两篇来写的,主要还是两者在项目使用中着实有不小的差距。

💯本人在 基于 Vue2 的老项目和基于 Vue3 的新项目上在路由这块确实懵逼过。在 Vue2 项目上安装路由组件的时候经常会安装 v4.x 版本,然后就各种问题。 所以我们先从 v3.x 版本学习,然后再对比 v4.x 版本看看都有哪些差异。


一、导航守卫💂

vue-router 提供的导航守卫主要用来通过 跳转或取消 的方式 守卫导航 。有多种机会植入路由导航过程中:全局的 , 单个路由 独享的, 或者 组件级 的。

记住参数或查询的改变并不会触发 进入/离开 的导航守卫。你可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫。

1.1 全局前置守卫

你可以使用 router.beforeEach 注册一个全局前置守卫:

js 复制代码
const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  // ...
})

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中

每个守卫方法接收三个参数:

  • to: Route: 即将要进入的目标 路由对象

  • from: Route: 当前导航正要离开的路由

  • next : Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。

    • next() : 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。

    • next(false) : 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。

    • next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: truename: 'home' 之类的选项以及任何用在 router-linkto proprouter.push 中的选项。

    • next(error): (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。

确保 next 函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。这里有一个在用户未能验证身份时重定向到 /login 的示例:

js 复制代码
// BAD
router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
  // 如果用户未能验证身份,则 `next` 会被调用两次
  next()
})
js 复制代码
// GOOD
router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
  else next()
})

1.2 全局解析守卫

在 2.5.0+ 你可以用 router.beforeResolve 注册一个** 全局守卫**。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。

1.3 全局后置钩子

你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:

js 复制代码
router.afterEach((to, from) => {
  // ...
})

1.4 路由独享的守卫

你可以在路由配置上直接定义 beforeEnter 守卫:

js 复制代码
const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})

这些守卫与全局前置守卫的方法参数是一样的。

1.5 组件内的守卫

最后,你可以在路由组件内直接定义以下路由导航守卫:

  • beforeRouteEnter
  • beforeRouteUpdate (2.2 新增)
  • beforeRouteLeave
js 复制代码
const Foo = {
  template: `...`,
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
  },
  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave(to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
  }
}

🎯beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。

不过,你可以通过传一个回调给 next 来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。

js 复制代码
beforeRouteEnter (to, from, next) {
  next(vm => {
    // 通过 `vm` 访问组件实例
  })
}

🎯注意: beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdatebeforeRouteLeave 来说,this 已经可用了,所以不支持传递回调,因为没有必要了。

js 复制代码
beforeRouteUpdate (to, from, next) {
  // just use `this`
  this.name = to.params.name
  next()
}

这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。

js 复制代码
beforeRouteLeave (to, from, next) {
  const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
  if (answer) {
    next()
  } else {
    next(false)
  }
}

1.6 完整的导航解析流程

  1. 导航被触发。

  2. 在失活的组件里调用 beforeRouteLeave 守卫。

  3. 调用全局的 beforeEach 守卫。

  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。

  5. 在路由配置里调用 beforeEnter

  6. 解析异步路由组件。

  7. 在被激活的组件里调用 beforeRouteEnter

  8. 调用全局的 beforeResolve 守卫 (2.5+)。

  9. 导航被确认。

  10. 调用全局的 afterEach 钩子。

  11. 触发 DOM 更新。

  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

二、路由元信息✉️

定义路由的时候可以配置 meta 字段:

js 复制代码
const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      children: [
        {
          path: 'bar',
          component: Bar,
          // a meta field
          meta: { requiresAuth: true }
        }
      ]
    }
  ]
})

那么如何访问这个 meta 字段呢?

首先,我们称呼 routes 配置中的每个路由对象为 路由记录。路由记录可以是嵌套的,因此,当一个路由匹配成功后,他可能匹配多个路由记录

例如,根据上面的路由配置,/foo/bar 这个 URL 将会匹配父路由记录以及子路由记录。

一个路由匹配到的所有路由记录会暴露为 $route 对象 (还有在导航守卫中的路由对象) 的 $route.matched 数组。因此,我们需要遍历 $route.matched 来检查路由记录中的 meta 字段。

🌰下面例子展示在全局导航守卫中检查元字段:

js 复制代码
router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // this route requires auth, check if logged in
    // if not, redirect to login page.
    if (!auth.loggedIn()) {
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    } else {
      next()
    }
  } else {
    next() // 确保一定要调用 next()
  }
})

三、过渡动效

<router-view> 是基本的动态组件,所以我们可以用 <transition> 组件给它添加一些过渡效果:

js 复制代码
<transition>
  <router-view></router-view>
</transition>

3.1 单个路由的过渡

上面的用法会给所有路由设置一样的过渡效果,如果你想让每个路由组件有各自的过渡效果,可以在各路由组件内使用 <transition> 并设置不同的 name

js 复制代码
const Foo = {
  template: `
    <transition name="slide">
      <div class="foo">...</div>
    </transition>
  `
}

const Bar = {
  template: `
    <transition name="fade">
      <div class="bar">...</div>
    </transition>
  `
}

3.2 基于路由的动态过渡

还可以基于当前路由与目标路由的变化关系,动态设置过渡效果:

html 复制代码
<!-- 使用动态的 transition name -->
<transition :name="transitionName">
  <router-view></router-view>
</transition>
js 复制代码
// 接着在父组件内
// watch $route 决定使用哪种过渡
watch: {
  '$route' (to, from) {
    const toDepth = to.path.split('/').length
    const fromDepth = from.path.split('/').length
    this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
  }
}

四、数据获取🌐

有时候,进入某个路由后,需要从服务器获取数据。例如,在渲染用户信息时,你需要从服务器获取用户的数据。我们可以通过两种方式来实现:

导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示"加载中"之类的指示。

导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。

从技术角度讲,两种方式都不错 ------ 就看你想要的用户体验是哪种。

4.1 导航完成后获取数据

当你使用这种方式时,我们会马上导航和渲染组件,然后在组件的 created 钩子中获取数据。这让我们有机会在数据获取期间展示一个 loading 状态,还可以在不同视图间展示不同的 loading 状态。

假设我们有一个 Post 组件,需要基于 $route.params.id 获取文章数据:

html 复制代码
<template>
  <div class="post">
    <div v-if="loading" class="loading">
      Loading...
    </div>

    <div v-if="error" class="error">
      {{ error }}
    </div>

    <div v-if="post" class="content">
      <h2>{{ post.title }}</h2>
      <p>{{ post.body }}</p>
    </div>
  </div>
</template>
js 复制代码
export default {
  data () {
    return {
      loading: false,
      post: null,
      error: null
    }
  },
  created () {
    // 组件创建完后获取数据,
    // 此时 data 已经被 observed 了
    this.fetchData()
  },
  watch: {
    // 如果路由有变化,会再次执行该方法
    '$route': 'fetchData'
  },
  methods: {
    fetchData () {
      this.error = this.post = null
      this.loading = true
      // replace getPost with your data fetching util / API wrapper
      getPost(this.$route.params.id, (err, post) => {
        this.loading = false
        if (err) {
          this.error = err.toString()
        } else {
          this.post = post
        }
      })
    }
  }
}

4.2 在导航完成前获取数据

通过这种方式,我们在导航转入新的路由前获取数据。我们可以在接下来的组件的 beforeRouteEnter 守卫中获取数据,当数据获取成功后只调用 next 方法。

js 复制代码
export default {
  data () {
    return {
      post: null,
      error: null
    }
  },
  beforeRouteEnter (to, from, next) {
    getPost(to.params.id, (err, post) => {
      next(vm => vm.setData(err, post))
    })
  },
  // 路由改变前,组件就已经渲染完了
  // 逻辑稍稍不同
  beforeRouteUpdate (to, from, next) {
    this.post = null
    getPost(to.params.id, (err, post) => {
      this.setData(err, post)
      next()
    })
  },
  methods: {
    setData (err, post) {
      if (err) {
        this.error = err.toString()
      } else {
        this.post = post
      }
    }
  }
}

💯在为后面的视图获取数据时,用户会停留在当前的界面,因此建议在数据获取期间,显示一些进度条或者别的指示。如果数据获取失败,同样有必要展示一些全局的错误提醒。

五、滚动行为⚓

使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router 能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。

🎯注意: 这个功能只在支持 history.pushState 的浏览器中可用。

当创建一个 Router 实例,你可以提供一个 scrollBehavior 方法:

js 复制代码
const router = new VueRouter({
  routes: [...],
  scrollBehavior (to, from, savedPosition) {
    // return 期望滚动到哪个的位置
  }
})

scrollBehavior 方法接收 tofrom 路由对象。第三个参数 savedPosition 当且仅当 popstate 导航 (通过浏览器的 前进/后退 按钮触发) 时才可用。

这个方法返回滚动位置的对象信息,长这样:

  • { x: number, y: number }
  • { selector: string, offset? : { x: number, y: number }} (offset 只在 2.6.0+ 支持)

如果返回一个 falsy (译者注:falsy 不是 false,参考这里 (opens new window))的值,或者是一个空对象,那么不会发生滚动。

🌰举例:

js 复制代码
scrollBehavior (to, from, savedPosition) {
  return { x: 0, y: 0 }
}

对于所有路由导航,简单地让页面滚动到顶部。

返回 savedPosition,在按下 后退/前进 按钮时,就会像浏览器的原生表现那样:

js 复制代码
scrollBehavior (to, from, savedPosition) {
  if (savedPosition) {
    return savedPosition
  } else {
    return { x: 0, y: 0 }
  }
}

如果你要模拟 "滚动到锚点" 的行为:

js 复制代码
scrollBehavior (to, from, savedPosition) {
  if (to.hash) {
    return {
      selector: to.hash
    }
  }
}

我们还可以利用 路由元信息 更细颗粒度地控制滚动。

5.1 异步滚动

你也可以返回一个 Promise 来得出预期的位置描述:

js 复制代码
scrollBehavior (to, from, savedPosition) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ x: 0, y: 0 })
    }, 500)
  })
}

将其挂载到从页面级别的过渡组件的事件上,令其滚动行为和页面过渡一起良好运行是可能的。但是考虑到用例的多样性和复杂性,我们仅提供这个原始的接口,以支持不同用户场景的具体实现。

5.2平滑滚动

只需将 behavior 选项添加到 scrollBehavior 内部返回的对象中,就可以为支持它的浏览器 (opens new window)启用原生平滑滚动:

js 复制代码
scrollBehavior (to, from, savedPosition) {
  if (to.hash) {
    return {
      selector: to.hash,
      behavior: 'smooth',
    }
  }
}

六、路由懒加载☕

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

结合 Vue 的异步组件 (opens new window)和 Webpack 的代码分割功能 (opens new window),轻松实现路由组件的懒加载。

首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):

js 复制代码
const Foo = () =>
  Promise.resolve({
    /* 组件定义对象 */
  })

第二,在 Webpack 2 中,我们可以使用动态 import (opens new window)语法来定义代码分块点 (split point):

js 复制代码
import('./Foo.vue') // 返回 Promise

🎯注意:

如果您使用的是 Babel ,你将需要添加 syntax-dynamic-import (opens new window)插件,才能使 Babel 可以正确地解析语法。

结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件。

js 复制代码
const Foo = () => import('./Foo.vue')

在路由配置中什么都不需要改变,只需要像往常一样使用 Foo:

js 复制代码
const router = new VueRouter({
  routes: [{ path: '/foo', component: Foo }]
})

6.1 把组件按组分块

有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk ) 中。只需要使用 命名 chunk (opens new window),一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)。

js 复制代码
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')

Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。

七、导航故障🚫

当使用 router-link 组件时,Vue Router 会自动调用 router.push 来触发一次导航。 虽然大多数链接的预期行为是将用户导航到一个新页面,但也有少数情况下用户将留在同一页面上:

  • 用户已经位于他们正在尝试导航到的页面
  • 一个导航守卫通过调用 next(false) 中断了这次导航
  • 一个导航守卫抛出了一个错误,或者调用了 next(new Error())

当使用 router-link 组件时,这些失败都不会打印出错误。然而,如果你使用 router.push 或者 router.replace 的话,可能会在控制台看到一条 "Uncaught (in promise) Error" 这样的错误,后面跟着一条更具体的消息。让我们来了解一下如何区分导航故障。

💯背景故事:

在 v3.2.0 中,可以通过使用 router.push 的两个可选的回调函数:onComplete 和 onAbort 来暴露导航故障。从版本 3.1.0 开始,router.push 和 router.replace 在没有提供 onComplete/onAbort 回调的情况下会返回一个 Promise。这个 Promise 的 resolve 和 reject 将分别用来代替 onComplete 和 onAbort 的调用。

7.1 检测导航故障

导航故障是一个 Error 实例,附带了一些额外的属性。要检查一个错误是否来自于路由器,可以使用 isNavigationFailure 函数:

js 复制代码
import VueRouter from 'vue-router'
const { isNavigationFailure, NavigationFailureType } = VueRouter

// 正在尝试访问 admin 页面
router.push('/admin').catch(failure => {
  if (isNavigationFailure(failure, NavigationFailureType.redirected)) {
    // 向用户显示一个小通知
    showToast('Login in order to access the admin panel')
  }
})

🎗️提示:如果你忽略第二个参数:isNavigationFailure(failure),那么就只会检查这个错误是不是一个导航故障。

NavigationFailureType 可以帮助开发者来区分不同类型的导航故障。有四种不同的类型:

  • redirected:在导航守卫中调用了 next(newLocation) 重定向到了其他地方。
  • aborted:在导航守卫中调用了 next(false) 中断了本次导航。
  • cancelled:在当前导航还没有完成之前又有了一个新的导航。比如,在等待导航守卫的过程中又调用了 router.push。
  • duplicated:导航被阻止,因为我们已经在目标位置了。

7.3 导航故障的属性

所有的导航故障都会有 tofrom 属性,分别用来表达这次失败的导航的目标位置和当前位置。

js 复制代码
// 正在尝试访问 admin 页面
router.push('/admin').catch(failure => {
  if (isNavigationFailure(failure, NavigationFailureType.redirected)) {
    failure.to.path // '/admin'
    failure.from.path // '/'
  }
})

在所有情况下,tofrom 都是规范化的路由位置。

八、总结💯

路由真的很重要,这么说吧,路由不学好你是不可能做好一个像样的前端框架的。


相关推荐
鱼大大博客1 小时前
Edge Scdn的应用场景有哪些?
前端·edge
两只鱼丿1 小时前
Edge安装问题,安装后出现:Could not find Edge installation
前端·edge
screct_demo4 小时前
通俗易懂的讲一下Vue的双向绑定和React的单向绑定
前端·javascript·html
有心还是可以做到的嘛4 小时前
ref() 和 reactive() 区别
前端·javascript·vue.js
xcLeigh7 小时前
HTML5实现好看的博客网站、通用大作业网页模板源码
前端·课程设计·html5
mit6.8247 小时前
[Qt] 输入控件 | Line | Text | Combo | Spin | Date | Dial | Slider
前端·qt·学习·ubuntu
狗狗显卡8 小时前
一些计算机零碎知识随写(25年1月)
前端
Burt8 小时前
【unibest】可以去掉hbx模版了,base模板一统天下
前端·微信小程序·uni-app
拖孩8 小时前
💥大家好,我是拖孩🎤
前端·javascript·后端
CHANG_THE_WORLD8 小时前
Linux 基础七 内存
linux·服务器·前端