【Vue-Router】导航守卫

文章详细介绍了VueRouter的导航守卫,特别是前置守卫(beforeEach)在实现登录验证、权限控制、数据预加载和路由拦截中的应用,以及后置守卫(afterEach)如加载进度条示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Vue Router 的导航守卫是一组函数,用于在路由发生变化时控制路由的导航。

这些函数可以在导航到某个路由之前、之后或是取消导航时执行特定的逻辑。

应用场景有 诸如路由鉴权、路由切换动画、数据预取等功能。

全局前置守卫

在路由切换之前进行一些操作,例如验证用户的登录状态、权限控制、数据预加载等。前置守卫的应用场景如下:

  1. 登录验证:可以使用前置守卫来验证用户是否已经登录。例如,如果用户未登录,则导航到登录页面,否则允许继续访问。

  2. 权限控制:前置守卫可以用于实现基于用户权限的路由访问控制。在跳转某个需要特定权限的路由时,可以通过前置守卫检查用户是否具有足够的权限。

  3. 数据预加载:有时,我们希望在访问某个路由之前加载一些数据,以确保页面渲染时具备所需的数据。前置守卫可以用来在路由切换之前触发数据加载的逻辑。

  4. 路由跳转拦截:前置守卫还可以用于捕获导航行为并进行相应的操作。例如,在某些特定情况下,我们希望阻止路由的切换或者进行一些额外的处理。

参数:

  • to : 即将要进入的目标的 routes 对象

    在这里插入图片描述

  • from : 当前当好正要离开的 routes 对象
  • next : 即 next() 放行函数

next() 放行函数的作用是决定是否继续进行导航或中断导航,并可以指定要导航到的目标。next()函数可以接受不同的参数,决定了导航的行为:

  1. 不传任何参数:表示继续进行导航,进入到下一个导航钩子。如果没有后续导航钩子,导航将会被确认。
  2. 传递一个false参数:中断当前导航。不会进入下一个导航钩子,导航将被中断。
  3. 传递一个路径或命名的路由对象:会立即进行新的导航,当前导航被中断。
  4. 传递一个带有replace: true选项的路由对象:导航将替换当前的路由历史记录条目而不是添加新的。当前导航被中断。
  5. 传递一个回调函数:允许在导航确认之前异步处理。回调函数会被调用,并接收一个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

在这里插入图片描述

全局解析守卫

它在每次导航时都会触发,不同的是,解析守卫刚好会在导航被确认之前、所有组件内守卫和异步路由组件被解析之后调用。

这里有一个例子,根据路由在元信息中的 requiresCamera 属性确保用户访问摄像头的权限:

router.beforeResolve(async to => {
  if (to.meta.requiresCamera) {
    try {
      await askForCameraPermission()
    } catch (error) {
      if (error instanceof NotAllowedError) {
        // ... 处理错误,然后取消导航
        return false
      } else {
        // 意料之外的错误,取消导航并把错误传给全局处理器
        throw error
      }
    }
  }
})

全局后置钩子

注册全局后置钩子,和前置守卫不同,这些钩子不会接受 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, failure) => {
  Vnode.component?.exposed?.endLoading()
})
app.mount('#app')

在这里插入图片描述
在这里插入图片描述

全局注入

在 app.provide() 中提供的所有内容都可以在 router.beforeEach()、router.beforeResolve()、router.afterEach() 内获取:

// main.ts
const app = createApp(App)
app.provide('global', 'hello injections')

// router.ts or main.ts
router.beforeEach((to, from) => {
  const global = inject('global') // 'hello injections'
  // a pinia store
  const userStore = useAuthStore()
  // ...
})

路由独享的守卫

直接在路由配置上定义 beforeEnter 守卫。beforeEnter 守卫 只在进入路由时触发,不会在 params、query 或 hash 改变时触发。

它们只有在 从一个不同的 路由导航时,才会被触发。而且如果有嵌套路由,beforeEnter 放在父路由上,那么子路由改变不会引发 beforeEnter 守卫。

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: (to, from) => {
      // reject the navigation
      // return false
    // },
    beforeEnter: [removeQueryParams, removeHash],
  },
  {
    path: '/about',
    component: UserDetails,
    beforeEnter: [removeQueryParams],
  },
]

组件内的守卫

<script>
export default {
  beforeRouteEnter(to, from) {
    // 在渲染该组件的对应路由被验证前调用
    // 不能获取组件实例 `this` !
    // 因为当守卫执行时,组件实例还没被创建!
  },
  beforeRouteUpdate(to, from) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
    // 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
  },
  beforeRouteLeave(to, from) {
    // 在导航离开渲染该组件的对应路由时调用
    // 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
  },
}
</script>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秀秀_heo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值