Vue-Router的使用

1,149 阅读7分钟

安装vue-router

//使用npm或yarn安装
npm i vue-router
yarn add vue-router

使用

需要显式的对vue-router组件进行注册,否则router-link组件和router-view组件无法使用

import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);

最基本的路由表的配置

import VueRouter from 'vue-router';

import Home from '../components/Home.vue';
import User from '../components/User.vue';

// 假设存在这两个Vue单文件组件,通过import导入,并映射到每个路由的对应组件中
const routes = [
    {path:"/home",component:Home},
    {path:"/user",component:User}
]

// 创建router实例
const router = new VueRouter({
    routes
})

// 在根实例中通过构造选项传入router注入,使整个应用都可以访问$router和$route
const app = new Vue({
    router,
    render:h=>h(App)
}).$mount('#app');

// 匹配到的路由对应组件会替换渲染到router-view中
// 在当前挂载html中编写router-view标签
// <router-view></router-view>

动态路由匹配

当需要某一规则下的路由,都映射到同一个组件时,可以使用动态路由。如有一个表单详情页,不同表单ID对应的组件都是同一个,但是ID并不同

// FormDetail假设存在
// 动态路径参数需要以:开头
const routes = [
    { path:"/formDetail/:formId",component:FormDetail }
]

此时不管/formDetail/jsadklfjlksadjflsda还是/formDetail/12312321都会映射到同一个路由,并渲染同一个组件,相当于formId会被对应的字符替换。一个路由中可以设置多个动态参数path:"/formDetail/:formId/:value"。通过this.$route.params可以获取动态参数对象。

// 假设路由现在时/formDetail/1/2
// this.$route.params获取到的对象即为
{
    formId:1,
    value:2
}

当使用路由参数时,从/formDetail/a跳转到/formDetail/b时,组件实例会被复用,两个路由公用一个组件效率更高,但也意味着组件实例的生命周期不会被二次调用,如果需要在参数变更时,做出对应的操作,需要对路由对象进行监听,或者使用vue-router提供的导航守卫。

watch监听$route对象

export default {
    watch:{
        $route(to,from){
            // to为跳转目标路由信息
            // from为当前路由信息
            // do something
        }
    }
}

匹配通配符路径

{
  // 会匹配所有路径,但是应该放至最后,在前面所有路由都没有匹配到的情况下,将*映射到404组件是常规用法
  path: '*',
  component:NotFound
},
// 也可以和字符串拼接匹配
{
  // 会匹配以 `/form-` 开头的任意路径
  path: '/form-*'
}

// 使用通配符后,$route.params对象中会添加一个pathMatch字段,值为通配符匹配到的字符串
// 匹配规则path: '/form-*',路径为/from-test,pathMatch值为test
// 匹配规则path: '*',路径为/a/b/c,pathMatch值为/a/b/c
匹配优先级:同一个路径可以匹配多个路由,此时,匹配的优先级就按照路由的定义顺序:路由定义得越早,优先级就越高。

嵌套路由

可以理解为A包含着B,B包含着C路由的关系

const router = new VueRouter({
  routes: [
    {
      path: '/',
      component: Layout, // 根路径会匹配到布局组件,在布局组件的特定位置编写<router-view/>组件,children字段中的路由匹配到时,会渲染在Layout组件自己的router-view组件中
      children: [
        {
          // 当 /home 匹配成功,
          // Home会被渲染在Layout的 <router-view> 中
          // 不需要写/,会与父路由拼接,加了/的嵌套路由就会被当做根路径
          path: 'home',
          component: Home
        }
      ]
    }
  ]
})

// 在没有匹配到嵌套子路由时,Layout的router-view输出为空,如果想在匹配不到时,渲染对应内容,可以添加一个嵌套空子路由
const router = new VueRouter({
  routes: [
    {
      path: '/',
      component: Layout, // 根路径会匹配到布局组件,在布局组件的特定位置编写<router-view/>组件,children字段中的路由匹配到时,会渲染在Layout组件自己的router-view组件中
      children: [
        {
          // 当 /home 匹配成功,
          // Home会被渲染在Layout的 <router-view> 中
          // 不需要写/,会与父路由拼接,加了/的嵌套路由就会被当做根路径
          path: 'home',
          component: Home
        },
        {
            path:'',
            // 当路径为/时,Layout的router-view都会输出Test组件
            component:Test
        }
      ]
    }
  ]
})

