Skip to content

导航守卫

所谓导航守卫,就是在当你进行导航的时候将其拦截下来,从而方便做一些其他的事情。

快速上手

js
// 全局导航守卫
router.beforeEach((to, from, next) => {
  // 回调函数里面决定了拦截下来后做什么
  console.log("from:", from);
  console.log("to:", to);
  console.log("导航到:", to.name);
  next(); // 调用该方法代表放行
});

这是一个全局导航守卫,回调会自动传入 3 个参数:

  • to:即将要进入的目标路由,是一个对象,对象里面有 path、fullPath、hash、params 等参数
  • from:当前导航正要离开的路由,同样是一个对象,对象内部有上述参数
  • next:是一个函数,表示导航放行

各种拦截守卫

整体分为 3 大类:

全局守卫

  • beforeEach:全局前置守卫,会在解析组件守卫和异步路由组件之前被调用

  • beforeResolve:全局解析守卫,在导航被确认之前,但在所有组件内守卫和异步路由组件被解析之后调用

    • 上面两个其实就是执行的时机一前一后
  • afterEach:全局后置守卫,在导航确认后触发的钩子函数。该钩子函数执行后会触发 DOM 更新,用户看到新的页面。

    • 思考 🤔:既然导航都已经确认了,这里安插一个守卫干嘛呢?
    • 全局后置守卫经常用于如下的场景:
      1. 记录页面访问历史:可以使用 afterEach 来记录用户访问的页面,以便进行统计或分析。
      2. 关闭加载指示器:在 beforeEach 中开启加载指示器,在 afterEach 中关闭它,以提供更好的用户体验。
      3. 页面切换动画:在 afterEach 中触发页面切换动画或其他视觉效果,以提升用户体验。
      4. 更新文档标题:在导航完成后更新页面标题,以反映当前页面内容。
    js
    //全局拦截--路由进入前
    /* 一般进行权限控制,比如登录权限,权限等级,权限跳转等 */
    router.beforeEach((to, from, next) => {
      console.log("全局路由守卫--进入前");
      console.log(from);
      console.log(to);
    
      next();
    });
    
    // 全局拦截--路由解析后
    router.beforeResolve((to, from, next) => {
      console.log("全局路由守卫--解析后");
      console.log(from);
      console.log(to);
    
      next();
    });
    
    // 全局拦截--路由进入后
    router.afterEach((to, from) => {
      console.log("全局路由守卫--进入后");
      console.log(from);
      console.log(to);
    });

路由守卫

路由守卫 beforeEnter:针对特定路由设置的守卫,因此设置的方式也不再是在 router 路由实例上面设置,而是在某个路由配置中设置。

js
const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    // 在路由的配置中进行设置,只针对特定的路由进行拦截
    beforeEnter: (to, from,next) => {
     next()
    },
  },
]

相关细节:

  1. beforeEnter 守卫只在进入路由时触发不会在 params、query 或 hash 改变时触发
  • 从 /users/2 进入到 /users/3 这种不会触发
  • 从 /users/2#info 进入到 /users/2#projects 这种也不会触发
  1. 如果放在父级路由上,路由在具有相同父级的子路由之间移动时,它不会被触发。
js
const routes = [
  {
    path: "/user",
    beforeEnter() {
      // ...
    },
    children: [
      { path: "list", component: UserList },
      { path: "details", component: UserDetails },
    ],
  },
];

从 /user/list 跳转到 /user/details 不会触发路由级别守卫。

组件守卫

组件守卫:这种守卫是组件级别,取决于是否进入或者离开某个组件

  • onBeforeRouteUpdate:当前路由改变,但是该组件被复用时调用,例如对于一个带有动态参数的路径 /users/:id,在 /users/1 和 /users/2 之间跳转的时候会触发
  • onBeforeRouteLeave:离开了该导航,组件失活的时候

整体的执行顺序:

  1. 组件离开守卫
  2. 全局前置守卫
  3. 路由级别守卫
  4. 全局解析守卫
  5. 全局后置守卫
  6. 组件进入守卫

如果是组件复用,参数变化的情况,执行顺序如下:

  1. 全局前置守卫
  2. 组件更新守卫
  3. 全局解析守卫
  4. 全局后置守卫
vue
<script setup>
import { onBeforeRouteUpdate, onBeforeRouteLeave } from "vue-router";

//组件内守卫只有更新和离开

onBeforeRouteUpdate((to, from) => {
  // /detail/:id  当id变化,可以监听到
  console.log("组件内守卫--更新", to, from);
});

