Vue Router 的导航守卫是一组函数,用于在路由发生变化时控制路由的导航。
这些函数可以在导航到某个路由之前、之后或是取消导航时执行特定的逻辑。
导航守卫是 Vue Router 提供的重要特性,用于实现诸如路由鉴权、路由切换动画、数据预取等功能。
前置守卫
前置守卫(beforeEach)是Vue Router提供的一种导航守卫,它可以在路由切换之前进行一些操作,例如验证用户的登录状态、权限控制、数据预加载等。前置守卫的应用场景如下:
-
登录验证:可以使用前置守卫来验证用户是否已经登录。例如,如果用户未登录,则导航到登录页面,否则允许继续访问。
-
权限控制:前置守卫可以用于实现基于用户权限的路由访问控制。在跳转某个需要特定权限的路由时,可以通过前置守卫检查用户是否具有足够的权限。
-
数据预加载:有时,我们希望在访问某个路由之前加载一些数据,以确保页面渲染时具备所需的数据。前置守卫可以用来在路由切换之前触发数据加载的逻辑。
-
路由跳转拦截:前置守卫还可以用于捕获导航行为并进行相应的操作。例如,在某些特定情况下,我们希望阻止路由的切换或者进行一些额外的处理。
通过使用前置守卫,我们可以在路由切换之前进行一些必要的操作,以实现更好的用户体验和控制。
参数:
to
: 即将要进入的目标的 routes 对象from
: 当前当好正要离开的 routes 对象next
: 即 next() 放行函数
next() 放行函数的作用是决定是否继续进行导航或中断导航,并可以指定要导航到的目标。next()
函数可以接受不同的参数,决定了导航的行为:
- 不传任何参数:表示继续进行导航,进入到下一个导航钩子。如果没有后续导航钩子,导航将会被确认。
- 传递一个
false
参数:中断当前导航。不会进入下一个导航钩子,导航将被中断。 - 传递一个路径或命名的路由对象:会立即进行新的导航,当前导航被中断。
- 传递一个带有
replace: true
选项的路由对象:导航将替换当前的路由历史记录条目而不是添加新的。当前导航被中断。 - 传递一个回调函数:允许在导航确认之前异步处理。回调函数会被调用,并接收一个
next()
的回调函数作为参数,用于确认导航。
main.ts
import { createApp } from 'vue'
import App from './App.vue'
import {router} from './router'
// import 引入
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(router)
// use 注入 ElementPlus 插件
app.use(ElementPlus)
const whiteList = ['/']
// beforeEach 可以定义不止一个,vue会收集所有定义的路由钩子,所以next的作用不应该是跳转,而是使步骤进行到下一个你定义的钩子
router.beforeEach((to, from, next) => {
// token每次都要跟后端校验一下是否过期
if(whiteList.includes(to.path) || localStorage.getItem('token')){
next()
}else{
next('/')
}
})
app.mount('#app')
index.ts
import { createRouter, createWebHistory } from 'vue-router'
export const router = createRouter({
// import.meta.env.BASE_URL 应用的基本 URL。基本 URL 是指在你的应用部署到某个域名或子路径时,URL 的起始部分。例如,如果你的应用部署在 https://example.com/myapp/ 这个路径下,那么 import.meta.env.BASE_URL 就会是 /myapp/。
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: () => import('@/views/Login.vue'),
},
{
path: '/index',
component: () => import('@/views/Index.vue'),
},
],
})
App.vue
<template>
<router-view></router-view>
</template>
<script setup lang="ts">
</script>
<style>
/* 注意 style 标签 别加 scoped 不然设置宽高不生效 */
* {
margin: 0;
padding: 0;
}
html, body, #app {
height: 100%;
overflow: hidden;
}
</style>
Login.vue
<template>
<div class="login">
<el-card class="box-card">
<el-form ref="form" :rules="rules" :model="formInline" class="demo-form-inline">
<el-form-item prop="user" label="账号:">
<el-input v-model="formInline.user" placeholder="请输入账号" />
</el-form-item>
<el-form-item prop="password" label="密码:">
<el-input v-model="formInline.password" placeholder="请输入密码" type="password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import type { FormItemRule, FormInstance } from 'element-plus';
import { ElMessage } from 'element-plus'
const router = useRouter()
type Form = {
user: string,
password: string
}
type Rules = {
[k in keyof Form]?: Array<FormItemRule>
}
const formInline = reactive<Form>({
user: '',
password: '',
})
const form = ref<FormInstance>()
const rules = reactive({
user: [
{
required: true,
message: '请输入账号',
type: 'string',
}
],
password: [
{
required: true,
message: '请输入密码',
type: 'string',
}
]
})
const onSubmit = () => {
console.log('submit!', form.value)
form.value?.validate((validate)=>{
if (validate) {
router.push('/index')
localStorage.setItem('token', '1')
} else {
ElMessage.error('账号或密码错误')
}
})
}
</script>
<style scoped lang="less">
.login {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
</style>
刚开始未登录过,不能使用输入 /index
的方式跳转,localStorage
不能读取 token
。
登录之后可以使用输入 /index
的方式跳转,localStorage
已存储 token
。
后置守卫
注册全局后置钩子,和前置守卫不同,这些钩子不会接受 next 函数,也不会改变导航本身。
例如:添加一个加载进度条功能
loadingBar.vue
<template>
<div class="wraps">
<div ref="bar" class="bar"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
let speed = ref<number>(1)
let bar = ref<HTMLElement>()
let timer = ref<number>(0)
const startLoading = () => {
speed.value = 1
let dom = bar.value as HTMLElement
timer.value = window.requestAnimationFrame(function fn() {
if (speed.value < 90) {
speed.value += 1;
dom.style.width = speed.value + '%'
timer.value = window.requestAnimationFrame(fn)
} else {
speed.value = 1
window.cancelAnimationFrame(timer.value)
}
})
}
const endLoading = () => {
let dom = bar.value as HTMLElement
setTimeout(() => {
window.requestAnimationFrame(() => {
speed.value = 100
dom.style.width = speed.value + '%'
})
}, 500)
}
defineExpose({ startLoading, endLoading })
</script>
<style scoped lang="less">
.wraps {
width: 100%;
position: fixed;
height: 10px;
top: 0;
.bar {
height: inherit;
width: 0;
background-color: #409eff;
}
}
</style>
main.ts
import { createApp,createVNode,render } from 'vue'
import App from './App.vue'
import {router} from './router'
// import 引入
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import loadingBar from './components/loadingBar.vue'
const Vnode = createVNode(loadingBar)
render(Vnode,document.body)
const app = createApp(App)
app.use(router)
// use 注入 ElementPlus 插件
app.use(ElementPlus)
const whiteList = ['/']
// beforeEach 可以定义不止一个,vue会收集所有定义的路由钩子,所以next的作用不应该是跳转,而是使步骤进行到下一个你定义的钩子
router.beforeEach((to, from, next) => {
Vnode.component?.exposed?.startLoading()
// token每次都要跟后端校验一下是否过期
if(whiteList.includes(to.path) || localStorage.getItem('token')){
next()
}else{
next('/')
}
})
router.afterEach((to, from) => {
Vnode.component?.exposed?.endLoading()
})
app.mount('#app')