不同的导航方式

使用vue-router提供的router-link组件

<router-link to="/foo">Go to Foo</router-link>
// router-link最终会输出为一个a标签,传入to指定链接目标
// 在点击a标签时,本质也是使用编程式导航进行跳转
// 可以理解为调用的是this.$router.push(to)

router-link的使用

  1. 传入to属性,用于指定目标地址
  2. 默认会渲染成带正确预期的a标签,但是可以通过传入tag属性生成其他标签,类型为字符串。(会在vue-router4.x中删除)
  3. 如果目标路由被激活时,router-link会自动添加一个代表激活的样式。
  4. 传入active-class和exact-active-class属性,可以在激活时添加到class属性中,exact前缀的只有在精准匹配时会被激活css。
  5. 传入replace属性,类型为boolean,设置为true后,会调用replace,而不是push。
  6. 传入append属性,类型为boolean,设置为true后,会添加前缀路径,当前路径为/home,目标路径为room/bed,最终跳转路径为/home/room/bed
  7. 传入exact,类型为boolean,设置为true后,代表精准匹配,如果/路径设置后,/home便不会再被激活,只有路径为/时会激活
  8. 传入event,类型为字符串或者字符串数组,代表可以执行导航的事件,如click,dblclick。(会在vue-router4.x中删除)
  9. 结合v-slot插槽,如果不传入一个单独的子元素标签,默认会将子元素额外包括一层span。v-slot提供一个对象,对象中有各种可供使用的属性值。
href:解析后的 URL。将会作为一个 a 元素的 href attribute。
route:解析后的规范化的地址。
navigate:触发导航的函数。会在必要时自动阻止事件,和 router-link 同理。
isActive:如果需要应用激活的 class 则为 true。允许应用一个任意的 class。
isExactActive:如果需要应用精确激活的 class 则为 true。允许应用一个任意的 class
// 结合代码编写对应案例,to属性的使用参考编程式导航的内容

// tag属性,此时dom树中输出的便是div标签包裹着home文字
<router-link tag="div" to="/home">home</router-link>

// 如果当前路径为/home,则标签上会添加对应的class
<router-link active-class="my-active" to="/home">home</router-link>

// 如果当前路径为/精准匹配后,则标签上会添加对应的class,需要与exact属性配合使用
<router-link exact exact-active-class="my-exact-active" to="/">home</router-link>

// 路由跳转后,可以点击后退按钮观察页面历史记录变化,跳转前的历史记录已被替换,将前前历史,前历史,和当前比作123的话,不加replace跳转后是123,加了后便是13,2被3替代了
<router-link replace to="/home">home</router-link>

// 原路径是/test的话,则跳转后路径为/test/home
<router-link append to="home">home</router-link>

// 此时单击标签并不会执行跳转,因为字符串数组以替换掉默认值,双击才会执行路由跳转
// 加上custom属性可消除控制台中,在vue-router4.x中默认包裹为a标签的警告
// route对象提供了上述多个属性,可根据需求自定义实现
<router-link custom to="/home" v-slot="route">
  <span @click="route.navigate">{{ "使用插槽了" }}</span>
</router-link>

使用编程式导航 router.push,router.replace

在Vue实例中,可以通过实例.router访问路由器,router访问路由器,router指向路由器的实例,可以调用this.$router.push(...)进行跳转,router.replace与push类似,唯一区别是不会向history添加新的记录,而是替换掉当前的历史记录,该方法的参数可以是一个字符串路径,或者一个描述地址的对象。例如:

// 字符串
router.push('home')

// 对象
router.push({ path: 'home' })

// 命名的路由,变成路由表中name为user的路由,其中存在动态参数userId会替换为'123'
router.push({ name: 'user', params: { userId: '123' }})

// 带查询参数,query对象的键值对会被拼接在?后,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})

当提供了path,则params会被忽略,可以选择命名路由+params,或者手写完整的带动态参数的path

const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123,此处使用了命名路由
router.push({ path: `/user/${userId}` }) // -> /user/123,手写完整的path
// 这里的 params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user

router-link 组件的 to 属性遵循相同的规则

router.push 或 router.replace 中提供 onComplete 和 onAbort 回调作为第二个和第三个参数。onComplete回调将会在导航成功完成 (在所有的异步钩子被解析之后)调用。 onAbort在终止 (导航到相同的路由、或在当前导航完成之前导航到另一个不同的路由) 的时候调用。

