前言

在使用前端框架之前链接其他页面我们可以使用<a>标签来实现,但是使用了vue等框架后,我们的源码都是各种.vue文件,自然无法再使用<a>标签进行跳转,如果要想使用<a>标签实现则必须知道编译后的路径+名称,这种虽然能做到但是太过费事。为此Vue官方为我们开发者提供了vue-router来实现页面跳转,它的作用就跟<a>标签类似。

安装

CDN使用

https://unpkg.com/vue-router@4

npm安装

npm i vue-router@4

一般在使用vue-cli创建项目时勾选vue-router自动会生成示例代码.

快速开始

需要用到路由的页面

<template>
  <router-link to="/"/>
  <!-- 对于命名路由还可以通过名称跳转 -->
  <router-link to="{name: 'home'}"/>
  <router-link to="/hello"/>
  
  <!-- 通过此标签展示跳转后的页面内容 -->
  <router-view/>
</template>

对路由进行配置的JavaScript

import {createRouter, createWebHistory} from 'vue-router';
import HelloWorld from "@/components/HelloWorld";

const routes = [
    {
        name: 'Home', // 设置了name属性的路由被称为命名路由
        path: '/',
        // 路由懒加载(推荐)
        component: () => {
            import('@/components/Home')
        }
    },
    {
        name: 'HelloWorld',
        path: 'hello',
        // 常规加载
        component: HelloWorld
    }
];

const router = createRouter({
    history: createWebHistory(process.env.BASE_URL),
    routes,
})


export default router;

把上面这个配置在vue对象中用上.

import { createApp } from 'vue'
import App from './App.vue'
// @是src的别名
import router from "@/router";

const app =  createApp(App)
app.use(router)
app.mount('#app')

基础

路由参数获取

可以使用$route.query或者$route.params来获取。

  • $route.query中存放的是URL中的参数(/api/user?userId=123)
  • $route.params中存放的是URL中的路径参数(/api/user/123123即为路径参数)

路由跳转

此处的路由跳转指的是通过JavaScript的方式实现跳转。

路由跳转可以使用$router.push()函数实现,$router中还提供了其他的实用函数,例如:$router.go()$router.replace()等。

以下几个API就是模仿的window.history的API,用的时候不会有太多陌生感。

  • $router.push({path: '', query: {}})
  • $router.go(1)在历史记录中前进,与$router.forward()等价
  • $router.go(-1)在历史记录中后退,与$router.back()等价
  • $router.push({path: '/home', replace: true})替换当前位置(不会记录在历史记录中),它等价于$router.replace({path: '/home'})
// !!! 需要注意的是
$router.push({path: '', params: {}})
// 这种情况下params会被忽略,它们两个不能用在一起。可以使用下面两种替代

// 方式1
$router.push({name: 'home', params: {user: 123}})
// 方式2
$router.push({path: `/home/${user}`})

动态路由

SpringWebMVC中有@RequestMapping支持路径参数、正则语法,vue-router中支持这样的使用。

const routes = [
  // 动态段以冒号开始
  { path: '/users/:id', component: UserDemo },
]

这个路由能匹配到/users/abc/users/1234等,其中abc1234可以通过$route.params.id获取到。一个路由上还可以设置多个路径参数。

路由匹配路径$route.params
/users/:id/user/abc{id:abc}
/users/:id/:type/user/abc/1234{id:abc,type:1234}

使用带有参数的路由时需要注意的是,当用户从/users/johnny导航到/users/jolyne时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用

404路由

const routes = [
  // 将匹配所有内容并将其放在$route.params.pathMatch下
  { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }
]

高级匹配模式

一般使用静态路由/abc和动态路由/:abc就够我们用了,但vue-router还为我们提供了正则匹配模式。

自定义正则

const routes = [
  // /:orderId -> 仅匹配数字
  { path: '/:orderId(\\d+)' },
  // /:productName -> 匹配其他任何内容
  { path: '/:productName' },
]

可重复参数

如果要匹配多级路由比如/abc/1234/xyz可以使用*(将参数重复0次或多次)和+(将参数重复1次或多次)。

