Skip to main content

Vue Router 路由及其原理

Vue Router 是 Vue 的官方路由,便于构建单页应用,将路径和组件映射起来。

一、安装与使用

1、安装

npm install vue-router@4
# yarn
yarn add vue-router@4

2、基本用法

src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../components/Home.vue'
import About from '../components/About.vue'

const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
]

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

export default router

这里的 history 用来声明使用 Hash 模式还是 History 模式,点击查看两种模式的区别

实现效果:

3、路由懒加载

上面 router 注册的写法使得打包构建应用时,router 中所有组件都会被一次加载,例如有时不被用到的 About 组件也要加载,这对首页的显示有很大影响,而 Vue Router 支持动态导入,当路由被访问的时候才加载对应组件,改造如下:

src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../components/Home.vue'
import About from '../components/About.vue'

const routes = [
{
path: '/',
component: Home,
component: () => import('../components/Home.vue')
},
{
path: '/about',
component: About,
component: () => import('../components/About.vue')
}
]

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

export default router

上面的 ../components 可通过配置别名简化为 @components,改造如下:

vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@components': path.resolve(__dirname, 'src/components')
}
}
})

4、router.push 实现跳转

除了使用 <router-link :to="..."> 声明式跳转外,还可以使用 router.push 编程式导航的方式跳转,效果是一样的:

src/App.vue
<template>
<nav class="nav">
<button @click="() => router.push('/')">Home</button>
<button @click="() => router.push('/about')">About</button>
</nav>
<router-view></router-view>
</template>

<script setup lang="ts">
import { useRouter } from 'vue-router'

const router = useRouter()
</script>

push 除了传字符串路径外,还可以传一个描述地址的对象:

// 字符串路径
router.push('/about/123')

// 带有路径的对象,结果同上
router.push({ path: '/about/123' })

// 带查询参数,结果是 /about/123?num=666
// 可通过 route.query 获取,输出 {num: '666'}
router.push({ path: '/about/123', query: { num: 666 } })

// 带 hash,结果是 /about/123#gz
// 可通过 route.hash 获取,输出 #gz
router.push({ path: '/about/123', hash: '#gz' })

5、替换 / 跨级跳转链接用法

使用 replace 在跳转时不会向 history 添加新记录,也就是取代了当前链接进行跳转,后退时会跳转至原链接的上一级。使用如下:

src/App.vue
<template>
<nav class="nav">
<router-link to="/about" replace>About</router-link>
<!-- 或 -->
<button @click="() => router.replace('/about')">About</button>
</nav>
<router-view></router-view>
</template>

window.history.go(n) 类似,可以使用 router.go(n) 前进或后退多少步:

router.go(1)

6、动态路由与 params 读参

动态路由指的是路由中的路径不固定,例如将 path 在 Route 匹配时写成 /detail/:id,那么 /detail/abc/detail/123 都可以匹配到该 Route。可通过 route.params.xx 的方式读取路由的参数:

src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
{
path: '/',
component: () => import('@components/Home.vue')
},
{
path: '/about',
path: '/about/:useId',
component: () => import('@components/About.vue')
}
]

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

export default router

实现效果:

7、配置 404 错误空白页

可通过 /:pathMatch(.*) 这个正则匹配错误路径的页面:

src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
{
path: '/',
component: () => import('@components/Home.vue')
},
{
path: '/about/:useId',
component: () => import('@components/About.vue')
},
{
path: '/:pathMatch(.*)*',
component: () => import('@components/Error.vue')
}
]

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

export default router

实现效果:

8、嵌套路由的用法

可以在注册路由时配置 children 来嵌套路由,例如:

src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
{
path: '/',
component: () => import('@components/Home.vue')
},
{
path: '/about/:useId',
component: () => import('@components/About.vue'),
children: [
{
// 当 /user/:useId/child1 匹配成功
// AboutChild1 将被渲染到 User 的 <router-view> 中
path: 'child1',
component: () => import('@components/AboutChild1.vue'),
},
{
path: 'child2',
component: () => import('@components/AboutChild2.vue'),
},
],
},
{
path: '/:pathMatch(.*)*',
component: () => import('@components/Error.vue')
}
]

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

export default router

实现效果:

可以看到,访问 /about/123 时,在 About 的 router-view 中什么都不会呈现,因为没有匹配到嵌套路由。如果想在匹配到 /about/123 时在 router-view 渲染组件,可以提供一个空的嵌套路径:

children: [
{ path: '', component: AboutHome },
// ...其他子路由
],

9、路由命名与重定向

除了 path 外,还可以为路由提供 name,可以防止打错字以及绕过路径排序。使用 name 时如果需要用到动态路由,需要配合 params 实现:

src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
{
path: '/',
component: () => import('@components/Home.vue')
},
{
path: '/about/:useId',
name: 'about',
component: () => import('@components/About.vue')
},
]

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

export default router

Vue Router 还可以在 routes 配置中完成重定向,例如从 /home 重定向到 /

const routes = [{ path: '/home', redirect: '/' }]
// 重定向的目标可以是一个命名路由
const routes = [{ path: '/home', redirect: { name: 'homepage' } }]

10、导航守卫(路由守卫)

vue-router 导航守卫提供了一些钩子函数,用于在路由跳转的各个过程中执行指定操作。例如需要登陆才能访问的页面,用户如果未登录进入时会跳转到登录页。

通用钩子参数:

  • to:目标路由对象;
  • from:即将要离开的路由对象;
  • next:用于使导航守卫队列继续向下执行。
    • 调用 next() 继续执行下一个钩子,否则路由跳转等会停止;
    • 调用 next(false) 中断当前的导航;
    • 调用 next('/')next({ path: '/' }) 跳转到指定地址。

10-1、全局的路由守卫

  • beforeEach:全局前置守卫,在路由跳转前触发,主要是用于登录验证;
  • beforeResolve:全局解析守卫,与 beforeEach 类似,区别在于这个钩子在 beforeEach 和 beforeRouteEnter 之后,afterEach 之前调用;
  • afterEach:全局后置守卫,在路由跳转后触发。注意 afterEach 不在导航守卫队列内,没有迭代的 next。

使用示例:

src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
{
path: '/',
redirect: '/login'
},
{
path: '/login',
component: () => import('@components/Login.vue')
},
{
path: '/about',
component: () => import('@components/About.vue')
}
]

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

// 挂载路由导航守卫, 控制页面访问权限
router.beforeEach((to, from, next) => {
if (to.path === '/login') return next()
// 获取 token
const tokenStr = window.sessionStorage.getItem('token')
if (!tokenStr) return next('/login')
next()
})

export default router

10-2、路由独享的守卫

beforeEnter:与 beforeEach 相同,在 beforeEach 之后紧随执行。

10-3、组件内的路由守卫

10-4、完整流程

路由导航解析流程:

  1. 导航被触发;
  2. 在失活的组件中调用 beforeRouteLeave
  3. 调用全局的 beforeEach
  4. 在复用的组件中调用 beforeRouteUpdate
  5. 在路由配置中调用 beforeEnter
  6. 解析异步路由组件;
  7. 在被激活的组件中调用 beforeRouteEnter
  8. 调用全局的 beforeResolve
  9. 导航被确认;
  10. 调用全局的 afterEach 钩子;
  11. 触发 DOM 更新;
  12. 调用 beforeRouteEnter 中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

钩子触发顺序:

  1. beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开;
  2. beforeEach:路由全局前置守卫,可用于登录验证、全局路由 loading 等;
  3. beforeEnter:路由独享守卫;
  4. beforeRouteEnter:路由组件的组件进入路由前钩子;
  5. beforeResolve:路由全局解析守卫;
  6. afterEach:路由全局后置钩子;
  7. beforeCreate:组件生命周期;
  8. created:组件生命周期;
  9. beforeMount:组件生命周期;
  10. deactivated:离开缓存组件 a,或触发 a 的组件销毁钩子;
  11. mounted:访问/操作 dom;
  12. activated:进入缓存组件,进入 a 的嵌套子组件;
  13. beforeRouteEnter:执行其回调函数 next。

点击查看更多 Vue Router 用法

二、Vue Router 原理解析

Vue Router 的原理是通过 JS 监听 URL 的变化,对页面进行处理。其中:

默认的 hash 模式通过 onhashchange 事件监听 url 的 hash 值来实现路由跳转。

而 history 模式则利用 pushStatereplaceState 来修改浏览器的历史记录栈,并通过 onpopState 事件监听历史记录的变化来实现路由跳转,由于是单页面应用,所以还需要后台配置。

具体实现可参考「SPA 的原理及实现」