Vue Router 学习笔记-核心
# Vue Router 学习笔记-核心
Vue Router是Vue.js的官方路由,用于构建SPA(单页面应用) (opens new window)。
理解Vue Router从核心和扩展两部分入手,核心部分是Vue Router的最基础最核心功能,扩展部分是基于核心而扩展用于不同场景和需求的写法。
核心部分,是路由最小核心,是路由不可或缺的部分,分为三部分:
- 路由配置
- 导航函数
- 导航守卫
扩展部分,在基础的基础上进行了更多的扩展写法,适用于更复杂的场景。例如:
- 对路由配置进行扩展,加入了动态路由匹配、路由正则匹配等
- 引入嵌套路由、命名路由、命名视图、重定向和别名、路由组件传参等
- 历史记录模式分为hash模式和H5模式
- 路由元信息,可将任意附加信息添加到路由上
- 引入组合式API、过渡动效、滚动行为、路由懒加载、自定义RouterLink、导航故障、动态路由等
注,这里核心是指把路由拆解后的最小核心功能,扩展是基于最小核心添加适合不同场景和需求的一些额外写法(没有严格按照官方文档的基础与进阶来写)。
# 最小核心
# 路由配置
使用路由前需要对路由进行配置,即定义path、componet等,一般配置文件放在 /src/router/
目录下。
路由配置的最小单元需包含path和component,配置示例如下:
import Home from "Home.vue"
const routes = [
{
path: "/",
component: Home,
},
{
path: "/about",
component: () => import("@/page/about.vue"), // 路由懒加载写法,一般直接使用这种写法
},
];
下面是一个较为完整的路由配置文件示例,包含了路由配置、创建Router等
// 1. 创建路由配置
const routes = [
{ path: "/home", redirect: "/" },
{
path: "/",
component: () => import("@/page/Home.vue"),
},
{
path: "/a/:test",
name: "PageA",
props: true,
component: () => import("@/page/PageA.vue"),
beforeEnter: (to, from) => {
// ...
console.log("配置守卫 beforeEnter", to, from);
return true;
},
children: [
{
path: "a/sub/:userId",
name: "pageSub",
component: () => import("@/page/PageASub.vue"),
props: true,
},
],
},
];
// 2. 创建Router
const router = createRouter({
history: createWebHashHistory(),
routes,
});
// 3. 导出Router,让Vue使用
export default router;
路由配置好后,就可以使用<router-view>
组件进行显示,下面是一个示例:
<router-view v-slot="{ Component, route }">
<component :is="Component" :key="route.meta.name"></component>
</router-view>
# 导航函数
在HTML文件内,可以使用<router-link>
标签进行跳转,代码示例:
<div id="app">
<h1>Hello App!</h1>
<p>
<!--使用 router-link 组件进行导航 -->
<!--通过传递 `to` 来指定链接 -->
<!--`<router-link>` 将呈现一个带有正确 `href` 属性的 `<a>` 标签-->
<router-link to="/">Go to Home</router-link>
<router-link to="/about">Go to About</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
但是更多的时候是使用导航函数来实现跳转(编程式导航),常用的导航函数有:
- push,这个方法会向 history 栈添加一个新的记录,点击返回会返回到之前的URL
- replace,这个方法不会向history 栈添加新记录,其他基本与push相同
- go,这个方法表示横跨历史,正数表示前进,负数表示后退
注意点:
- 导航方法都是异步的,返回的结果是一个promise对象,该promise会在所有的导航守卫走完之后触发,表示导航的结果。
- **导航函数如果传入了path,则会忽略params。**因为path本身就是路径,params是用于动态路由中组成path的一部分(例如
path: "/a/:userId"
中的userId这个动态参数),所以path和params不能互相传。(params可通过this.$route.params
获取) <router-link>
to的参数类型为RouteLocationRaw
,与push函数的入参相同
# push
向 history 栈添加一个新的记录,点击返回会返回到之前的URL,需要注意方法是异步函数,返回对象时Promise对象,表示导航是否成功。
这里列几个官方的例子参考:
// 字符串路径
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' })
const username = 'eduardo'
// 我们可以手动建立 url,但我们必须自己处理编码
router.push(`/user/${username}`) // -> /user/eduardo
// 同样
router.push({ path: `/user/${username}` }) // -> /user/eduardo
// 如果可能的话,使用 `name` 和 `params` 从自动 URL 编码中获益
router.push({ name: 'user', params: { username } }) // -> /user/eduardo
// `params` 不能与 `path` 一起使用
router.push({ path: '/user', params: { username } }) // -> /user
push返回对象时Promise对象,如果导航报错会返回 NavigationFailureType
类型,无错会返回undefined
// 写法一,判断是否有错误
const navigationResult = await router.push('/my-profile')
if (navigationResult) {
// 导航被阻止
} else {
// 导航成功 (包括重新导航的情况)
this.isMenuOpen = false
}
// 写法二,检察错误是否属于某种类型
import { NavigationFailureType, isNavigationFailure } from 'vue-router'
// 试图离开未保存的编辑文本界面
const failure = await router.push('/articles/2')
// 判断错误是否是 NavigationFailureType.aborted 类型
if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
// 给用户显示一个小通知
showToast('You have unsaved changes, discard and leave anyway?')
}
# replace
使用基本与push相同,只是不会在history新增栈,而是取代当前栈。
此外,也可在push方法里新增 replace: true
参数来使push达到replace的效果
router.replace({ path: '/home' })
// 相当于
router.push({ path: '/home', replace: true })
# go
表示在历史堆栈中前进或后退多少部,相当于 window.history.go(n)
// 向前移动一条记录,与 router.forward() 相同
router.go(1)
// 返回一条记录,与 router.back() 相同
router.go(-1)
// 前进 3 条记录
router.go(3)
// 如果没有那么多记录,静默失败
router.go(-100)
router.go(100)
# 导航传参
query,添加到url上的query部分
params,是path的一部分,需要实现在路由上定义,不定义的参数不能传递,即不能随意传参。例
1. 路由定义:/users/:username/posts/:postId,params为:{ username: 'eduardo', postId: '123' } 2. 路由定义:/users/:username,params为:{ username: 'eduardo' } 3. 路由定义:/users/:username,如果 设置 params为:{ username: 'eduardo', postId: '123' },页面只能收到username参数,其他path未定义的参数都会忽略
如果需要传递复杂对象,可以通过Storage存储和读取
# 导航守卫
导航守卫用于通过跳转或取消的方式守卫导航,用于鉴权、修改标题等等需要在路由发生变化时做的操作。
# 介绍
导航守卫从作用类型上分为两类:
- 守卫函数,这类函数里可以修改导航行为,比如跳转到其他页面或取消等操作
- 钩子函数,这类函数只是导航行为到某个阶段的通知,不可以修改导航行为(只有一个钩子函数:全局后置钩子 afterEach)
注意点:
导航守卫函数都接受两个参数:
to
、from
,表示要进入的目标和要离开的目标,类型为RouteLocationNormalized (opens new window)守卫函数还接受第三个函数
next函数
,用于实现修改导航行为,但现在基本已经可以不用,直接在守卫函数内使用return语句
返回,即可实现修改、取消等行文,守卫函数返回的值返回类型为:- false,取消当前的导航
- 一个路由地址 (opens new window),即同push函数的传参一样,会中断当前的导航开始新的导航
- 无返回、undefined、true,表示当前导航有效,继续导航
钩子函数(只有一个钩子函数 afterEach)接受第三个参数failure (opens new window),表示导航结果
从级别角度分为三类:
- 全局守卫,包括:
- 全局前置守卫(beforeEach),可以对导航 做取消、重定向、判断是否登录等前置拦截操作
- 全局解析守卫(beforeResolve),最后一次可以取消导航的机会,一般用于权限申请等操作,该步骤过后,导航将被确认,是用于获取数据或执行任何其他操作(如果用户无法进入页面时你希望避免执行的操作)的理想位置。
- 全局后置钩子(afterEach),对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用
- 路由守卫,包括:
- 路由守卫(beforeEnter),路由守卫只会在进入路由时触发,不会在
params
、query
或hash
改变时触发,例如从/users/2
进入到/users/3
或者从/users/2#info
进入到/users/2#projects
时不会触发路由守卫,即只有进入一个不同的路由导航时,才会触发路由守卫。路由守卫与其他守卫不同的地方在于,可以给路由守卫传入一组函数,实现特定行为。
- 路由守卫(beforeEnter),路由守卫只会在进入路由时触发,不会在
- 组件守卫,包括:
- beforeRouteEnter,在渲染该组件的对应路由被验证前调用,不能获取组件实例
this
,因为当守卫执行时,组件实例还没被创建。需要注意的是,可给该守卫的next函数传入一个回调,当导航确认后会执行该回调,并传入该组件实例 - beforeRouteUpdate,组件被复用时触发
- beforeRouteLeave,导航在离开该组件时触发
- beforeRouteEnter,在渲染该组件的对应路由被验证前调用,不能获取组件实例
一些代码示例:
// 全局前置守卫 beforeEach
const router = createRouter({ ... })
router.beforeEach((to, from) => {
// ...
// 返回 false 以取消导航
return false
})
// 全局解析守卫 beforeResolve
router.beforeResolve(async to => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch (error) {
if (error instanceof NotAllowedError) {
// ... 处理错误,然后取消导航
return false
} else {
// 意料之外的错误,取消导航并把错误传给全局处理器
throw error
}
}
}
})
// 全局钩子函数 afterEach
router.afterEach((to, from) => {
sendToAnalytics(to.fullPath)
})
// 路由守卫 beforeEnter
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]
// 路由守卫可以接受一组函数,"这在为不同的路由重用守卫时很有用"
function removeQueryParams(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: UserDetails,
beforeEnter: [removeQueryParams, removeHash],
},
{
path: '/about',
component: UserDetails,
beforeEnter: [removeQueryParams],
},
]
// 组件守卫
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this` !
// 因为当守卫执行时,组件实例还没被创建!
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
},
}
// 可给 beforeRouteEnter 的next函数传入一个回调函数,当导航确认后会执行该回调,并传入该组件实例
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
# 组件守卫组合式API
组件守卫在组合式API的写法需要特殊注意下(全局守卫和路由守卫与组合式API相同)。
Vue Router提供
onBeforeRouteLeave
,onBeforeRouteUpdate
两个组合式API函数import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router' import { ref } from 'vue' export default { setup() { // 与 beforeRouteLeave 相同,无法访问 `this` onBeforeRouteLeave((to, from) => { const answer = window.confirm( 'Do you really want to leave? you have unsaved changes!' ) // 取消导航并停留在同一页面上 if (!answer) return false }) const userData = ref() // 与 beforeRouteUpdate 相同,无法访问 `this` onBeforeRouteUpdate(async (to, from) => { //仅当 id 更改时才获取用户,例如仅 query 或 hash 值已更改 if (to.params.id !== from.params.id) { userData.value = await fetchUser(to.params.id) } }) }, }
beforeRouteEnter
的写法有点特殊,因为在执行beforeRouteEnter
时,组件还没被创立,即该函数的执行在setup
函数之前,所以不能写在setup
函数之内// 写法一,使用组合式API export default defineComponent({ components: {}, props: {}, // This needs to be defined outside the setup block beforeRouteEnter(to, from, next) { next(vm => { // vm表示组件的实例,可以在这里对vm内属性进行赋值等操作 console.log(vm); }) }, setup() { onBeforeRouteUpdate(to => { }); } }); // 写法二,setup标签写法,在增加一个script,不过需要注意lang要相同 <script> export default { beforeRouteEnter: (to, from, next) => { next((vm) => { // vm表示组件的实例,可以在这里对vm内属性进行赋值等操作 console.log(vm); }); return true; }, }; </script> <script setup> import { useRouter, useRoute } from "vue-router"; </script> //写法三,setup标签TS的写法 <script lang="ts"> import { defineComponent, ComponentPublicInstance } from 'vue' interface IInstance extends ComponentPublicInstance { setPathFrom(from: string): void } export default defineComponent({ beforeRouteEnter(to, from, next) { next((vm) => { const instance = vm as IInstance instance.setPathFrom(from.path) }) }, }) </script> <script lang="ts" setup> let pathFrom: string const setPathFrom = (path: string) => { pathFrom = path console.log('vue-route::from::', pathFrom) } defineExpose({ setPathFrom }) </script> // 注意点,在 beforeRouteEnter 内也可以利用 路由的props特性,给组件的props赋值 <script> import { useFetch } from '@vueuse/core' export default { beforeRouteEnter: function (to) { const { data } = useFetch(`/your/url/${to.params.id}`) // 赋值给 meta,最终利用路由的props配置给组件的props赋值 to.meta.data = data } } </script> <script setup> defineProps({ data: { type: [null, Object], required: true } }) // ... </script> // 组件内写法 { path: '/your/url/:id', // 利用 meta,赋值给props props: to => ({ data: to.meta.data }) }
# 导航解析流程
完整的导航解析流程:
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。- 调用全局的
beforeEach
守卫。- 在重用的组件里调用
beforeRouteUpdate
守卫(2.2+)。- 在路由配置里调用
beforeEnter
。- 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。- 调用全局的
beforeResolve
守卫(2.5+)。- 导航被确认。
- 调用全局的
afterEach
钩子。- 触发 DOM 更新。
- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入。
以上是Vue Router文档路由导航解析的流程。
简单来说,执行顺序上 全局守卫 >(早于) 路由守卫 >(早于) 组件守卫。
需要注意的是,导航函数(push、replace、go等)的回调会在最后执行,即在第12步以后执行。
router.push({ name: "PageC" }).then((res) => {
// 导航函数的回调,在最后执行,如果导航报错会返回 `NavigationFailureType` 类型,无错会返回`undefined`,错误类型可以用 isNavigationFailure 函数判断
console.log("res", res);
});
VueRouter完整导航解析流程图示:
注:
当组件重用时,不会触发路由配置守卫,且组件守卫
beforeRouteUpdate
会在全局前置守卫之后就触发,守卫触发流程如下:1. 全局前置守卫 router.beforeEach 2. 组件守卫 beforeRouteUpdate 3. 全局解析守卫 router.beforeResolve 4. 全局后置钩子 router.afterEach