const routes = [
  // /:chapters ->  匹配 /one, /one/two, /one/two/three, 等
  { path: '/:chapters+' },
  // /:chapters -> 匹配 /, /one, /one/two, /one/two/three, 等
  { path: '/:chapters*' },

  // 仅匹配数字
  // 匹配 /1, /1/2, 等
  { path: '/:chapters(\\d+)+' },
  // 匹配 /, /1, /1/2, 等
  { path: '/:chapters(\\d+)*' },
]

此时如果使用$router.push进行跳转需要传入数组类型参数方可。$router.push({name: 'home', params: {chapters: ['one', 'two']}})

可选参数

如果要想让一个参数可传可不传则可以使用?(0次或1次)。

const routes = [
  // 匹配 /users 和 /users/posva
  { path: '/users/:userId?' },
  // 匹配 /users 和 /users/42
  { path: '/users/:userId(\\d+)?' },
]

*号虽然也可以做到,但它与?的区别是?参数只能用0或者1次,而*则还可以让参数用多次。

嵌套路由

有些项目中会存在一些嵌套组件,比如用户页面是由个人信息、设置、收藏等组件组成的,而这些组件假设也有对应的路由,并且点击后是在用户这个组件中展示(类似于手风琴式的UI),这种路由就属于嵌套路由。

对于嵌套路由vue-router提供了children属性来配置:

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,
      },
    ],
  },
]
<!-- User Component -->
<template>
  ...

  <router-view/>
</template>

命名视图

如果想同时展示多个视图,并且不是嵌套展示,这时候就需要命名视图了。
router-view默认的name属性值为default,多个视图就可以用多个router-view并设置上name属性(必须否则将会展示同样的内容)。

<router-view class="view left-sidebar" name="LeftSidebar"></router-view>
<router-view class="view main-content"></router-view>
<router-view class="view right-sidebar" name="RightSidebar"></router-view>
const routes = [
    {
      path: '/',
      components: {
        default: Home,
        // LeftSidebar: LeftSidebar 的缩写
        LeftSidebar,
        // 它们与 `<router-view>` 上的 `name` 属性匹配
        RightSidebar,
      },
    },
  ],

嵌套命名视图同理,在需要嵌套的组件中使用router-view组件,并在嵌套路由中使用命名视图即可。

重定向和别名

重定向

在使用redirect时可以不用写component,因为它是直接通过重定向后的配置访问的组件,这里可以不配置,除非是嵌套路由

// path的方式
const router = [{path: "/abc", redirect: "/xyz"}]

// name的方式
const router = [{path: "/abc", redirect: {name: "demo"}}]

//方法的形式
const router = [{
    path: "/abc/:s", redirect: to => {
        // to和$route是一样的对象类型
        return {path: "/s", query: {q: to.params.s}}
    }
}]

别名

重定向是指当用户访问/home时,URL 会被/替换,然后匹配成/。那么什么是别名呢?

/别名为/home,意味着当用户访问/home时,URL 仍然是/home,但会被匹配为用户正在访问/

const routes = [{ path: '/', component: Homepage, alias: '/home' }]

嵌套路由中使用别名可以使用绝对路径来改变这个路由的路径

const routes = [
  {
    path: '/users',
    component: UsersLayout,
    children: [
      // 为这 3 个 URL 呈现 UserList
      // - /users
      // - /users/list
      // - /people
      { path: '', component: UserList, alias: ['/people', 'list'] },
    ],
  },
]

如果路由中有参数,那么绝对路径的别名路由必须包含此参数

const routes = [
  {
    path: '/users/:id',
    component: UsersByIdLayout,
    children: [
      // 为这 3 个 URL 呈现 UserDetails
      // - /users/24
      // - /users/24/profile
      // - /24
      { path: 'profile', component: UserDetails, alias: ['/:id', ''] },
    ],
  },
]

路由组件传参

vue-router提供了另一种传参给组件的方法,那就是props属性。props拥有布尔模式、对象模式、函数模式。

  • 布尔模式

propstrue$router.params将被设置为组件的props

