Vue3 + Vue Router + Pinia 路由守卫规范:beforeEach 应做 / 不应做,避死循环、防重复请求|状态管理与路由规范篇

【Vue3 + Vue Router + Pinia】中后台前端实战:从路由守卫规范到落地实操,彻底搞懂beforeEach最佳写法,避开死循环、重复请求两大高频坑!

📑 文章目录

  • 一、开篇:为什么路由守卫值得单独写一篇文章?
  • [二、先搞清楚:什么是路由守卫?beforeEach 在哪个时机执行?](#二、先搞清楚:什么是路由守卫?beforeEach 在哪个时机执行?)
    • [2.1 路由守卫的时机](#2.1 路由守卫的时机)
    • [2.2 必须记住的 3 点](#2.2 必须记住的 3 点)
  • [三、beforeEach 里「应该做」什么?](#三、beforeEach 里「应该做」什么?)
    • [3.1 适合做的事](#3.1 适合做的事)
    • [3.2 完整示例:登录校验 + 标题设置](#3.2 完整示例:登录校验 + 标题设置)
  • [四、beforeEach 里「不应该做」什么?](#四、beforeEach 里「不应该做」什么?)
    • [4.1 不适合做的事](#4.1 不适合做的事)
    • [4.2 为什么「在 beforeEach 里发请求」容易出问题?](#4.2 为什么「在 beforeEach 里发请求」容易出问题?)
  • 五、死循环:为什么会发生?怎么避免?
    • [5.1 常见死循环场景](#5.1 常见死循环场景)
    • [5.2 正确写法:白名单 + 明确分支](#5.2 正确写法:白名单 + 明确分支)
  • 六、重复请求:如何避免?
    • [6.1 问题示例](#6.1 问题示例)
    • [6.2 正确做法:在 Store 里做「请求去重」](#6.2 正确做法:在 Store 里做「请求去重」)
    • [6.3 更推荐:在应用启动或登录后拉取](#6.3 更推荐:在应用启动或登录后拉取)
  • 七、状态管理与路由守卫的配合规范
    • [7.1 职责划分](#7.1 职责划分)
    • [7.2 推荐的项目流程](#7.2 推荐的项目流程)
  • 八、完整实战示例
  • [九、小结:beforeEach 使用清单](#九、小结:beforeEach 使用清单)
  • [🔍 系列模块导航](#🔍 系列模块导航)

同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。

(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)

很多前端开发者都会遇到一个瓶颈:

代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。

想写出干净、优雅、可维护 的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验

这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。

帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。


一、开篇:为什么路由守卫值得单独写一篇文章?

在 Vue3 + Vue Router 项目里,beforeEach 几乎是必用的。很多人写了不少业务代码,但对「在 beforeEach 里该做什么、不该做什么」没有清晰规范,经常出现:

  • 死循环(页面白屏或一直跳转)
  • 重复请求(接口被多次调用)
  • 逻辑和登录状态、权限、面包屑等混在一起,难以维护

这篇文章的目标是:结合日常实战,讲清楚 beforeEach 的规范用法,以及如何避免常见问题。不追求底层实现,而是「日常该怎么写」。

[⬆ 返回目录](#⬆ 返回目录)

二、先搞清楚:什么是路由守卫?beforeEach 在哪个时机执行?

2.1 路由守卫的时机

路由守卫分为三种:全局守卫、路由独享守卫、组件内守卫。本文重点讲 **全局前置守卫 ** beforeEach

js 复制代码
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [...]
})

// 全局前置守卫:每次路由跳转「之前」都会执行
router.beforeEach((to, from, next) => {
  // to: 要去的目标路由
  // from: 当前要离开的路由
  // next: 控制跳转的函数
  next()  // 必须调用 next(),否则路由不会继续
})

执行顺序可以简单理解为:

  1. 用户点击链接 / 执行 router.push
  2. 路由开始切换
  3. beforeEach 被调用
  4. 如果调用了 next() → 继续到目标路由
  5. 如果调用 next(false) → 取消本次跳转
  6. 如果调用 next('/login') → 重定向到其他路由

[⬆ 返回目录](#⬆ 返回目录)

2.2 必须记住的 3 点

要点 说明
必须调用 next() 不调用 next,路由会一直卡住
只调用一次 next() 多次调用会导致警告和异常行为
在异步逻辑里也要保证调用 next 例如发请求后要调用 next,否则路由无法继续

[⬆ 返回目录](#⬆ 返回目录)

三、beforeEach 里「应该做」什么?

3.1 适合做的事

  1. 登录校验:未登录用户访问需登录页面 → 重定向到登录页
  2. 权限校验:无权限访问某页面 → 重定向到 403 或首页
  3. 设置页面标题document.title = to.meta.title
  4. 路由元信息加工:面包屑、菜单高亮等
  5. 轻量级状态同步:如将「需要登录」标记写入 Vuex/Pinia

核心原则:快、简单、无副作用

[⬆ 返回目录](#⬆ 返回目录)

3.2 完整示例:登录校验 + 标题设置

js 复制代码
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { title: '登录', requiresAuth: false }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { title: '工作台', requiresAuth: true }
  }
]

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

// 假设从 Pinia 获取登录状态
import { useUserStore } from '@/stores/user'

router.beforeEach((to, from, next) => {
  // 1. 设置页面标题
  if (to.meta.title) {
    document.title = `${to.meta.title} - 我的应用`
  }

  // 2. 登录校验
  const userStore = useUserStore()
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth)

  if (requiresAuth && !userStore.isLoggedIn) {
    next({ path: '/login', query: { redirect: to.fullPath } })
    return
  }

  // 3. 已登录访问登录页 → 直接去 redirect 或首页
  if (to.path === '/login' && userStore.isLoggedIn) {
    next({ path: (to.query.redirect || '/dashboard').toString() })
    return
  }

  next()
})

说明:

  • requiresAuth:通过 to.matched.some(...) 判断目标路由(含父级)是否要求登录
  • redirect:登录后可以跳回用户原来想去的页面
  • 每个分支内 next() 只调用一次,逻辑清晰

[⬆ 返回目录](#⬆ 返回目录)

四、beforeEach 里「不应该做」什么?

4.1 不适合做的事

不要做 原因
发起接口请求(如获取用户信息、菜单) 每次跳转都请求,容易重复、阻塞,不好控制
复杂业务逻辑 路由守卫应尽量轻量,复杂逻辑放组件或 store
修改全局状态(如 Vuex 大量 mutation) 增加心智负担,难以排查问题
依赖尚未就绪的 store 可能拿到空数据或导致时序问题

[⬆ 返回目录](#⬆ 返回目录)

4.2 为什么「在 beforeEach 里发请求」容易出问题?

js 复制代码
// ❌ 错误示例:在 beforeEach 里发请求
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  if (!userStore.userInfo) {
    await api.getUserInfo()  // 每次没 userInfo 就请求
    userStore.setUserInfo(...)
  }
  next()
})

典型问题:

  1. 重复请求 :多次快速切换路由,userInfo 还没写进 store,会触发多次 getUserInfo
  2. 竞态:多个请求返回顺序不确定,后返回的可能覆盖先返回的
  3. 阻塞路由:请求较慢时,用户会觉得「点了没反应」

正确思路:登录态、用户信息等在进入应用或登录成功后统一拉取,守卫只做「读状态 + 判断」

[⬆ 返回目录](#⬆ 返回目录)

五、死循环:为什么会发生?怎么避免?

5.1 常见死循环场景

场景 1:未登录 → 去 /login,又在 /login 里再次重定向

js 复制代码
// ❌ 死循环写法
router.beforeEach((to, from, next) => {
  const userStore = useUserStore()
  if (!userStore.isLoggedIn) {
    next('/login')  // 未登录 → 去登录页
  } else {
    next()
  }
})

如果 /login 也被标记为 requiresAuth: true,或者你的判断逻辑把 /login 也当成「需要登录」的页面,就会出现:

  • 进入 /login → 判断未登录 → next('/login') → 再次进入 /login → 无限循环

场景 2:重定向到自身

js 复制代码
// ❌ 死循环写法
router.beforeEach((to, from, next) => {
  if (!hasPermission(to)) {
    next(to.fullPath)  // 没权限却重定向到当前页,相当于循环
  }
  next()
})

[⬆ 返回目录](#⬆ 返回目录)

5.2 正确写法:白名单 + 明确分支

js 复制代码
// ✅ 正确:对登录页等做白名单处理
const whiteList = ['/login', '/register', '/forgot-password']

router.beforeEach((to, from, next) => {
  const userStore = useUserStore()

  if (whiteList.includes(to.path)) {
    // 白名单直接放行
    if (userStore.isLoggedIn && to.path === '/login') {
      next('/dashboard')
    } else {
      next()
    }
    return
  }

  if (!userStore.isLoggedIn) {
    next(`/login?redirect=${encodeURIComponent(to.fullPath)}`)
    return
  }

  next()
})

核心:对 /login 等页面做白名单,避免「进登录页 → 再判断未登录 → 再进登录页」的循环。

[⬆ 返回目录](#⬆ 返回目录)

六、重复请求:如何避免?

6.1 问题示例

js 复制代码
// ❌ 每次进页面都可能请求一次
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  if (to.meta.requiresAuth && !userStore.userInfo) {
    await userStore.fetchUserInfo()  // 并发、重复、竞态
  }
  next()
})

多次快速切换路由时,fetchUserInfo 可能被调用多次。

[⬆ 返回目录](#⬆ 返回目录)

6.2 正确做法:在 Store 里做「请求去重」

js 复制代码
// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: null,
    userInfoPromise: null  // 用 Promise 做去重
  }),
  actions: {
    async fetchUserInfo() {
      if (this.userInfo) return this.userInfo
      if (this.userInfoPromise) return this.userInfoPromise

      this.userInfoPromise = api.getUserInfo()
      try {
        this.userInfo = await this.userInfoPromise
        return this.userInfo
      } finally {
        this.userInfoPromise = null
      }
    }
  }
})

守卫里只做「是否需要用户信息」的判断,拉取逻辑全部在 store 中,且通过 userInfoPromise 保证同一时刻只会有一个请求。

[⬆ 返回目录](#⬆ 返回目录)

6.3 更推荐:在应用启动或登录后拉取

js 复制代码
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { useUserStore } from './stores/user'

const app = createApp(App)
app.use(router)

router.isReady().then(() => {
  const userStore = useUserStore()
  if (userStore.token) {
    userStore.fetchUserInfo()  // 有 token 时预拉取
  }
  app.mount('#app')
})

这样:

  • 用户信息在应用初始化时或登录后统一拉取
  • beforeEach 只读 userStore.userInfo,不做请求
  • 避免在路由切换时频繁发请求

[⬆ 返回目录](#⬆ 返回目录)

七、状态管理与路由守卫的配合规范

7.1 职责划分

层级 职责 示例
路由守卫 读状态、做校验、决定是否放行 判断 isLoggedInhasPermission
Store 存状态、发请求、提供方法 fetchUserInfologout
组件 业务 UI、交互、调用 store 调用 userStore.fetchUserInfo

[⬆ 返回目录](#⬆ 返回目录)

7.2 推荐的项目流程

复制代码
用户打开页面
    ↓
main.js:有 token 时预拉取用户信息(store)
    ↓
路由跳转
    ↓
beforeEach:读 store 判断是否登录、是否有权限
    ↓
放行 → 进入目标组件

[⬆ 返回目录](#⬆ 返回目录)

八、完整实战示例

下面是一个「登录校验 + 动态标题 + 登录后回跳」的示例。

js 复制代码
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'

const routes = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { title: '登录', requiresAuth: false }
  },
  {
    path: '/',
    component: () => import('@/layouts/MainLayout.vue'),
    meta: { requiresAuth: true },
    children: [
      {
        path: 'dashboard',
        name: 'Dashboard',
        component: () => import('@/views/Dashboard.vue'),
        meta: { title: '工作台' }
      },
      {
        path: 'profile',
        name: 'Profile',
        component: () => import('@/views/Profile.vue'),
        meta: { title: '个人中心' }
      }
    ]
  }
]

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

const whiteList = ['/login']

router.beforeEach((to, from, next) => {
  // 1. 设置标题
  const title = to.meta.title
  document.title = title ? `${title} - 我的应用` : '我的应用'

  // 2. 白名单直接放行(但要处理已登录访问登录页)
  if (whiteList.includes(to.path)) {
    const userStore = useUserStore()
    if (to.path === '/login' && userStore.isLoggedIn) {
      const redirect = (to.query.redirect || '/dashboard').toString()
      next(redirect)
      return
    }
    next()
    return
  }

  // 3. 需要登录的路由
  const userStore = useUserStore()
  const requiresAuth = to.matched.some(r => r.meta.requiresAuth)

  if (requiresAuth && !userStore.isLoggedIn) {
    next({
      path: '/login',
      query: { redirect: to.fullPath }
    })
    return
  }

  next()
})

export default router
js 复制代码
// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    token: localStorage.getItem('token'),
    userInfo: null
  }),
  getters: {
    isLoggedIn: (state) => !!state.token
  },
  actions: {
    setUser(info, token) {
      this.userInfo = info
      this.token = token
      if (token) localStorage.setItem('token', token)
    },
    logout() {
      this.userInfo = null
      this.token = null
      localStorage.removeItem('token')
    }
  }
})
js 复制代码
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import router from './router'
import { useUserStore } from './stores/user'

const app = createApp(App)
app.use(createPinia())
app.use(router)

router.isReady().then(() => {
  const userStore = useUserStore()
  if (userStore.token) {
    userStore.fetchUserInfo?.()  // 如有封装,在此预拉取
  }
  app.mount('#app')
})

[⬆ 返回目录](#⬆ 返回目录)

九、小结:beforeEach 使用清单

类别 内容
应该做 登录/权限校验、标题设置、白名单、轻量逻辑
不应该做 在守卫里发请求、复杂业务、重状态修改
避免死循环 白名单处理登录页、不要重定向到当前页
避免重复请求 请求放到 store,用 Promise 去重;优先在启动/登录后拉取
next 使用 必须调用、只调用一次、每个分支都要调用

按以上规范来写 beforeEach,可以避免绝大多数路由守卫相关的坑,逻辑也会更清晰、好维护。

[⬆ 返回目录](#⬆ 返回目录)

🔍 系加粗样式列模块导航

📝 状态管理与路由规范

一、《Vue3 Pinia 状态管理规范:状态拆分、Actions 写法、持久化实战,避坑状态污染|状态管理与路由规范篇》
二、《Vue3 Pinia 状态管理规范:何时用 Pinia 何时用本地状态|状态管理与路由规范篇》
三、《Vue Router 实战规范:path/name/meta 配置 + 动态 / 嵌套路由,统一团队标准|状态管理与路由规范篇》

四、《Vue3 + Vue Router + Pinia 路由守卫规范:beforeEach 应做 / 不应做,避死循环、防重复请求|状态管理与路由规范篇》
五、《Vue keep-alive 实战避坑:include/exclude + 路由 meta 标记,中后台路由缓存精准可控|状态管理与路由规范篇》

👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~

📚 系列总览

前端规范实战系列 」正在持续更新中,后续会整理一篇《前端规范实战系列全系列目录导航》,包含每篇文章简介 + 直达链接,方便大家按顺序、体系化学习。

更新中,敬请期待~

[⬆ 返回目录](#⬆ 返回目录)


技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护

哪怕每次只吃透一条规范,长期下来,差距会非常明显。

后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。

觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。

我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~

相关推荐
Greg_Zhong1 小时前
Css知识之伪类和伪元素
前端·css
Mintopia1 小时前
GPT-5.3-Codex 底层逻辑是什么,为什么编码强?
前端·人工智能·ai编程
桃气媛媛1 小时前
python流程控制-匹配语句match
开发语言·python
Mintopia1 小时前
Opus 模型凭什么收费贵,与其他模型对比理由是什么?
前端·人工智能
东东__net2 小时前
js逆向与谷歌加密库
开发语言·前端·javascript
ulias2122 小时前
C++ 异常处理机制
java·开发语言·c++
zyhomepage2 小时前
科技的成就(七十二)
开发语言·人工智能·科技·算法·内容运营
计算机安禾2 小时前
【数据结构与算法】第2篇:C语言核心机制回顾(一):指针、数组与结构体
c语言·开发语言·数据结构·c++·算法·链表·visual studio
dapeng28702 小时前
C++代码重构实战
开发语言·c++·算法