一、概述
Vue Router >> 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:
- 嵌套的路由/视图表
- 模块化的、基于组件的路由配置
- 路由参数、查询、通配符
- 基于 Vue.js 过渡系统的视图过渡效果
- 细粒度的导航控制
- 带有自动激活的 CSS class 的链接
- HTML5 历史模式或 hash 模式,在 IE9 中自动降级
- 自定义的滚动条行为
用 Vue + Vue Router 创建单页应用非常简单:通过 Vue.js,我们已经用组件组成了我们的应用。当加入 Vue Router 时,我们需要做的就是将我们的组件映射到路由上,让 Vue Router 知道在哪里渲染它们。下面是一个基本的例子:
二、初探
接下来,我们先通过一个示例,让大家了解路由的基础使用:
1. 构建项目
$ npm init vite@latest vue-router-test -- --template vue-ts
2. 新建页面文件
在项目根目录,输入如下终端指令,创建页面文件:
# 新建目录$ mkdir -p src/pages/Home$ mkdir -p src/pages/News# 新建文件$ cd > src/pages/Home/index.vue$ cd > src/pages/News/index.vue
并在页面文件中,填入如下内容:
<script setup lang="ts"></script><template><p>This is XXX page.</p></template>
提示:请将
XXX替换为目录名。
3. 新建路由文件 & 安装路由
# 新建路由文件$ mkdir -p src/router && cd > src/router/index.ts# 安装路由$ npm install vue-router@4
src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';// → 引入组件模块import Home from '../pages/Home/index.vue';// → 定义路由const routes: Array<RouteRecordRaw> = [{ path: '/', redirect: '/home' /** 重定向 */ },{ path: '/home', component: Home },{ path: '/news', component: () => import('../pages/News/index.vue') /** 懒加载组件模块 */ },];// → 创建路由const router = createRouter({history: createWebHistory() /** 创建基于history模式的路由 */,routes,});// → 导出路由export default router;
4. 注册路由
src/main.ts
import { createApp } from 'vue';import router from './router';import App from './App.vue';// → 创建应用实例const app = createApp(App);// → 注入路由app.use(router);// → 挂载app.mount('#app');
5. 配置路由链接
src/App.vue
<script setup lang="ts"></script><template><!-- 导航 --><nav><router-link to="/home">Home</router-link><span> / </span><router-link to="/news">News</router-link></nav><!-- 路由视图 --><router-view></router-view></template>
6. 运行项目,查看效果
$ npm run dev
演示效果:

注意:
- 路由实例创建后需在vue实例中注册路由。
- 通过
<router-link>实现路由动态切换,最终会被渲染成超链接。- 通过
<router-view>呈现路由切换后显示的组件内容。
三、基础
1. 路由模式
vue-router 在创建路由实例时,提供了2种路由有模式:hash / history
| # | hash |
history |
|---|---|---|
| 呈现形式 | http://localhost:3000/#/home | http://localhost:3000/home |
| 构造函数 | createWebHashHistory() |
createWebHistory() |
| 是否重载 | 否(Hash不会被包括在Http请求中) | 是 |
| 回车刷新 | 无影响 | 404,需配置重定向 |
2. 路由嵌套 *
一些应用程序的 UI 由多层嵌套的组件组成,比如一些UI组件库的使用文档就是使用这种结构,我们称之为路由嵌套结构。在路由配置中,同样根据嵌套结构进行嵌套配置,比如:
/web/html/web/css/web/js
在 /web 路由下,嵌套 /html、/css和 /js 路由:
{path: '/web',redirect: '/web/html',component: import('../pages/Web/index.vue'),children: [{ path: 'html', component: import('../pages/Html/index.vue') },{ path: 'css', component: import('../pages/CSS/index.vue') },{ path: 'js', component: import('../pages/JavaScript/index.vue') },],};
注意事项:
- 配置路由时通过指定
children属性来配置当前路由的子路由。 - 子路由属于谁,
<router-view />就写在谁下面。 * - 默认重定向,使用
redirect属性。 - 如果子路由在主页中,设置
path属性时,直接/子路由路径即可。
3. 编程式的导航
除了使用 <router-link> 创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现。
3.1. router.push() *
语法如下:
router.push(location, onComplete?, onAbort?)
注意:在 Vue 实例内部的模板文件中,你可以通过
$router访问路由实例。
示例代码:
<script setup lang="ts">import { useRouter } from 'vue-router';// → 获取router实例const router = useRouter();// → 在事件函数中调用const onGoNews = () => {router.push('/news');};</script><template><p>This is Home page.</p><button type="button" @click="onGoNews">Go News</button><!-- 直接在模板中获取router实例 --><button type="button" @click="$router.push('/news')">Go News</button></template>
想要导航到不同的 URL,可以使用 router.push 方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,会回到之前的 URL。
当你点击 <router-link> 时,内部会调用这个方法,所以点击 <router-link :to="..."> 相当于调用 router.push(...) :
| 声明式 | 编程式 |
|---|---|
<router-link :to="..."> |
router.push(...) |
该方法的参数可以是一个字符串路径,或者一个描述地址的对象。例如:
// → 字符串router.push('/home')// → 对象router.push({ path: '/home' })// → 命名的路由router.push({ name: 'home', params: { id: '123' }})// → 带查询参数,变成 /register?plan=privaterouter.push({ path: '/register', query: { plan: 'private' }})
注意:如果提供了
path,params会被忽略,上述例子中的query并不属于这种情况。取而代之的是下面例子的做法,你需要提供路由的name或手写完整的带有参数的path:
const username = 'muzili'// → 我们可以手动建立 url,但我们必须自己处理编码router.push(`/user/${username}`) // → /user/muzili// → 同样router.push({ path: `/user/${username}` }) // → /user/muzili// → 如果可能的话,使用 name 和 params 从自动 URL 编码中获益router.push({ name: 'user', params: { username } }) // → /user/muzili// params 不能与 path 一起使用router.push({ path: '/user', params: { username } }) // → /user
提示:同样的规则也适用于
<router-link\>组件的to属性。
3.2. router.replace()
语法如下:
router.replace(location, onComplete?, onAbort?)
跟 router.push() 很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。
| 声明式 | 编程式 |
|---|---|
<router-link :to="..." replace> |
router.replace(...) |
也可以直接在传递给 router.push 的 routeLocation 中增加一个属性 replace: true :
router.push({ path: '/home', replace: true })// 相当于router.replace({ path: '/home' })
3.3. router.go()
这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)。
// → 向前移动一条记录,与 router.forward() 相同router.go(1)// → 返回一条记录,与router.back() 相同router.go(-1)// → 前进 3 条记录router.go(3)// → 如果没有那么多记录,静默失败router.go(-100)router.go(100)
4. 命名路由
除了 path 之外,你还可以为任何路由提供 name。这有以下优点:
- 没有硬编码的 URL
params的自动编码/解码。- 防止你在 url 中出现打字错误。
- 绕过路径排序(如显示一个)
{path: '/commodities/:kind',name: 'commodities',component: Commodities}
要链接到一个命名路由,可以给 router-link 的 to 属性传一个对象:
<router-link :to="{name:'commodities', params:{ kind:'phone'}}">Commodities</router-link>
这跟代码调用 router.push() 是一回事:
router.push({ name:'commodities', params:{ kind:'phone'}})
这两种方式都会把路由导航到 /commodities/phone 路径。
5. 命名视图
有时候想同时 (同级) 展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar (侧导航) 和 main (主内容) 两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view 没有设置名字,那么默认为 default。
<router-view class="view left-sidebar" name="LeftSidebar" /><router-view class="view main-content" /><router-view class="view right-sidebar" name="RightSidebar" />
一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用 components 配置 (带上 s):
{path: '/',components: {default: Home,// LeftSidebar: LeftSidebar 的缩写LeftSidebar,// 它们与 <router-view> 上的 name 属性匹配RightSidebar,},},
6. 路由高亮效果
在路由链接选中时,会自动添加 router-link-active 类名:

你可以在 App.vue 中全局设置高亮时的颜色,如:
.router-link-active {color: blue;}
7. 重定向/取别名
7.1. 重定向
重定向也是通过 routes 配置来完成,下面例子是从 /a 重定向到 /b:
{ path: '/a', redirect: '/b' }
重定向的目标也可以是一个命名的路由:
{ path: '/a', redirect: { name: 'foo' }}
甚至是一个方法,动态返回重定向目标:
{ path: '/a', redirect: to => {// → 方法接收 目标路由 作为参数// → return 重定向的 字符串路径/路径对象return { path: '/search', query: { q: to.params.searchText } }}}
注意:导航守卫 并没有应用在跳转路由上,而仅仅应用在其目标上。
7.2. 取别名
重定向 的意思是,当用户访问 /a时,URL 将会被替换成 /b,然后匹配路由为 /b,那么 别名 又是什么呢?
/a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。
上面对应的路由配置为:
{ path: '/a', component: A, alias: '/b' }
别名的功能让你可以自由地将 UI 结构映射到任意的 URL,而不是受限于配置的嵌套路由结构。
8. 路由参数 *
设置路由跳转时,可以进行参数传递,如下所示:
8.1. 参数以 / 传递(path)
@指定传递参数:
{// → 动态路径参数 以冒号开头// → name、course 为传递数据path: '/news/:name/:course',name: 'news',component: News}
@设置传递数据:
<!-- 方法1 -><router-link to="/news/muzili/web">News</router-link><!-- 方法2 -><router-link :to="{name:'news', params:{name:'muzili', course:'web'}}">News</router-link>
@获取传递数据:
<script setup lang="ts">import { useRoute } from 'vue-router';const route = useRoute();// 1. 脚本中访问console.log(route.params); // → {name: 'muzili', course: 'web'}</script><template><!-- 2. 模板中访问 --><p>{{ $route.params.name }} - {{ $route.params.course }}</p></template>
注意:是
$route.params而不是$router.params,后面不要加s。
8.2. 参数以 ? 传递(query)
我们常见的参数传递是通过 location.search 传递的,即在 URI 中将参数 以 ? 隔开进行传递,vue中要使用此方式传递设置如下:
@指定参数传递:
使用query传参,路由配置的时候path不用带参数
@设置传递参数:
<router-link to="/news?name=muzili&course=web">News</router-link><router-link :to="{path:'/news', query:{name:'muzili', course:'web'">}}">News</router-link>
@获取传递数据:
<script setup lang="ts">import { useRoute } from 'vue-router';const route = useRoute();// 1. 脚本中访问console.log(route.query); // → {name: 'muzili', course: 'web'}</script><template><!-- 2. 模板中访问 --><p>{{ $route.query.name }} - {{ $route.query.course }}</p></template>
8.3. Path vs Query
| # | 类型 | 路由配置文件 | 传递 | 访问 |
|---|---|---|---|---|
**Path** |
/ |
/path/:params |
name/ params |
$route.params.xxx |
**Query** |
? |
无需特殊配置 | path/ query |
$route.query.xxx |
9. 匹配 404 Not Fount
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }
10. v-slot
<router-link> 通过一个作用域插槽暴露底层的定制能力。这是一个更高阶的 API,主要面向库作者,但也可以为开发者提供便利,大多数情况下用在一个类似 NavLink 这样的组件里。
<router-linkto="/about"customv-slot="{ href, route, navigate, isActive, isExactActive }"><NavLink :active="isActive" :href="href" @click="navigate">{{ route.fullPath }}</NavLink></router-link>
href:解析后的 URL。将会作为一个<a>元素的href属性。如果什么都没提供,则它会包含base。route:解析后的规范化的地址。navigate:触发导航的函数。 会在必要时自动阻止事件,和router-link一样。例如:ctrl或者cmd+ 点击仍然会被navigate忽略。isActive:如果需要应用 active class,则为true。允许应用一个任意的 class。isExactActive:如果需要应用 exact active class,则为true。允许应用一个任意的 class。
四、进阶
1. 导航守卫
导航:表示路由正在发生改变
守卫: 防守护卫,类似于关卡,决定是否放行
正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的,单个路由独享的,或者 组件级 的。
守卫参数:
每个守卫方法接收三个参数:
返回值:
各个守卫可以返回的值如下:
false: 取消当前的导航(不执行跳转)路由地址:多用于重定向,比如在守卫中判断登录状态,如果没有登录则重定向到登录页undefine/true:导航有效,放行并调用下一个导航守卫。
2.1. 全局守卫
@前置守卫
当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于等待中。
// -- 前置守卫router.beforeEach(async (to, from) => {console.log('__beforeEach__');// → 检测登录状态if (to.path !== '/login' && !localStorage.getItem('loginStatus')) {return '/login';}});
@解析守卫
和前置守卫类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
// -- 解析守卫router.beforeResolve(async (to, from) => {console.log('__beforeResolve__');});
提示:解析守卫是获取数据或执行任何其他操作(如果用户无法进入页面时你希望避免执行的操作)的理想位置。
@后置钩子
你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:
// -- 后置钩子router.afterEach(async (to, from) => {console.log('__afterEach__');document.title = to.meta.title as string;});
提示:后置钩子对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用.
2.2. 路由独享守卫
你可以直接在路由配置上定义 beforeEnter 守卫:
{path: '/home',component: Home,beforeEnter: (to, from) => {/** ... */},},
2.3. 组件内守卫
最后,你可以在路由组件内直接定义路由导航守卫(传递给路由配置的)
beforeRouteEnter- `beforeRouteUpdate
beforeRouteLeave
const UserDetails = {template: `...`,beforeRouteEnter(to, from) {// 在渲染该组件的对应路由被验证前调用// 不能获取组件实例 `this` !// 因为当守卫执行时,组件实例还没被创建!},beforeRouteUpdate(to, from) {// 在当前路由改变,但是该组件被复用时调用// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`},beforeRouteLeave(to, from) {// 在导航离开渲染该组件的对应路由时调用// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`},};
beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。
不过,你可以通过传一个回调给 next 来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数:
beforeRouteEnter (to, from, next) {next(vm => {// → 通过 vm 访问组件实例})}
注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了,所以不支持传递回调,因为没有必要了:
beforeRouteUpdate (to, from) {// just use `this`this.name = to.params.name}
这个 离开守卫 通常用来预防用户在还未保存修改前突然离开。该导航可以通过返回 false 来取消。
beforeRouteLeave (to, from) {const answer = window.confirm('Do you really want to leave? you have unsaved changes!')if (!answer) return false}
提示:在組合式API
<script setup>中,你可以通过onBeforeRouteUpdate和onBeforeRouteLeave分别添加update和leave守卫。
2.4. 完整的导航解析流程
- → 导航被触发
- → 在失活的组件里调用
beforeRouteLeave守卫 - → 调用全局的
beforeEach守卫 - → 在重用的组件里调用
beforeRouteUpdate守卫(2.2+) - → 在路由配置里调用
beforeEnter - → 解析异步路由组件
- → 在被激活的组件里调用
beforeRouteEnter - → 调用全局的
beforeResolve守卫(2.5+) - → 导航被确认
- → 调用全局的
afterEach钩子 - → 触发 DOM 更新
- → 调用
beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入
2. 路由元信息
定义路由的时候可以配置 meta 字段,用于添加关于路由的一些额外信息,比如页面标题:
{path: '/home',name: 'home',component: Home,meta: { title: '首页' },}
那么如何访问这个 meta 字段呢?
// -- 后置钩子router.afterEach(async (to, from) => {console.log('__afterEach__');document.title = to.meta.title as string;});
3. 过渡动效
想要在你的路径组件上使用转场,并对导航进行动画处理,你需要使用 v-slot API >>
<router-view v-slot="{ Component }"><transitionenter-active-class="animate__animated animate__fadeInRight"leave-active-class="animate__animated animate__fadeOutLeft"><component :is="Component" /></transition></router-view>
提示:上述示例中,通过 animate.css >> 来定义过渡效果。
!异常处理
提示:
Component inside <Transition> renders non-element root node that cannot be animated.
原因:由于 <Transition> 只能应用在 单节点元素 上,而我们在构造组件模块时,直接在 <template> 中写布局,如:
<template><p>This is Home page.</p><p>Hello, vue.js!</p></template>
在渲染之后,实际呈现的结构为:
<transition><p>This is Home page.</p><p>Hello, vue.js!</p></transition>
所以抛出了异常,解决方案,我们只需要在组件外部包裹一层标签即可,如:
<template><div class="page"><p>This is Home page.</p><p>Hello, vue.js!</p></div></template>
1.1. 单个路由的过渡
上面的用法会对所有的路由使用相同的过渡。如果你想让每个路由的组件有不同的过渡,你可以将元信息和动态的 name 结合在一起,放在 <transition> 上:
const routes = [{path: '/custom-transition',component: PanelLeft,meta: { transition: 'slide-left' },},{path: '/other-transition',component: PanelRight,meta: { transition: 'slide-right' },},]
<router-view v-slot="{ Component, route }"><!-- 使用任何自定义过渡和回退到 fade --><transition :name="route.meta.transition || 'fade'"><component :is="Component" /></transition></router-view>
1.2. 基于路由的动态过渡
还可以根据目标路由和当前路由之间的关系,动态地确定使用的过渡。使用和刚才非常相似的片段:
<!-- 使用动态过渡名称 --><router-view v-slot="{ Component, route }"><transition :name="route.meta.transition"><component :is="Component" /></transition></router-view>
我们可以添加一个 导航后置钩子(**afterEach**),根据路径的深度动态添加信息到 meta 字段。
router.afterEach((to, from) => {const toDepth = to.path.split('/').lengthconst fromDepth = from.path.split('/').lengthto.meta.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'})
4. 数据获取 *
有时候,进入某个路由后,需要从服务器获取数据。例如,在渲染用户信息时,你需要从服务器获取用户的数据。我们可以通过两种方式来实现:
- 导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示 加载中… 之类的指示。
- 导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。
从技术角度讲,两种方式都不错 —— 就看你想要的用户体验是哪种。
@导航完成之后获取
created() {// → 显示加载中,提升用户体验// → ajax}
@导航完成之前获取
beforeRouteEnter() {// => ajax}
5. 滚动行为
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router 能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。
注意: 这个功能只在支持
history.pushState的浏览器中可用。
当创建一个 Router 实例,你可以提供一个 scrollBehavior 方法:
const router = createRouter({history: createWebHashHistory(),routes: [...],scrollBehavior (to, from, savedPosition) {// return 期望滚动到哪个的位置}})
scrollBehavior 方法接收 to 和 from 路由对象。第三个参数 savedPosition 当且仅当 popstate 导航 (通过浏览器的 前进/后退 按钮触发) 时才可用。
该函数可以返回一个 [ScrollToOptions](https://developer.mozilla.org/en-US/docs/Web/API/ScrollToOptions) 位置对象:
{ x: number, y: number }{ selector: string, offset? : { x: number, y: number }}(offset 只在 2.6.0+ 支持)
如果返回一个 falsy (译者注:falsy 不是 false,参考这里)的值,或者是一个空对象,那么不会发生滚动。
scrollBehavior (to, from, savedPosition) {return { x: 0, y: 0 }}
对于所有路由导航,简单地让页面滚动到顶部。
返回 savedPosition,在按下 后退/前进 按钮时,就会像浏览器的原生表现那样:
scrollBehavior (to, from, savedPosition) {if (savedPosition) {return savedPosition} else {return { x: 0, y: 0 }}}
如果你要模拟“滚动到锚点”的行为:
scrollBehavior (to, from, savedPosition) {if (to.hash) {return {selector: to.hash}}}