const routes = [{ path: '/user/:id', component: User, props: true }]

// 命名视图必须为为每个视图定义props的值
const routes = [
  {
    path: '/user/:id',
    components: { default: User, sidebar: Sidebar },
    props: { default: true, sidebar: false }
  }
]
  • 对象模式

props是对象时,内容会原样设置到组件的props中。

const routes = [
  {
    path: '/user/profile',
    component: User,
    props: { isLogin: true }
  }
]
  • 函数模式

也可以是一个函数,参数为$route类型,返回值将被设置到组件的props中。

const routes = [
  {
    path: '/user',
    component: User,
    props: route => ({ name: route.query.abc })
  }
]

/user?abc=lisi这个URL将传递{name: 'lisi'}User组件。

历史模式

创建路由时demo使用的历史模式是HTML5模式,除此还有Hash模式。

  • HTML5模式

这种模式对SEO很友好,URL看上去与正常的URL没什么区别。

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [],
})

但如果用户直接访问http://demo.com/user/123会是404错误,这需要在服务器配置一个会退路由来解决,让其为匹配到URL时找到你程序的index.html

以下仅提供Nginx的配置,其他服务器请看官方文档

location / {
  try_files $uri $uri/ /index.html;
}
  • Hash模式

Hash模式会在URL中用哈希字符#隔开实际上用到的URL,这种模式与常规URL不同,因此对SEO不是很友好。

import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
  history: createWebHashHistory(),
  routes: [],
})

进阶内容

导航守卫

全局前置守卫

const router = createRouter({ ... })

router.beforeEach((to, from) => {
  // ...
  // 返回 false 以取消导航
  return false
})
  • to目标路由对象
  • from来源路由对象
  • 可以返回false来取消导航,也可以返回一个路由地址(可以是一个字符串或路由对象,返回的内容跟router.push()中可以用的参数是一样的)

除此支持旧版的第三个参数next

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

全局解析守卫

router.beforeResolve(async to => {
  if (to.meta.requiresCamera) {
    try {
      await askForCameraPermission()
    } catch (error) {
      if (error instanceof NotAllowedError) {
        // ... 处理错误,然后取消导航
        return false
      } else {
        // 意料之外的错误,取消导航并把错误传给全局处理器
        throw error
      }
    }
  }
})

全局后置钩子

router.afterEach((to, from) => {
  console.log(to.fullPath)
})

// 没有next参数,只有failure,在此钩子无法终止导航
router.afterEach((to, from, failure) => {
  if(failure){
    console.log(to.fullPath);
  }
})

这三个守卫/钩子跟拦截器的效果差不多,顺序就是按命名来理解。

路由级导航守卫

此导航守卫只在进入路由时才会触发,不会在paramsqueryhash变更时触发。

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // 返回值与全局前置导航守卫一样
      return true;
    },
  },
]

组件内导航守卫

提供三个可用的API:

  • beforeRouteEnter
  • beforeRouteUpdate
  • beforeRouteLeave
export default {
  name: 'Demo',
  beforeRouteEnter(to, from) {
    // 在渲染该组件的对应路由被验证前调用
    // 不能获取组件实例`this`,因为当守卫执行时,组件实例还没被创建!
  },
  beforeRouteUpdate(to, from) {
    // 在当前路由改变,但是该组件被复用时调用
    // 比如用的是一个带有动态参数的路径 `/users/:id`,参数变化组件会复用
    // 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
  },
  beforeRouteLeave(to, from) {
    // 在导航离开渲染该组件的对应路由时调用
    // 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
  },
}

beforeRouteEnter中虽然无法直接使用this访问实例,但是可以用next参数,在导航确认后调用此回调函数访问实例。

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

完整的导航流程

  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的回调函数,创建好的组件实例会作为回调函数的参数传入。

数据元信息

const routes = [
    {
        path: '/',
        name: 'Default',
        component: () => import('@/views/home/Home'),
        meta: {
            title: '首页',
            needAuth: false
        }
    }
];

router.beforeEach((to, from) => {
    // 设置每个路由页面的标题
    document.title = to.meta.title;
    return true;
})

参考文档