也可以使用a标签去进行路由跳转

<a href="#/home"></a>
// 使用hash模式时需要添加锚点前缀

导航执行前进及后退,go back forward方法

this.$router.back();
// 相当于点击浏览器的后退按钮

this.$router.forward();
// 相当于点击浏览器的前进按钮

// router.go接收一个数字形参,用于表示前进或后退X步,后退为负数,如果超出边界则失败
this.$router.go(1) // 等价this.$router.forward();
this.$router.go(-1) // 等价this.$router.back();
this.$router.go(999) // 如果不存在第999步,则失败

命名路由,顾名思义就是给路由加一个名字而已

const router = new VueRouter({
    routes:[
        {
            path:"/",
            name:'myLayout',
            component:Layout
        }
    ]
})

// 在后续的使用中,router-link和编程式导航,都可以使用name来指定对应路由信息
<router-link :to="{name:'myLayout'}">toLayout</router-link>

this.$router.push({name:'myLayout'}) //replace同理

处理路由重复跳转的报错

相同的路由重复跳转,会被捕获异常并抛出。此报错不会影响任何路由行为,仅仅是捕获到了异常并抛出,如果不想抛出异常,可以对VueRouter原型上的方法进行略微修改

const { push , replace } = VueRouter.prototype
// 先获取对象原型上方法的引用地址

VueRouter.prototype.push = function(location){
    // 将被捕获并抛出的异常二次捕获,并且什么都不处理,控制台就不会再输出异常信息,replace同理
    return push.call(this,location).catch(err => err)
}

VueRouter.prototype.replace = function(location){
    return push.call(this,location).catch(err => err)
}

命名视图

页面的路由视图,有时候不是只有一个,当需要在同级展示多个视图,就可以使用命名视图,router-view 没有设置名字时,那么默认为 default。

// 路由表的代码层面上,不再是component对应一个组件,需要改为components,值为对象
const router = new VueRouter({
  routes: [
    {
      path: '/',
      components: {
        // 在根路径应该输出3个组件
        default: Header,
        sider: Sider,
        main: Main
      }
    }
  ]
})

// 以App.vue页面举例
<template>
  <div id="app">
    <div class="container">
      <router-view></router-view> // Header组件会渲染在无命名视图
      <router-view name="sider"></router-view> // 渲染Sider组件
      <router-view name="main"></router-view> // 渲染Main组件
    </div>
  </div>
</template>

命名视图在嵌套路由中也可以使用,只需要在父级组件中设置命名视图,并在路由配置表中完成相对应的components对象配置即可

重定向与别名

重定向

重定向的意义在于,原本要跳转/a页面,但是强行使其跳转到/b页面,这个过程行为称之为重定向

const router = new VueRouter({
  routes: [
    { 
        path: '/', 
        component:Layout,
        // 访问/时,只会渲染Layout组件,组件中的router-view无渲染,通过重定向跳转至/home,Layout中的router-view会渲染出Home组件
        redirect:'/home',
        // 重定向的值也可以是一个命名路由或方法
        
        // 命名路由对象
        // redirect:{
        //    name:'home'
        // }
        
        // 方法,接收路由信息为参数,返回路径字符串或对象
        // redirect: to => {
        //   return '/home' // 与命名路由效果相同
        //   return { name: 'home' }
        // },
        
        children:[
            {
                name:'home',
                path:'home',
                component:Home
            }
        ]
    }
  ]
})

需要注意的时,路由守卫不会作用在被跳转的路由上,而会作用于目标路由

别名

别名的意义在于,可以将组件映射到任意URL

const router = new VueRouter({
  routes: [
    { path: '/home', component: Home, alias: '/myBigHome' }
  ]
})
// 此时访问/myBigHome等价于访问/home,组件Home会渲染,并且URL为/myBigHome

路由组件router-view传参,有布尔,对象,函数模式

首先对应组件需要接收对应的props,然后在路由配置表中即可进行props传参

// Home组件中定义props
props:['id']

// 路由配置中使用Boolean模式,会将$route.params.id设置到组件的props的id
const routes = [{
  path: '/home/:id',
  component: Home,
  props: true
}]

// 使用对象的方式,Home组件中props的id被设置为123
const routes = [{
  path: '/home',
  component: Home,
  props: {id:123}
}]

