Vue Router 路由及其原理
Vue Router 是 Vue 的官方路由,便于构建单页应用,将路径和组件映射起来。
一、安装与使用
1、安装
npm install vue-router@4
# yarn
yarn add vue-router@4
2、基本用法
- 注册路由
- 引入路由
- 使用路由
- Home〡About 组件
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 模式,点击查看两种模式的区别。
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
createApp(App).mount('#app')
createApp(App).use(router).mount('#app')
router-view
将显示与 url 对应的组件,可以放在任何地方,以适应布局。
<template>
<nav class="nav">
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
</nav>
<router-view></router-view>
</template>
<template>
<div>Home</div>
</template>
实现效果:
3、路由懒加载
上面 router 注册的写法使得打包构建应用时,router 中所有组件都会被一次加载,例如有时不被用到的 About 组件也要加载,这对首页的显示有很大影响,而 Vue Router 支持动态导入,当路由被访问的时候才加载对应组件,改造如下:
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,改造如下:
- 配置别名
- 使用简化后的别名
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')
}
}
})
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
component: () => import('../components/Home.vue')
component: () => import('@components/Home.vue')
},
{
path: '/about',
component: () => import('../components/About.vue')
component: () => import('@components/About.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
4、router.push 实现跳转
除了使用 <router-link :to="...">
声明式跳转外,还可以使用 router.push 编程式导航的方式跳转,效果是一样的:
<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 添加新记录,也就是取代了当前链接进行跳转,后退时会跳转至原链接的上一级。使用如下:
<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
的方式读取路由的参数:
- 路由注册
- 路由跳转
- 获取参数
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
<template>
<nav class="nav">
<button @click="() => router.push('/')">Home</button>
<button @click="() => router.push('/about')">About</button>
<button @click="() => router.push('/about/123')">About</button>
</nav>
<router-view></router-view>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
</script>
<template>
<div>About</div>
</template>
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.params)
</script>
输出:
实现效果:
7、配置 404 错误空白页
可通过 /:pathMatch(.*)
这个正则匹配错误路径的页面:
- 路由注册
- 路由跳转
- 错误页面
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
<template>
<nav class="nav">
<button @click="() => router.push('/')">Home</button>
<button @click="() => router.push('/about/123')">About</button>
<button @click="() => router.push('/xxx')">Error</button>
</nav>
<router-view></router-view>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
</script>
<template>
<div>404</div>
</template>
实现效果:
8、嵌套路由的用法
可以在注册路由时配置 children 来嵌套路由,例如:
- 路由注册
- 路由跳转
- About 组件
- AboutChild1〡AboutChild2
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
<template>
<nav class="nav">
<button @click="() => router.push('/')">Home</button>
<button @click="() => router.push('/about/123')">About</button>
<button @click="() => router.push('/about/123/child1')">AboutChild1</button>
<button @click="() => router.push('/about/123/child2')">AboutChild2</button>
<button @click="() => router.push('/xxx')">Error</button>
</nav>
<router-view></router-view>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
</script>
<template>
<div>About</div>
<router-view></router-view>
</template>
<template>
<div>AboutChild1</div>
</template>
实现效果:
可以看到,访问 /about/123
时,在 About 的 router-view 中什么都不会呈现,因为没有匹配到嵌套路由。如果想在匹配到 /about/123
时在 router-view 渲染组件,可以提供一个空的嵌套路径:
children: [
{ path: '', component: AboutHome },
// ...其他子路由
],
9、路由命名与重定向
除了 path 外,还可以为路由提供 name,可以防止打错字以及绕过路径排序。使用 name 时如果需要用到动态路由,需要配合 params
实现:
- 路由注册
- 路由跳转
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
<template>
<nav class="nav">
<button @click="() => router.push('/')">Home</button>
<button @click="() => router.push('/about/123')">About</button>
<button @click="() => router.push({ name: 'about', params: { useId: '123' } })">
About
</button>
<!-- 或 -->
<router-link :to="{ name: 'about', params: { useId: '123' } }">
About
</router-link>
</nav>
<router-view></router-view>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
</script>
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。
使用示例:
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、组件内的路由守卫
- beforeRouteEnter:进入该路由时执行;
- beforeRouteUpdate:该路由中参数改变时执行;
- beforeRouteLeave:离开该路由时执行。
10-4、完整流程
路由导航解析流程:
- 导航被触发;
- 在失活的组件中调用 beforeRouteLeave;
- 调用全局的 beforeEach;
- 在复用的组件中调用 beforeRouteUpdate;
- 在路由配置中调用 beforeEnter;
- 解析异步路由组件;
- 在被激活的组件中调用 beforeRouteEnter;
- 调用全局的 beforeResolve;
- 导航被确认;
- 调用全局的 afterEach 钩子;
- 触发 DOM 更新;
- 调用 beforeRouteEnter 中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
钩子触发顺序:
beforeRouteLeave
:路由组件的组件离开路由前钩子,可取消路由离开;beforeEach
:路由全局前置守卫,可用于登录验证、全局路由 loading 等;beforeEnter
:路由独享守卫;beforeRouteEnter
:路由组件的组件进入路由前钩子;beforeResolve
:路由全局解析守卫;afterEach
:路由全局后置钩子;- beforeCreate:组件生命周期;
- created:组件生命周期;
- beforeMount:组件生命周期;
- deactivated:离开缓存组件 a,或触发 a 的组件销毁钩子;
- mounted:访问/操作 dom;
- activated:进入缓存组件,进入 a 的嵌套子组件;
beforeRouteEnter
:执行其回调函数 next。
二、Vue Router 原理解析
Vue Router 的原理是通过 JS 监听 URL 的变化,对页面进行处理。其中:
默认的 hash 模式通过 onhashchange 事件监听 url 的 hash 值来实现路由跳转。
而 history 模式则利用 pushState
和 replaceState
来修改浏览器的历史记录栈,并通过 onpopState 事件监听历史记录的变化来实现路由跳转,由于是单页面应用,所以还需要后台配置。
具体实现可参考「SPA 的原理及实现」