基本的框架:
import { createRouter, createWebHashHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
vue-router
采用了 path -> component
的方式,我们只需要修改上述代码的 routes
,添加类似下面数据即可:
// () => import('xxx') 表示路由拉加载,即访问这个路由才会加载这个资源
const routes: RouteRecordRaw[] = [
{ path: '/', component: Home },
{ path: '/about', component: () => import('../components/About.vue') }
]
最后在 main.ts
中使用:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
顾名思义,createRouter
即创造路由,语法如下:
createRouter(options: RouterOptions)
其中 options
中必须传递两个参数:
history
:路由模式,有两种模式可选,一个是 createWebHistory()
,另一个是 createWebHashHistory
routes
:路由参数,即配置的 path -> component
对象数组两者均是路由模式
createWebHashHistory
:在路由后使用了哈希字符 #
(例如学校的远程实验平台 http://www.ycsypt.com/#/
),这样 URL 处理不需要在服务器层面上进行,虽然简单,但是对 SEO 有不好的影响createWebHistory
:正常的路由地址,也是 vue 官网推荐的模式,不过 vue 作为一个单页的客户端渲染框架,在 dev 环境下测试是正常的,但是在部署环境时会发生错误,因为这种正常的路由一般交给服务器处理,我们需要手动配置 nginx
才可以
createWebHistory
配置:不同的历史模式 | Vue Router (vuejs.org)
vue-router
提供了两个标签:
router-link
router-view
vue-router
没有使用常规的 a
标签,而是使用自定义组件 router-link
。这是由于 a
标签跳转会重新加载页面,向服务端请求数据,这样就失去了 vue
对页面的控制。router-link
允许在不重新加载页面的前提下更改 URL
router-view
切换路由时需要展示的位置,可以放在任何地方
const routes: RouteRecordRaw[] = [
{ path: 'a', component: A },
{ path: 'b', component: B }
]
App.vue
我们不一定要配置 path: ‘/’
对应的组件,因为这个路径默认对应的组件就是 App.vue
<template>
<div>
<h1>App 组件</h1>
<RouterLink to='a'>a</RouterLink>
<RouterLink to='b'>b</RouterLink>
<RouterView />
</div>
</template>
router-link
组件中的to
属性表示要跳转的链接
a.vue
<template>
<h2>A 组件</h2>
</template>
b.vue
同理
有时候我们会访问类似 /user/${id}
来查看不同 id 的用户,我们不可能把用户所有 id 遍历后写到路由配置中,为此 vue-router
提供了动态路由,使用 :id
来区分不同路由
const routes: RouteRecordRaw[] = {
{ path: '/user/:id', component: User}
}
当我们访问 user/1
时,组件该如何获取到 id 呢?
vue-router
提供了$route
参数,我们可以直接在 <template>
模板上调用:
<template>
<h2>id: {{ $route.params.id }}</h2>
</template>
当然也可以在 <script>
标签中使用
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.params.id)
</script>
vue-router
官网给出的示例:
匹配模式 | 匹配路径 | $route.params |
---|---|---|
/users/:username | /users/eduardo | { username: 'eduardo' } |
/users/:username/posts/:postId | /users/eduardo/posts/123 | { username: 'eduardo', postId: '123' } |
$route/route
除了 params
属性,其实还有 query
(路由查询参数) 和 hash
(hash参数) 属性等
动态路由在切换时,例如从
/user/1
到/user/2
,相同的组件实例将被重复使用,因为两个路由都渲染同一个组件,比起销毁再创建,复用则更高效,这也意味着:组件的某些生命周期钩子不会被调用(update会调用)
在上述的讲解中,我们已经知道了 :
匹配运算符,事实上,vue-router
内部基于正则表达式处理,还有这更多的匹配运算符:
const routes = [
// 表示 /:orderId 只匹配数字
{ path: '/:orderId(\\d+)' },
// /:productName 匹配任何内容
{ path: '/:productName' }
]
当我们访问 /25
时,对应的路由地址时 /:orderId
,其他情况则对应的是 /:productName
如果需要匹配多个部分的路由,例如 /2003/03/03
,可以使用以下两个运算符:
*
:匹配 0 个或多个+
:匹配 1 个或多个const route = [
{ path: '/params/:paramsId*', component: Prams }
]
当我们访问 /params/2003/03/03
时,打印的 $route.params
为数组:
import { useRoute } from 'vue-router'
const route = useRoute()
cosnole.log(route.params)
// paramsId: ['2003', '03', '03']
可以通过使用 ?
修饰符将一个参数标记为可选
const routes = [
{ path: '/users/:userId?' }
]
默认情况下,vue-router
配置的路由地址是不敏感且不区分大小写的,例如 path: ‘/user’
会匹配 /users
、/users/
、/User
,我们可以通过修改 createRouter
的参数来修改,可以全局设置,也可以设置在某个单独路由:
const router = createRouter({
history: createWebHistory(),
routes: [
// 单独配置 sensitive
{ path: '/user/:id', sensitive: true }
],
// 全局配置 strict
strict: true
})
前面讲到过 <RouteView>
可以使用多次,而使用多次的场景便是嵌套路由,即子路由
/
展示的组件// App.vue
<template>
<div>
<h1>App 组件</h1>
<RouterView />
</div>
</template>
/sons
展示的组件// Sons.vue
<template>
<div>
<h2>Sons 组件</h2>
<RouterView />
</div>
</template>
/sons/:id(\\d+)
展示的组件// Son.vue
<template>
<div>
<h3>我是 {{$route.params.id}} 儿子</h3>
</div>
</template>
配置 router
配置
const routes = [
{
path: '/sons',
component: Sons,
// 使用 children 配置子路由
children: [
{ path: '/sons/id(\\d+)', component: Son }
]
}
]
当我们访问 /sons/4
显示的网页效果:
命名路由很容易理解,即给每一个路由添加 name
属性:
const routes = [
{
path: '/user',
name: 'user',
component: User
}
]
这有一个好处,即当我们使用 <router-link>
跳转时,不需要写 url
地址,可以写一个配置对象:
<RouterLink :to="{name: 'user'}">去 user 组件</RouterLink>
也可以使用编程式导航(下节内容):
import { useRouter } from 'vue-router'
const router = useRouter()
router.push({
name: 'user'
})
以下示例均基于以下路由配置:
const routes = [
{ path: '/user/:username', component: User }
]
上述代码中,我们只通过 <router-link>
标签进行路由切换,vue-router
提供了 js 中的路由切换功能:
import { useRouter } from 'vue-router'
const router = useRouter()
router.push('/user')
router.push
可以接受一个字符串路径,也可以接受一个描述地址的对象,例如:
// 字符串路径
router.push('/user/2')
// 带有路径的对象
router.push({ path: '/user/2' })
// 命名路由,并加上 params 参数
router.push({ name: 'user', params: {
username: 'xjj'
} })
// 携带查询参数
router.push({ path: '/user', query: {
password: 'xxx'
} })
// 携带 hash(通常是目录定位)
// 结果是 /user#age
router.push({ path: '/user', hash: '#age' })
注意
path
不能与params
一起使用,最后解析的仍然是path
<router-link>
中的 to
属性与 router.push
完全相同,因此不再讲解
有时候我们希望两个视图在同一层,例如 2 个侧导航栏 + 1 个主内容,我们可以采用以下写法:
<RouterView name="LeftSideBar" />
<RouterView />
<RouterView name="RightSidebar"
配置路由:
注意,是配置
components
属性,而不是component
属性
const routes = [
{
path: '/',
components: {
default: Home,
// 下面两个都是组件,简写形式
LeftSidebar,
RightSidebar
}
}
]
这样,当我们访问 /
是,展示的组件有 Home
、LeftSidebar
、RightSidebar
重定向可以通过 routes
完成:
const routes = [
// 从 `/` 重定向到 `/home`
{ path: '/', redirect: '/home' }
]
重定向的目标也可以是命名路由:
{ path: '/', redirect: { name: 'home' } }
也可以是一个方法:
{
path: '/',
redirect: to => {
return { path: '/home', query: { q: to.params.pathText } }
}
}
在写
redirect
属性时,可以省略component
配置,因为跳转后本身访问不到这个路由例外:如果一个路由有
children
和redirect
属性,那么也应该有component
属性**
相对重定向是以当前目录为准,例如下面代码:
相比于上面代码,这里的写法省去了
/
/users/2/posts
时,会被重定向到 /user/2/profile
没成功,不知道具体什么原因
const routes = [
{
path: '/users/:id/posts',
redirect: to => {
return 'profile' // 或者 { path: 'profile' }
}
}
]
这种效果在 <router-link>
也适用:
/users/2/posts
时,点击以下标签,页会跳转到 users/2/profile
<router-link to="profile">去个人首页</router-link>
我们可以通过使用 $route
将组件与页面结合,但是这样就限制了组件的灵活性,因为这只能应用于特定的 URL,这不是坏事,但是 vue-router
额外提供了 props
传参:
// 将 props 属性设置为 true
const routes = [
{ path: '/user/:id', component: User, props: true }
]
组件中使用 defineProps
定义动态路由的参数:
// User.vue
<script setup>
defineProps<{
id: string
}>()
</script>
<template>
<div>
{{id}}
</div>
</template>
路由导航守卫可以让我们控制路由之间的跳转,并实现一些功能
例如有些路由需要登录才可以访问,而有些不可以,此时我们可以使用 router.beforeEach
注册一个全局前置守卫:
const router = createRouter({ /**/ })
router.beforeEach((to, form) => {
// 返回 false 即取消路由跳转
return false
})
当路由切换时,全局前置守卫先被调用,此方法是个异步方法,这意味着当守卫 resolve
完成之前,一直是 pending
(等待中) 状态
beforeEach
方法接收三个参数:
to
:表示要导向的路由from
:表示由哪个路由导向next
:这是一个可选参数,我们也可以使用它来表示路由是否可以跳转或跳转到哪里可以返回的值:
false
:取消这次路由切换path(一个路由地址)
:表示跳转的路由地址,例如我们未登陆成功,返回到登录页:router.beforeEach((to, from) => {
if(!isAuthenticated && to.name !== 'Login') {
return { name: 'Login' }
}
})
undefined | true
:表示这次导航是有效的,调用下一个导航守卫可选的
next
参数是一个函数,接收的参数与beforeEach
守卫的返回值一样,例如:Typscriptreturn '/login' // 上下两者效果相等 next('/login')
router.beforeResolve
和 router.beforeEach
比较类似,也是在每次路由切换时触发,不过解析守卫会在导航确认之前、所有组件内守卫和异步路由组件被解析之后调用
我们可以使用 beforeResolve
全局解析钩子访问自定义的 meta
属性,例如官方给的例子:
router.beforeResolve(async (to) => {
if(to.meta.requireCamera) {
try {
await askForCameraPermission()
} catch(error) {
if(error instanceof NotAllowedError) {
// ...处理错误
return false // 取消导航
} else {
// 意料之外的错误,取消当行并传递给全局处理器
throw error
}
}
}
})
同时
router.beforeResolve
是获取数据或执行任何其他操作的理想位置
钩子和守卫不同,钩子并不会影响路由的切换
全局后置可用于分析、更改页面标题、声明页面
router.afterEach((to, from) => {
sendToAnalytics(to.fullPath)
})
也可以接收 navigation failures 作为第三个参数:
router.afterEach((to, from, failure) => {
if (!failure) sendToAnalytics(to.fullPath)
})
可以直接在路由配置上定义 beforeEnter
守卫
const routes = [
{
path: '/users/:id',
component: User,
beforeEnter: (to, from) => {
// reject he navigation
return false
}
}
]
beforeEnter
只在路由切换后触发,例如从 /users/2
到 /users/3
,/users/2#one
到 /users/2#two
同时,beforeEnter
还支持接收一个数组,数组项为函数:
function removeQueryPrams(to) {
if(Object.keys(to.query).length)
return { path: to.path, query: {}, hash: to.hash }
}
function removeHash(to) {
if(to.hash) return {
path: to.path,
query: to.query,
hash: ''
}
}
const routes = [
{
path: '/users/:id',
component: User,
beforeEnter: [removeQueryParams, removeHash]
}
]
我们可以在 .vue
组件内定义路由导航守卫,可用的路由组件有一下 3 个:
onBeforeRouteUpdate
onBeforeRouteLeave
<script>
onBeforeRouteUpdate((to, from) => {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
})
onBeforeRouteLeave((to, from) => {
// 在导航离开渲染该组件的对应路由时调用
// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
})
</script>
如果我们希望在路由切换的时候,能给用户动画提示,可以使用 vue
提供了 transition
API 和 vue-router
提供的 router-view
API:
<RouterView v-slot="{ Component }">
<transition name="fade">
<component :is="Componet" />
</transition>
</RouterView>
不过目前
vue
的更新想法是未来去掉is
用法,另外吐槽一点vue-router
的文档真的好老
如果我们希望单个路由有过渡效果或者路由之间有不同的过渡效果,可以将路由元信息和 transition
的 name
结合在一起:
const routes = [
{
path: '/left',
component: Left,
meta: { transition: 'slide-left' }
},
{
path: '/right',
component: Right,
meta: { transition: 'slide-right' }
}
]
此时使用 <router-view>
标签的 v-slot
属性解构出 route
即可:
<route-view v-slot="{ Component, route }">
<transition :name="route.meta.transition || 'fade'">
<component :is="Component" />
</transition>
</route-view>
手动指定往左滑动还是往右滑动太过复杂了,此时我们可以指定路由的深度,通过 afterEach
全局后置钩子比较路由的深度来判断是路由的层级:
判断路由的层级
router.afterEach((to, from) => {
// 获取前往路由的数组长度(以 '/' 分隔)
const toDepth = to.path.split('/').length
// 同上
const fromDepth = from.path.split('/').length
to.meta.transition = toDepth < fromDepth ? 'slide-right' : 'slide-left'
})
vue
会自动复用一些组件,从而忽略过渡,我们可以添加,key
属性强制进行过度:
<router-view v-slot="{ Component, route }">
<transition name="fade">
<component :is="Component" :key="route.path" />
</transition>
</router-view>
当我们切换路由时,想要滚动到页面顶部,或者是保留原先的滚动位置时,我们可以配置 createRouter
的 scrollBehavior
属性
注意:这个功能只支持在 history.pushState 的浏览器中可用
const router = createRouter({
history: createWebHashHistory(),
routes,
scrollBehavior: (to, from, savedPosition) {
return /* 位置 */
}
})
scrollBehavior(to, from, savedPosition) {
return { top: 0 }
}
scrollBehavior(to, from, savedPosition) {
return {
el: '#main',
top: -10
}
}
scrollBehavior(to, from, savedPosition) {
if(savedPosition) {
return savedPosition
}
return {
top: 0
}
}
scrollBehavior(to, from, savedPosition) {
if(to.hash) {
return {
el: to.hash
}
}
}
如果浏览器支持滚动行为,那么滚动可以更加流畅
scrollBehavior(to, from, savedPosition) {
if(to.hash) {
return {
el: to.hash,
behavior: 'smooth'
}
}
}
为了做到延迟滚动,我们需要返回一个 Promise
对象
scrollBehavior(to, from, savedPosition) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ left: 0, top: 0 })
}, 500)
})
}
想象一个场景,当安卓手机左滑出菜单栏后,点击菜单跳转,当我们进入对应路由时,希望菜单栏可以自动隐藏,你可能想这么做:
router.push('/my-profile')
closeMenu()
但这样做会在到达对应路由之前立马关闭菜单,因为导航是异步的,我们需要 await
处理:
await router.push('/my-profile')
closeMenu()
但是路由可能会跳转失败,这时候我们就不希望关闭菜单栏,因此我们需要一种方法来检测我们是否真的跳转到别的路由
如果路由切换被阻止,导致用户停留在一个页面上,由 router.push
返回的 Promise
的值将解析为 Navigation Failure,否则它将是一个 falsy(假值,通常是 undefined)
,这样我们就能判断路由是否切换成功了:
const navigationResult = await router.push('/my-profile')
if(navigationResult) {
// 导航被阻止
} else {
// 导航成功,关闭菜单栏
closeMenu()
}
上述章节中我们讲述了:路由切换失败后,router.push
返回的 Promise
值将被解析成 Navigation Failure 值,这个值是带有一些额外属性的 Error
实例,可以提供更多的信息,例如哪些导航被阻止了以及为什么被阻止,要检查导航结果的性质,需要使用 isNavigationFailure
函数:
import { NavigationFailureType, isNavigationFailure } from 'vue-router'
// 试图离开未保存的编辑文本界面
const failure = await router.push('/articles/2')
if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
// 给用户显示一个小通知
showToast('You have unsaved changes, discard and leave anyway?')
}
TIP
如果忽略第二个参数:
isNavigationFailure(failure)
,那么就只会检查这个failure
是不是一个 Navigation Failure。
NavigationFailureType
有三种不同类型:
aborted
:在导航守卫中返回 false
中断了本次导航。cancelled
: 在当前导航还没有完成之前又有了一个新的导航。比如,在等待导航守卫的过程中又调用了 router.push
。duplicated
:导航被阻止,因为我们已经在目标位置了。例如当导航守卫返回一个新的位置时,我们会触发一个新的导航,覆盖之前的导航,重定向不会阻止导航,而是创建一个新的导航,可以通过读取路由中的 redirectedFrom
属性,对其进行不同的检查:
await router.push('/my-profile')
if(router.currentRoute.value.redirectedFrom) {
// redirectedFrom 是解析出的路由地址,就像导航守卫中的 to 和 from
}