Tiana随笔

Vue Router 导航守卫的常见用法

· Vue

标签:#Vue #前端

Vue Router 导航守卫的常见用法

我这个博客是 Vue 3 写的,但没用 vue-router,路由是手写 pushState。不过守卫那套思路通用:登录校验、页面标题、滚动位置。这篇是当时研究 vue-router 的笔记,已经改成手写路由也能直接用的版本。

我的博客还没上 vue-router

先说个尴尬的事实。我这个博客是 Vue 3 + Vite 的 SPA,但没用 vue-router。路由是 router/appRouter.js 自己 pushState 拼的。vue-router-guard 那一套钩子也用不上。

我说这些不是泼冷水,是提醒同样在小项目里手写路由的人:用 vue-router 的守卫之前,先确认你确实要一套这么重的路由框架。我自己走下来发现小项目里「下一个路由 + 解码 + 渲染」三步就能 cover 90% 的场景。

但是守卫那一套思想是通用的,思路搬过来照样好用。下面我贴一份当时研究 vue-router 时的笔记,已经改成我博客手写路由的版本。

守卫要解决的三件事

守卫其实就是路由切换的几个钩子:beforeEach、beforeResolve、afterEach。它们分别在「即将跳转」、「目标组件已经异步加载完」、「跳转完成」三个时机触发。

第一件事是登录校验。很多后台路由都得登录才能看,但登录页本身不需要登录。这个判断如果散落在每个页面组件里,会出现两份逻辑两份判断,非常容易出错。放在守卫里集中写,入口就只有一个。

第二件事是页面标题和埋点。标题每个页面不同,但页面组件不应该关心「我被谁打开了」,否则每个页面都得 import 元信息管理模块。把这些放在守卫里一份,对所有页面都生效。

第三件事是滚动行为。用户点进一篇文章滚到中间,如果直接跳回列表页,结果整个页面的滚动位置没归零,体验就乱了。守卫的 scrollBehavior 是专门解决这个问题的。

自己写 router 时怎么搬这个思路

我那个手写 appRouter.js 大致长这样(简化版):

const routes = [
  { path: /^\/?$/, view: () => loadView('home') },
  { path: /^\/post\/([^/]+)$/, view: (m) => loadView('post', { slug: m[1] }) },
  { path: /^\/admin/, view: () => loadView('admin'), meta: { requiresAuth: true } },
  { path: /^\/login$/, view: () => loadView('login') },
  { path: /.*/, view: () => loadView('notFound') },
]

async function navigateTo(path, opts = {}) {
  for (const guard of guards) {
    const result = await guard({ to: path, meta: match.meta })
    if (result === false) return
    if (typeof result === 'string') return navigateTo(result, { replace: true })
  }
  history.pushState({}, '', path)
  render(match)
}

这里的 guards 数组就是我手写的「全局守卫」。每次跳转前依次跑一遍,任何一个返回 false 就停下来,返回字符串就跳到那个路径。这个版本比 vue-router 简陋,但够我用了。

登录校验的陷阱

最容易栽的是「重定向死循环」:未登录访问 /admin,守卫跳 /login;登录页自己也设置了 requiresAuth: true,守卫一看又要把它跳到 /admin,循环起来浏览器很快就栈溢出。

vue-router 解决这个靠两个机制:路径级别的 meta 和「next 区分 abort / redirect / allow」。我手写版本的处理方法是:守卫看到 /login 一定放行,不再走权限判断。

function authGuard({ to, meta }) {
  if (to === '/login') return true
  if (meta.requiresAuth && !hasSession()) return '/login'
  return true
}

另一个坑是 next() 返回位置写错。vue-router 里如果你想跳另一个路径,return '/login' 之后必须立即 return,不能继续执行后面的同步代码。我早期版本栽在这里,后台跳登录同时还把旧路径写进了 session,调试半天才查到。

页面标题归一

标题变化如果散在每个组件,每次写页面都得记住「我得改 title」,新人很容易忘。我把这件事移到守卫里去。

function titleGuard({ to, meta }) {
  const title = meta.title ? `${meta.title} - Tiana` : 'Tiana'
  document.title = title
  return true
}

注册顺序也要讲究。我在 authGuard 之前先跑 analyticsGuard(埋点)。埋点失败不应该拦下用户的访问,但权限校验失败必须拦。我用 guards 数组的注册顺序控制这个优先级,调试时也好找问题来源。

滚动位置要存到 sessionStorage 里

「跳详情读完滚回来还在原位置」这种细节,最容易在 SPA 里被忽略。我的处理是:每次 afterEach 把当前滚动位置按路径存一份,下次进同一路径时读出来恢复。

const positions = JSON.parse(sessionStorage.getItem('scrollpos') || '{}')

function restoreScroll(path) {
  const y = positions[path]
  if (typeof y === 'number') {
    requestAnimationFrame(() => window.scrollTo(0, y))
  }
}

function saveScroll(path) {
  positions[path] = window.scrollY
  sessionStorage.setItem('scrollpos', JSON.stringify(positions))
}

sessionStorage 而不是 localStorage,是因为用户关掉标签页就应该清掉记忆,隔夜回访回到首页而不是上次滚到一半的位置,体验更自然。

我从 vue-router 学到什么

vue-router 是为大型路由设计的,能力边界比手写版本宽得多。但小项目里把它当「过强」工具用反而是负担。我自己留下的经验是:先把守卫想解决的问题列出来,按问题挑 API,不要按 API 想问题。这样无论是用框架还是手写都不会偏。

相关文章