Skip to content

Vue-Router

路由定义得越早,优先级就越高。

动态路由匹配

  • 更自由的设计路由的表现
  • 我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果:
  • $route.query
  • $route.hash
  • $route.params
模式匹配路径$route.params
/user/:username/user/evanusername: 'evan'
/user/username/post/post_id/user/evan/post/123username: 'evan', post_id: '123'
JavaScript
const User = {
  template: '<div>User</div>'
  
  //可以读取/user/1001 和 /user/1002 中的 1001 1002
  template: '<div>User {{ $route.params.id }}</div>'
}

const router = new VueRouter({
  routes: [
    // 动态路径参数 以冒号开头 /user/1001 和 /user/1002 都将映射到相同的路由
    //一个“路径参数”使用冒号 : 标记。当匹配到一个路由时,参数值会被设置到 this.$route.params,可以在每个组件内使用。于是,我们可以更新 User 的模板,输出当前用户的 ID:
    { path: '/user/:id', component: User }
  ]
})

响应路由参数的变化

  • 路由参数变化时,执行动作。可利用导航守卫
  • 从 /user/1001 导航到 /user/1002 ,原来的组件实例会被复用。生命周期钩子不会再被调用
JavaScript
const User = {
  template: '...',
  //复用组件时,想对路由参数的变化作出响应的话,你可以简单地 watch (监测变化) $route 对象
  watch: {
    $route(to, from) {
      // 对路由变化作出响应...   如网络请求,本地数据读取等
    }
  },
  //复用组件时,导航守卫,便于路由变化时,做出相应的动作
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 'this'
    // 因为当守卫执行前,组件实例还没被创建
  },
  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 'this'
  },
  beforeRouteLeave(to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 'this'
  }
}

捕获所有路由或404路由

  • 如果想匹配任意路径,我们可以使用通配符 * ,含有通配符的路由应该放在最后。
JavaScript
{
  // 会匹配所有路径
  path: '*'
}
{
  // 会匹配以 `/user-` 开头的任意路径
  path: '/user-*'
}

// 给出一个路由 { path: '/user-*' }
this.$router.push('/user-admin')
this.$route.params.pathMatch // 'admin'
// 给出一个路由 { path: '*' }
this.$router.push('/non-existing')
this.$route.params.pathMatch // '/non-existing'

嵌套路由

  • 子界面路由效果,多层深入
  • 渲染组件同样可以包含自己的嵌套 router-view
JavaScript
const User = {
  template: `
    <div class="user">
      <h2>User {{ $route.params.id }}</h2>
      <router-view></router-view>
    </div>
  `
}

const routes  = [
    {
      path: '/user/:id',
      component: User,
      children: [
        // 当 /user/:id 匹配成功,
        // UserHome 会被渲染在 User 的 router-view 中
        { 
          path: '', 
          component: UserHome 
        },
        {
          // 当 /user/:id/profile 匹配成功,
          // UserProfile 会被渲染在 User 的 router-view 中
          path: 'profile',
          component: UserProfile
        },
        {
          // 当 /user/:id/posts 匹配成功
          // UserPosts 会被渲染在 User 的 router-view 中
          path: 'posts',
          component: UserPosts
        }
      ]
    }
  ]
const router = new VueRouter({
  routes
})

编程式的导航

  • 用代码,跳转路由

router.push(location, onComplete?, onAbort?)

  • Vue 实例内部,你可以通过 $router 访问路由实例。因此你可以调用 this. $router.push
  • 这个方法会向 history 栈添加一个新的记录。所以可以使用后退返回上一个页面。
  • 点击 < router-link :to="..."> 等同于调用 router.push(...)
JavaScript
// 字符串
router.push('home')
// 对象
router.push({ path: 'home' })
// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})
// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})
/*
如果提供了 path,params 会被忽略,上述例子中的 query 并不属于这种情况。取而代之的是下面例子的做法,你需要提供路由的 name 或手写完整的带有参数的 path
*/
const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123
// 这里的 params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user
/*
如果目的地和当前路由相同,只有参数发生了改变 (比如从一个用户资料到另一个 /users/1 -> /users/2),你需要使用 beforeRouteUpdate[导航守卫] 来响应这个变化 (比如抓取用户信息)
*/

router.replace(location, onComplete?, onAbort?)

  • Vue 实例内部,你可以通过 $router 访问路由实例。因此你可以调用 this. $router.replace
  • 这个方法不会向 history 栈添加一个新的记录,而是直接替换掉当前的history记录。所以可以无法使用后退返回上一个页面。
  • 点击 < router-link :to="..." replace> 等同于调用 router.replace(...)
JavaScript
用法和 router.push 同

router.go(n)

  • 这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)。
JavaScript
// 在浏览器记录中前进一步,等同于 history.forward()
router.go(1)

// 后退一步记录,等同于 history.back()
router.go(-1)

// 前进 3 步记录
router.go(3)

// 如果 history 记录不够用,那就默默地失败呗
router.go(-100)
router.go(100)

命名路由

  • 设置别名,便于代码编写
  • 在 routes 配置中给某个路由设置名称