onBeforeRouteLeave((to, from) => {
  console.log("组件内守卫--离开", to, from);
});
</script>

其他细节

1. 路由级别守卫 beforeEnter 设置多个值

路由级别守卫,也就是 beforeEnter 可以设置成一个数组,数组里面包含多个方法,每个方法进行一项处理。

js
const routes = [
  // ...
  {
    path: "/about",
    name: "About",
    component: About,
    beforeEnter: [
      (to, from, next) => {
        console.log("Route beforeEnter step 1");
        next();
      },
      (to, from, next) => {
        console.log("Route beforeEnter step 2");
        next();
      },
    ],
  },
  // ...
];

2. 在守卫内的全局注入

Vue 3.3 开始,你可以在导航守卫内使用 inject() 方法。这在注入像 pinia stores 这样的全局属性时很有用。

在 app.provide() 中提供的所有内容都可以在全局守卫里面获取到。

js
// main.js
const app = createApp();
app.provide("global", "some data");
js
// router.js
import { inject } from "vue";

router.beforeEach(() => {
  const data = inject("global");

  const userStore = useUserStore();
});

实战案例

导航守卫拦截拦截登录

使用导航守卫拦截未登录的用户,将未登录用户导航到登录页面。

已登录,但是不是管理员,在点击管理员页面的时候,跳转到首页。

角色:普通用户、管理员

页面:主页、用户页、管理员页、登录

未登录:主页、登录

用户身份登录:主页、用户页、登录

管理员身份登录:主页、用户页、管理员页、登录

js
//模拟状态管理存储的用户信息
let currentRole = null;

export function login(role) {
  currentRole = role;
  localStorage.setItem("role", role);
}

export function logout() {
  currentRole = null;
  localStorage.removeItem("role");
}

export function getCurrentRole() {
  return currentRole;
}
vue
<template>
  <div class="login-container">
    <template v-if="currentRole">
      <p>
        您当前的身份为: <strong>{{ userRole }}</strong
        >.
      </p>
      <button @click="logoutHandle">退出登录</button>
    </template>

    <template v-else>
      <h2>登录</h2>
      <form @submit.prevent="loginHandle">
        <input type="text" placeholder="请输入用户名" v-model="username" />
        <input type="password" placeholder="请输入密码" v-model="password" />
        <button type="submit">登录</button>
      </form>
    </template>
  </div>
</template>

<script setup>
import { ref, computed } from "vue";
import { getCurrentRole, login, logout } from "@/store/user";

const username = ref("");
const password = ref("");

const currentRole = ref(localStorage.getItem("role") || "");
const userRole = computed(() => {
  if (currentRole.value === "admin") {
    return "管理员";
  } else if (currentRole.value === "user") {
    return "普通用户";
  } else {
    return null;
  }
});
const loginHandle = () => {
  if (username.value === "admin" && password.value === "admin") {
    login("admin"); //设置角色为管理员
    currentRole.value = getCurrentRole();
  } else {
    alert("用户名或密码错误");
  }
};

const logoutHandle = () => {
  logout();
  currentRole.value = "";
};
</script>

<style scoped>
.login-container {
  max-width: 400px;
  margin: 100px auto;
  padding: 20px;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  text-align: center;
}

input {
  display: block;
  width: calc(100% - 24px);
  margin: 10px auto;
  padding: 10px;
  border: 1px solid #ced4da;
  border-radius: 4px;
}

button {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  background-color: #007bff;
  color: #fff;
  cursor: pointer;
}
</style>
js
import { createRouter, createWebHistory } from "vue-router";
import Home from "../views/Home.vue";
import Login from "../views/Login.vue";
import User from "../views/User.vue";
import Admin from "../views/Admin.vue";
import { getCurrentRole } from "@/store/user";

const routes = [
  { path: "/", name: "Home", component: Home },
  { path: "/login", name: "Login", component: Login },
  // 仅仅需要普通权限即可
  {
    path: "/user",
    name: "User",
    component: User,
    meta: {
      auth: true, //需要登陆鉴权
    },
  },
  {
    path: "/admin",
    name: "Admin",
    component: Admin,
    meta: {
      auth: true, //需要登陆鉴权
      requireAdmin: true, //需要管理员权限
    },
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

// 全局路由守卫--上面需求的逻辑在这里编写
router.beforeEach((to, from, next) => {
  if (to.meta.auth) {
    const role = getCurrentRole();
    if (!role) {
      next("/login");
    } else if (to.meta.requireAdmin && role !== "admin") {
      next("/");
    } else {
      next();
    }
  } else {
    next();
  }
});

export default router;

--

-EOF-

MIT License