// 使用函数模式,会将当前的路由参数传入,最后返回一个对象
const routes = [{
  path: '/home/:id',
  component: Home,
  props: route => {
    return { id: route.params.id }
  }
}]

路由守卫

路由守卫有分全局的,路由独享的,组件内定义的

全局路由守卫

// 在new VueRouter(...)后会得到路由器实例
const router = new VueRouter({
    routes:[
        {
            path:"/home",
            component:Home
        }
    ]
})

// beforeEach接收一个回调函数,函数接收的三个参数分别为,to(即将进入的目标路由对象),from(当前要离开的路由对象),next(是一个函数,相当于执行或放行的作用,可以空参调用,也可以传入对应参数)
router.beforeEach((to,from,next)=>{
    // 代码中可以有多个执行next的逻辑,但next必须被调用一次
    console.log("每次路由跳转都会执行这段话");
    next();
})

// router.beforeResolve和beforeEach类似,区别仅仅是调用时机,在组件被解析后并确认前执行

// 全局后置守卫,不接收next也不会改变导航事件
router.afterEach((to, from) => {
  // ...
  console.log("路由进入后会执行这段话")
})

路由独享守卫

在routes表直接定义beforeEnter钩子,使用方式与全局路由守卫一致

const router = new VueRouter({
    routes:[
        {
            path:"/home",
            component:Home,
            beforeEnter(to,from,next){
                console.log('每次进入Home组件都会执行这段话');
            }
        }
    ]
})

组件中的路由守卫

使用方式类似于生命周期,有3种钩子可供守卫调用,beforeRouteEnter,beforeRouteUpdate,beforeRouteLeave

// beforeRouteEnter,创建对应路由组件前,此时不能获取组件的实例this,执行该钩子时,组件实例并未创建在内存中
// 但是可以通过给next传入回调函数获取当前实例对象,但执行时机是在created后
export default {
    name:"TestComponent",
    data(){
        return {}
    },
    beforeRouteEnter(to,from,next){
        console.log("组件进入前会执行");
        next((vm)=>{
            console.log(vm);
            console.log('但是该回调会在created生命周期后调用')
        });
    }
}

// beforeRouteUpdate,在当前路由改变,但是组件被复用时调用,场景参考动态路由参数页面跳转,此钩子可以访问this实例
export default {
    name:"TestComponent",
    data(){
        return {}
    },
    beforeRouteUpdate(to,from,next){
        console.log("组件被复用时会执行");
        next();
    }
}

// beforeRouteLeave,在当前路由离开组件时调用,此钩子可以访问this实例
export default {
    name:"TestComponent",
    data(){
        return {}
    },
    beforeRouteUpdate(to,from,next){
        console.log("路由离开该组件时会执行");
        next();
    }
}

完整的导航解析流程

  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属性对象配置一些定制化信息字段
访问meta字段
在当前组件中获取到的$route对象中,有一个meta属性,存的对象中有路由表中定义的meta字段,可直接访问。同时还有一个matched对象,里面存着所有路由表中匹配到的路由信息(当前路由及父路由),遍历这个数组对象亦可访问到对应信息

export default{
    created(){
        console.log(this.$route.meta)
        // meta中会有对应的属性名和字段
    },
    mounted(){
        // matched是匹配到的所有路由数组
        this.$route.matched.map((route) => {
            console.log(route.meta);
        });    
    }
}

路由懒加载

所谓的懒加载就是需要时再调用,不使用懒加载时,代码即使没有被调用和执行都会被webpack打包至同一个代码块下,使用懒加载后,可以打开控制台观察Network,只有在访问/home时,对应的js代码才会被加载

const router = new VueRouter({
    routes:[
        {
            path:"/home",
            component: () => import('./components/Home.vue')
            // 原本引入的组件变为一个函数,返回使用动态import的组件,即为路由懒加载
        }
    ]
})

// 把组件按组分块,使用命名chunk,添加指定格式注释即可,此时观察Network,加载的js文件名便为my-chunk.js
const router = new VueRouter({
    routes:[
        {
            path:"/home",
            component: () => import(/* webpackChunkName: "my-chunk" */'./components/Home.vue')
        },
        {
            path:"/user",
            component: () => import(/* webpackChunkName: "my-chunk" */'./components/User.vue')
        }
    ]
})