JavaScript
const router = new VueRouter({
  routes: [
    {
      path: '/user/:userId',
      name: 'user',
      component: User
    }
  ]
})

//通过名称去对应的path 等于前往  /user/123
router.push({ name: 'user', params: { userId: 123 } })

命名视图

  • 同级别展示多个带router的界面。
  • 展示多个同级视图。例如有 sidebar (侧导航) 和 main (主内容) ,你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。
  • 如果 router-view 没有设置名字,那么默认为 default。
html
<router-view class="top"></router-view>
<router-view class="left" name="a"></router-view>
<router-view class="right" name="b"></router-view>
JavaScript
//同个路由,多个视图就需要多个组件。注意是components不是component
const router = new VueRouter({
  routes: [
    {
      path: '/',
      components: {
        default: Foo,
        a: Bar,
        b: Baz
      }
    }
  ]
})

嵌套命名视图

  • Nav 只是一个常规组件。
  • UserSettings 是一个视图组件。
  • UserEmailsSubscriptions、UserProfile、UserProfilePreview 是嵌套的视图组件。
html
<!-- UserSettings.vue -->
<div>
  <h1>User Settings</h1>
  <NavBar/>
  <router-view/>
  <router-view name="helper"/>
</div>
JavaScript
//同个路由,多个视图就需要多个组件。注意是components不是component
const router = new VueRouter({
  routes: [
    {
      path: '/settings',
      // 你也可以在顶级路由就配置命名视图
      component: UserSettings,
      children: [{
        path: 'emails',
        component: UserEmailsSubscriptions
      }, {
        path: 'profile',
        components: {
          default: UserProfile,
          helper: UserProfilePreview
        }
      }]
    }
  ]
})

重定向和别名

  • 验证登录,跳转到home,login等

重定向

JavaScript
//访问A时自动重定向到B
const router = new VueRouter({
  routes: [
    { path: '/about', redirect: '/info' }
  ]
})
//通用写法,命名路由
const router = new VueRouter({
  routes: [
    { path: '/a', redirect: { name: 'foo' }}
  ]
})
//动态判断,如根据login-session,重定向到login或home
const router = new VueRouter({
  routes: [
    { path: '/a', redirect: to => {
      // 方法接收 目标路由 作为参数
      // return 重定向的 字符串路径/路径对象
    }}
  ]
})

别名

JavaScript
///a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样
const router = new VueRouter({
  routes: [
    { path: '/a', component: A, alias: '/b' }
  ]
})

路由组件传参

  • 更优雅的传输路由参数
  • 使用 props 将组件和路由解耦
  • 保持props 函数为无状态的,因为它只会在路由发生变化时起作用
  • 需要状态来定义 props,请使用包装组件,这样 Vue 才可以对状态变化做出反应
JavaScript
//取代与 $route 的耦合
const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
  routes: [{ path: '/user/:id', component: User }]
})
//通过 props 解耦
const User = {
  props: ['id'],
  template: '<div>User {{ id }}</div>'
}
//布尔模式。如果 props 被设置为 true,route.params 将会被设置为组件属性
const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User, props: true },

    // 对于包含命名视图的路由,你必须分别为每个命名视图添加 `props` 选项:
    {
      path: '/user/:id',
      components: { default: User, sidebar: Sidebar },
      props: { default: true, sidebar: false }
    }
  ]
})

//对象模式
const router = new VueRouter({
  routes: [
    {
      path: '/promotion/from-newsletter',
      component: Promotion,
      //如果 props 是一个对象,它会被按原样设置为组件属性。当 props 是静态的时候有用。
      props: { newsletterPopup: false }
    }
  ]
})

//函数模式
//你可以创建一个函数返回 props。这样你便可以将参数转换成另一种类型,将静态值与基于路由的值结合等等。
const router = new VueRouter({
  routes: [
    {
      path: '/search',
      component: SearchUser,
      //URL /search?q=vue 会将 {query: 'vue'} 作为属性传递给 SearchUser 组件。
      props: route => ({ query: route.query.q })
    }
  ]
})

History 模式

JS
const router = new VueRouter({
  mode: 'history',
  routes: [...]
})
  • 服务器设置。在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。404的页面由之前的服务器设置转为vue中去进行设置。
  • nginx
location / {
  try_files $uri $uri/ /index.html;
}
  • nodejs
JS
const http = require('http')
const fs = require('fs')
const httpPort = 80

http.createServer((req, res) => {
  fs.readFile('index.html', 'utf-8', (err, content) => {
    if (err) {
      console.log('We cannot open "index.html" file.')
    }

    res.writeHead(200, {
      'Content-Type': 'text/html; charset=utf-8'
    })

    res.end(content)
  })
}).listen(httpPort, () => {
  console.log('Server listening on: http://localhost:%s', httpPort)
})

导航守卫

  • 截获路由变化,全局变化用于鉴权,组件内守卫二次确认未保存内容。

全局前置守卫

JS
router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !isAuthenticated){
    next({ name: 'Login' })
  }
  else {
    next()
  }
})

