1. Vue router 4 基础
在构建现代
Web
应用时,单页应用(SPA
)因其流畅的用户体验和快速的页面切换能力,成为了众多项目的首选架构。
然而,在SPA
中,随着应用功能的日益复杂,权限控制成为了一个不可忽视的问题。如何确保不同用户只能访问其被授权的资源,是保障应用安全和数据一致性的关键。
在这个过程中,路由管理扮演着至关重要的角色。Vue Router
,作为Vue
官方提供的路由管理器,不仅能够帮助我们定义应用的页面结构和导航逻辑,还能与权限控制机制紧密结合,实现细粒度的访问控制。
在Vue3
项目中,通过Vue Router
的路由守卫功能,我们可以在用户访问某个路由之前进行权限验证。例如,我们可以利用全局前置守卫(beforeEach
)来检查用户的登录状态和权限级别,从而决定是否允许用户继续访问目标页面。如果用户未登录或权限不足,我们可以重定向用户到登录页面或提示页面,确保应用的安全性。
此外,Vue Router
还支持路由元信息(meta
字段),这为我们在路由层面定义额外的信息提供了便利。通过路由元信息,我们可以为不同的路由设置不同的权限要求,并在路由守卫中根据这些信息进行权限验证。这种方式使得权限控制更加灵活和可配置。
1.1 安装
可以使用npm
包管理器直接安装,或者在创建新项目时,使用create-vue
创建一个基于Vite
的项目,并选择加入vue-router
选项。
npm install vue-router@4
or
npm create vue@latest
1.2 配置
安装完成后,接下来需要在项目中配置Vue Router
。这通常涉及以下几个步骤:
- 创建路由实例:首先,需要导入
Vue Router
并创建一个路由实例。在这个过程中,需要定义应用的路由配置,包括各个路由的路径(path
)、组件(component
)等信息。 - 挂载路由实例:然后,需要将创建的路由实例挂载到
Vue
应用中。这通常是在创建Vue
应用实例时,通过createApp
函数的.use()
方法完成的。
Vue Router 4
使用createRouter()
函数来创建并配置路由器实例。createRouter()
函数替代了Vue Router 3
中的new VueRouter()
函数。
// v4.x 示例
// 1. 引入createRouter和createWebHistory
import { createMemoryHistory, createRouter } from 'vue-router'
// 2. 引入路由组件
import HomeView from './HomeView.vue'
import AboutView from './AboutView.vue'
// 3. 定义路由
const routes = [
{ path: '/', component: HomeView },
{ path: '/about', component: AboutView },
]
// 4. 创建 router 实例并 export
const router = createRouter({
history: createMemoryHistory(),
routes,
})
export default router
// 5. 在main.js中挂载
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
// 6. 在App.vue中使用RouterView来渲染路由组件。
// RouterView: 可以使 Vue Router 知道你想要在哪里渲染当前 URL 路径对应的路由组件。
<template>
<RouterView></RouterView>
</template>
<script setup>
import { RouterView } from 'vue-router'
</script>
1.3 基本属性
path
:路由路径,字符串。应该以/
开头,除非该路由为另一条路由的子路由。当浏览器的URL
与这个路径匹配时,就会渲染对应的组件。name
:路由名称,字符串,必须唯一。命名路由可以在编程式导航中通过名称来引用路由,而不是通过路径字符串。component
:路由组件(通常是导入的组件)。这个组件会在路由匹配时渲染。children
:嵌套路由。redirect
:路由重定向。beforeEnter
:路由导航守卫。props
:允许将参数作为props
传递给由router-view
渲染的组件。应是一个具有与components
相同键的对象,或是一个应用于所有组件的布尔值。meta
:路由元信息,一个包含自定义信息的对象,用于存储路由的额外信息,如路由标题、是否需要登录等。这个对象可以在路由守卫(如在导航守卫中使用to.meta.requiresAuth
)中被访问,用于控制路由的访问权限、添加页面标题等。
meta.title
:路由标题。meta.requiresAuth
:是否需要登录。meta.keepAlive
:是否缓存路由组件。meta.icon
:路由图标。meta.hidden
:是否在菜单中隐藏。meta.activeMenu
:激活菜单。meta.breadcrumb
:面包屑信息。
1.4 动态路由匹配
动态路由匹配是指根据当前路由路径,动态匹配出对应的路由组件。在 Vue Router
中,我们可以使用:id
等动态参数来匹配路由路径。
import User from './User.vue'
const routes = [
// 动态字段以冒号开始
{ path: '/users/:id', component: User },
]
路径参数 用冒号 :
表示。当一个路由被匹配时,它的 params
的值将在每个组件中以 route.params
的形式暴露出来。
<template>
<div>
<!-- 当前路由可以通过 $route 在模板中访问 -->
User {{ $route.params.id }}
</div>
</template>
可以在同一个路由中设置有多个 路径参数,它们会映射到 $route.params
上的相应字段
匹配模式 | 匹配路径 | $route.params |
---|---|---|
/users/:username | /users/ayla | { username: 'ayla' } |
/users/:username/ids/:id | /users/ayla/ids/001 | { username: 'ayla', id: '001' } |
常规参数只匹配 url
片段之间的字符,用 /
分隔。如果想匹配任意路径,我们可以使用自定义的 路径参数 正则表达式,在 路径参数 后面的括号中加入 正则表达式 :
const routes = [
// 将匹配所有内容并将其放在 `route.params.pathMatch` 下
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
// 将匹配以 `/user-` 开头的所有内容,并将其放在 `route.params.afterUser` 下
{ path: '/user-:afterUser(.*)', component: UserGeneric },
]
更多的动态路由匹配规则,可以参考vue router 4
官方文档 路由的匹配语法 部分
注意:
使用带有参数的路由时,当用户从/users/johnny
导航到/users/jolyne
时,相同的组件实例将被重复使用(因为两个路由都渲染同个组件,比起销毁再创建,复用会更加高效)。不过,这也意味着组件的生命周期钩子不会被调用。
若要对同一个组件中参数的变化做出响应的话,可以watch
$route.params
或者在路由守卫beforeRouteUpdate
做出响应。
1.5 导航方式
声明式 | 编程式 |
---|---|
<router-link :to="..."> | router.push(...) |
-
声明式导航: 通过
JavaScript
代码来进行页面的跳转和切换。Vue Router
提供了一些方法来实现这种导航方式,如router.push
、router.replace
和router.go
。 -
编程式导航:通过在模板中声明的方式来进行页面的切换和跳转。在
Vue Router
中,可以使用<router-link>
组件来实现声明式导航。<router-link>
组件可以被渲染为一个<a>
标签,用于通过路由链接跳转页面。例如:<router-link to="/home">Home</router-link>
router push
:会向 history
栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,会回到之前的 URL
。
// 字符串路径
router.push('/users/eduardo')
// 带有路径的对象
router.push({ path: '/users/eduardo' })
// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })
// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })
// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })
router replace
:类似于 router.push
,唯一不同的是,它在导航时不会向 history
添加新记录。
router.push({ path: '/home', replace: true })
// 相当于
router.replace({ path: '/home' })
router.go
:该方法采用一个整数作为参数,表示在历史堆栈中前进或后退多少步。
// 向前移动一条记录,与 router.forward() 相同
router.go(1)
// 返回一条记录,与 router.back() 相同
router.go(-1)
// 前进 3 条记录
router.go(3)
// 如果没有那么多记录,静默失败
router.go(-100)
router.go(100)
1.6 历史模式
在创建路由器实例时,history
配置允许我们在不同的历史模式中进行选择。
Hash
模式:createWebHashHistory()
- 地址带#
号,不需要服务器配置,不利于SEO
。HTML5
模式:createWebHistory()
- 不带#
号,需要适当的服务器配置,否则刷新会有404错误。Memory
模式:createMemoryHistory()
- 不会假定自己处于浏览器环境,不会与URL
交互也不会自动触发初始导航,不能前进或后退,适合Node
环境和SSR
。
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(), // html5 模式
routes: [
//...
],
})
1.7 导航守卫
a. 全局前置守卫 (beforeEach)
b. 全局解析守卫 (beforeResolve)
c. 全局后置钩子 (afterEach)
d. 路由独享守卫 (beforeEnter)
e. 组件内的守卫 (beforeRouteEnter, beforeRouteUpdate, beforeRouteLeave)
1.7.1 全局前置守卫
router.beforeEach()
:全局前置守卫是最常用的守卫之一。当一个导航触发时,全局前置守卫会按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫resolve
完之前一直处于等待中。
router.beforeEach(async (to, from) => {
if (
// 检查用户是否已登录
!isAuthenticated &&
// ❗️ 避免无限重定向
to.name !== 'Login'
) {
// 将用户重定向到登录页面
return { name: 'Login' }
}
})
参数:
to
: 即将跳转的路由from
: 当前导航正要离开的路由next(可选)
: 在之前的Vue Router
版本中,还可以使用 第三个参数next
。这是一个常见的错误来源,vue
官方经过RFC
讨论将其移除。然而,目前next
仍然可以使用,这意味着你可以向任何导航守卫传递第三个参数。在这种情况下,需要确保next
在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。
返回值:
false
: 取消当前的导航。如果浏览器的URL
改变了(可能是用户手动或者浏览器后退按钮),那么URL
地址会重置到from
路由对应的地址。- 一个路由地址:通过一个路由地址重定向到一个不同的地址,如同调用
router.push()
,且可以传入诸如replace: true
或name: 'home'
之类的选项。它会中断当前的导航,同时用相同的from
创建一个新导航。undefined
或者true
:导航是有效的,之后会按流程调用下一个导航守卫。
1.7.2 全局解析守卫
router.beforeResolve()
:与 router.beforeEach
类似,在每次导航时都会触发,不同的是,解析守卫刚好会在导航被确认之前、所有组件内守卫和异步路由组件被解析之后调用。
因此,router.beforeResolve
是获取数据或执行任何其他操作(如果用户无法进入页面时你希望避免执行的操作)的理想位置。
router.beforeResolve(async to => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch (error) {
if (error instanceof NotAllowedError) {
// ... 处理错误,然后取消导航
return false
} else {
// 意料之外的错误,取消导航并把错误传给全局处理器
throw error
}
}
}
})
1.7.3 全局后置钩子
router.afterEach()
:与前置守卫不同的是,全局后置钩子不会接受 next
函数也不会改变导航本身。
利用它可以完成分析、更改页面标题、声明页面等辅助功能。
router.afterEach((to, from) => {
sendToAnalytics(to.fullPath)
})
1.7.4 路由独享守卫
beforeEnter
:路由独享守卫是针对单个路由的守卫,可以定义在路由配置中。
beforeEnter
守卫 只在进入路由时触发,不会在 params
、query
或 hash
改变时触发。例如,从 /users/2
进入到 /users/3
或者从 /users/2#info
进入到 /users/2#projects
。它们只有在 从一个不同的 路由导航时,才会被触发。
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/protected',
beforeEnter: (to, from, next) => {
if (store.getters.isLoggedIn) {
next();
} else {
next('/login');
}
},
component: ProtectedComponent
}
]
});
1.7.5 组件内的守卫
组件级别的守卫可以定义在组件内,包括进入前守卫、更新前守卫和离开前守卫。
beforeRouteEnter
:在进入路由之前被调用。
beforeRouteUpdate
:在当前路由改变,但是该组件被复用时调用。
beforeRouteLeave
:在离开路由之前被调用。
<script>
export default {
beforeRouteEnter(to, from) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this` !
// 因为当守卫执行时,组件实例还没被创建!
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
},
}
</script>
1.8 动态路由
对路由的添加通常是通过 routes
选项来完成的,但是在某些情况下,可能需要在应用程序已经运行的时候添加或删除路由。
1.8.1 添加路由
假设目前只有一个路由:
const router = createRouter({
history: createWebHistory(),
routes: [{ path: '/:articleName', component: Article }],
})
此时,进入任何页面,例如/about
或 /article
,都会被匹配到 Article
页面。若此时利用 router.addroute
添加/about
路由:
router.addRoute({ path: '/about', component: About })
页面仍然会显示 Article
组件,我们需要手动调用 router.replace()
来改变当前的位置,并覆盖原来的位置:
router.addRoute({ path: '/about', component: About })
// 我们也可以使用 this.$route 或 useRoute()
router.replace(router.currentRoute.value.fullPath)
*注意:如果需要等待新的路由显示,可以使用 await router.replace()
。
如果需要在导航守卫内部添加或删除路由,不应该调用 router.replace()
,而是要通过返回新的位置来触发重定向:
router.beforeEach(to => {
if (!hasNecessaryRoute(to)) {
router.addRoute(generateRoute(to))
// 触发重定向
return to.fullPath
}
})
1.8.2 删除路由
- 通过添加一个名称冲突的路由。如果添加与现有途径名称相同的途径,会先删除路由,再添加路由:
router.addRoute({ path: '/about', name: 'about', component: About })
// 这将会删除之前已经添加的路由,因为他们具有相同的名字且名字必须是唯一的
router.addRoute({ path: '/other', name: 'about', component: Other })
- 通过调用 router.addRoute() 返回的回调:
const removeRoute = router.addRoute(routeRecord)
removeRoute() // 删除路由如果存在的话
- 通过使用 router.removeRoute() 按名称删除路由:
router.addRoute({ path: '/about', name: 'about', component: About })
// 删除路由
router.removeRoute('about')
*注意:当路由被删除时,所有的别名和子路由也会被同时删除
1.8.3 添加嵌套路由
要将嵌套路由添加到现有的路由中,可以将路由的 name 作为第一个参数传递给 router.addRoute()
router.addRoute({ name: 'admin', path: '/admin', component: Admin })
router.addRoute('admin', { path: 'settings', component: AdminSettings })
// 等效于
router.addRoute({
name: 'admin',
path: '/admin',
component: Admin,
children: [{ path: 'settings', component: AdminSettings }],
})
1.8.4 查看现有路由
router.hasRoute():检查路由是否存在。
router.getRoutes():获取一个包含所有路由记录的数组。
内容参考:
[1] vue router 4 官方教程