全局解析守卫

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

全局后置钩子

  • 你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:
JS
router.afterEach((to, from) => {
  // ...
})

路由独享的守卫

JS
//特殊的路由二次鉴权等
const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})

组件内的守卫

JS
const Foo = {
  template: `...`,
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
    // 想访问组件实例的方式
    next(vm => {
      // 通过 `vm` 访问组件实例
    })
  },
  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave(to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
    // 通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消
    const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
    if (answer) {
      next()
    } else {
      next(false)
    }
  }
}

完整的导航解析流程

  • 导航被触发。
  • 在失活的组件里调用 beforeRouteLeave 守卫。
  • 调用全局的 beforeEach 守卫。
  • 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  • 在路由配置里调用 beforeEnter。
  • 解析异步路由组件。
  • 在被激活的组件里调用 beforeRouteEnter。
  • 调用全局的 beforeResolve 守卫 (2.5+)。
  • 导航被确认。
  • 调用全局的 afterEach 钩子。
  • 触发 DOM 更新。
  • 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

路由元信息

  • 结合全局导航守卫,用于鉴权。
JS
const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      children: [
        {
          path: 'bar',
          component: Bar,
          // a meta field
          meta: { requiresAuth: true }
        }
      ]
    }
  ]
})

router.beforeEach((to, from, next) => {
  //matched包含了当前所使用的router信息,对其进行判断是否为true决定是否需要进行登录校验等。
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // 检测是否登录,没有登录,返回登录页面,并把目标页面的链接传递,用于登录后回跳。
    if (!auth.loggedIn()) {
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    } else {
      next()
    }
  } else {
    next() // 确保一定要调用 next()
  }
})

过渡动效

  • 全页面的动画效果
html
<transition>
  <router-view></router-view>
</transition>
JS
//每一个页面使用不同的动效
const Foo = {
  template: `
    <transition name="slide">
      <div class="foo">...</div>
    </transition>
  `
}

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



//基于路由的动效
const User = {
  template: `
    <!-- 使用动态的 transition name -->
    <transition :name="transitionName">
      <router-view></router-view>
    </transition>
  `,
  // 接着在父组件内
  // 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'
    }
  }
}

数据获取

  • 导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示“加载中”之类的指示。
  • 导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。

导航完成后获取数据

  • 先跳转界面,再请求数据。
  • 常在created 钩子中获取数据,展现效果更优,用户点击后可能会看到未渲染的界面。
JS
const Info = {
  template : `<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>`

  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//loading显示
        // 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
          }
        })
      }
    }
  }
}

导航完成前获取数据

  • 在 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 能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。
JS
const router = new VueRouter({
  routes: [...],
  scrollBehavior (to, from, savedPosition) {
    // return 期望滚动到哪个的位置
    //顶部
    return { x: 0, y: 0 }
    //记录的位置
    if (savedPosition) {
      return savedPosition
    } else {
      return { x: 0, y: 0 }
    }
    //滚动到锚点
    if (to.hash) {
      return {
        selector: to.hash
      }
    }

  },

  //异步滚动
  scrollBehavior (to, from, savedPosition) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve({ x: 0, y: 0 })
      }, 500)
    })
  }

  //平滑滚动
  scrollBehavior (to, from, savedPosition) {
    if (to.hash) {
      return {
        selector: to.hash,
        behavior: 'smooth',
      }
    }
  }

})

路由懒加载

  • 所有代码打包,JavaScript 包会变得非常大,影响加载速度。不同路由对应的组件分割成不同的代码块,按需加载,增加访问速度。
JS
//结合 Vue 的异步组件 (opens new window)和 Webpack 的代码分割功能 (opens new window),轻松实现路由组件的懒加载
// 先将异步组件定义为一个返回promise的工厂函数
const Foo = () =>
  Promise.resolve({
    /* 组件定义对象 */
  })
//在router中来定义代码分块点 (split point)
const Foo = () => import('./Foo.vue') // 返回 Promise
const router = new VueRouter({
  routes: [{ path: '/foo', component: Foo }]
})


//把组件按组分块
//把某个路由下的所有组件都打包在同个异步块 (chunk) 中,只需要使用 命名 chunk
//Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')

导航故障

  • 用户留在同一页面上的情况。
  • 用户已经位于他们正在尝试导航到的页面
  • 一个导航守卫通过调用 next(false) 中断了这次导航
  • 一个导航守卫抛出了一个错误,或者调用了 next(new Error())
JS
// 导航故障是一个 Error 实例,附带了一些额外的属性。要检查一个错误是否来自于路由器,可以使用 isNavigationFailure 函数:
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')
  }
})

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


// 所有的导航故障都会有 to 和 from 属性,分别用来表达这次失败的导航的目标位置和当前位置。
// // 正在尝试访问 admin 页面
// router.push('/admin').catch(failure => {
//   if (isNavigationFailure(failure, NavigationFailureType.redirected)) {
//     failure.to.path // '/admin'
//     failure.from.path // '/'
//   }
// })

Last updated:

Author: Tt | 养老院高尚老头 | 张登哲