Skip to content

路由守卫

路由守卫是 Uni Router 的核心能力,允许在导航过程中插入自定义逻辑:鉴权、日志、数据预取、离开确认等。本章将深入讲解守卫的执行机制和返回值模式(推荐)。

守卫全景

Uni Router 提供五类守卫。前进导航的执行顺序:

导航触发

  ├─ 1. beforeEach        全局前置守卫(可多个)
  │     └─ 可中止 / 重定向 / 放行

  ├─ 2. beforeEnter       路由独享守卫(配置在 RouteConfig)
  │     └─ 可中止 / 重定向 / 放行

  ├─ 3. beforeResolve     全局解析守卫(可多个)
  │     └─ 可中止 / 重定向 / 放行

  ├─ 4. uni 导航 API 调用  navigateTo / redirectTo / ...

  └─ 5. afterEach         全局后置钩子(可多个)
        └─ 仅观察,无法改变导航结果

返回操作(物理返回键 / 浏览器后退 / router.back())先执行 onBeforeBack 返回守卫,放行后复用 beforeEach → beforeResolve → afterEach 链路,详见返回守卫

各守卫的定位

守卫注册方式典型场景
beforeEachrouter.beforeEach(fn)登录鉴权、权限检查、全局日志
beforeEnterRouteConfig.beforeEnter某路由的专属校验(如需读取特定数据)
beforeResolverouter.beforeResolve(fn)数据预取完成后的最终确认
afterEachrouter.afterEach(fn)设置标题、埋点、清理状态,接收失败信息
onBeforeBackrouter.onBeforeBack(fn)返回拦截、离开确认(物理返回键 / 浏览器后退 / router.back()

beforeResolve 的定位

beforeResolvebeforeEnter 之后执行,此时所有前置校验已通过。适合放"所有守卫都同意后"的最终逻辑,如确认数据已加载完毕。它与 beforeEach 的区别仅在于执行时机。

注册守卫

全局守卫

ts
const router = createRouter({ routes })

// 前置守卫(返回值模式,推荐)
const removeBefore = router.beforeEach((to, from) => {
  if (to.meta.requireAuth && !isLoggedIn()) {
    return { name: 'login' }  // 重定向
  }
  // 不返回值或 return true 表示放行
})

// 解析守卫(返回值模式)
router.beforeResolve(async (to) => {
  // 所有前置守卫通过后,预取数据
  if (to.name === 'detail') {
    await store.fetchDetail(to.query.id)
  }
  // 不返回值 = 放行
})

// 后置钩子(接收 failure 参数)
router.afterEach((to, from, failure) => {
  if (failure) {
    console.error('导航失败:', failure.message)
    return
  }
  if (to.meta.title) {
    uni.setNavigationBarTitle({ title: to.meta.title as string })
  }
})

// 移除守卫
removeBefore()

路由独享守卫

ts
const routes = [
  {
    path: 'pages/admin/admin',
    name: 'admin',
    meta: { requireAdmin: true },
    beforeEnter: (to, from) => {
      if (user.role === 'admin') return true  // 放行
      return { name: '403' }  // 重定向
    }
  },
  {
    path: 'pages/edit/edit',
    name: 'edit',
    // 支持数组形式
    beforeEnter: [
      checkAuth,
      checkPermission,
      checkLockStatus
    ]
  }
]

守卫数组

beforeEnter 支持传入数组,按顺序执行。任一守卫中止或重定向,后续守卫不再执行。

守卫返回值

返回值模式是 v2.1.0 推荐的守卫写法,守卫通过返回值控制导航行为,无需调用 next() 回调。

1. 放行:return undefined / return true

ts
router.beforeEach((to, from) => {
  return true // 放行,继续执行下一个守卫
})

// 不写 return 也等同于放行
router.beforeEach((to, from) => {
  // 默认放行
})

2. 中止:return false

ts
router.beforeEach((to, from) => {
  if (isOffline()) {
    uni.showToast({ title: '网络不可用', icon: 'none' })
    return false // 中止导航,停留在当前页
  }
})

中止会抛出 NavigationFailureNAVIGATION_ABORTED)。

3. 重定向:return RouteLocationRaw

ts
router.beforeEach((to, from) => {
  if (to.meta.requireAuth && !isLoggedIn()) {
    // 重定向到登录页,携带原目标用于登录后跳回
    return { name: 'login', query: { redirect: to.fullPath } }
  }
})

重定向会重新触发完整的守卫链(从 beforeEach 开始),并增加重定向深度计数。

4. 抛出错误中止

ts
router.beforeEach((to, from) => {
  if (to.meta.requireAuth) {
    throw new Error('权限不足')  // 取消导航(NAVIGATION_CANCELLED)
  }
})

// 或返回 Error 对象
router.beforeEach((to, from) => {
  if (to.meta.requireAuth) {
    return new Error('权限不足')  // 取消导航(NAVIGATION_CANCELLED)
  }
})

返回值总结

返回值行为
undefined / void / true放行,继续执行下一个守卫
false中止导航(NAVIGATION_ABORTED
string(如 '/login'重定向到路径
RouteLocationRaw(如 { name: 'login' }重定向到路由位置
NavigationRedirect(如 { location, mode }重定向到路由位置并指定导航方式
Error 对象取消导航(NAVIGATION_CANCELLED
抛出异常取消导航(NAVIGATION_CANCELLED

可控重定向

默认情况下,守卫重定向沿用触发守卫的原始导航方式(push 触发的守卫重定向时仍用 uni.navigateTo)。通过返回 { location, mode } 对象,可显式指定重定向使用的导航方式。

ts
router.beforeEach((to, from) => {
  if (to.meta.requireAuth && !isLoggedIn()) {
    // 用 replace 跳转登录页,避免登录页残留在页面栈中
    return { location: { name: 'login', query: { redirect: to.fullPath } }, mode: 'replace' }
  }
})

重定向方式优先级

显式 mode(NavigationRedirect.mode)> 原始导航方式 > back 回退 relaunch
触发导航守卫返回实际重定向方式
push{ location, mode: 'replace' }replace(显式指定)
push{ name: 'login' }push(沿用原始)
replace{ location, mode: 'relaunch' }relaunch(显式指定)
replace{ name: 'login' }replace(沿用原始)
back{ location, mode: 'push' }push(显式指定)
back{ name: 'login' }relaunch(back 无法跳转栈外目标,回退)

mode 取值

ts
type NavigationRedirectMode = 'push' | 'replace' | 'relaunch'
mode对应 uni API适用场景
'push'navigateTo登录后需返回原页面,保留目标页在栈中
'replace'redirectTo替换当前页,不留历史(如登录页)
'relaunch'reLaunch清空栈(如权限不足时回到首页)

实战:登录重定向

ts
router.beforeEach((to, from) => {
  if (to.meta.requireAuth && !isLoggedIn()) {
    if (from.name === 'login') {
      // 已在登录页还无权限,用 replace 避免栈堆积
      return false
    }
    // 用 replace 跳登录页,避免登录页残留在页面栈中
    return { location: { name: 'login', query: { redirect: to.fullPath } }, mode: 'replace' }
  }
})

// 登录成功后
async function onLoginSuccess(redirect: string) {
  // 用 replace 回到原页面,避免登录页留在栈中
  await router.replace(redirect)
}

实战:权限不足清栈

ts
router.beforeEach((to, from) => {
  if (to.meta.roles && !hasRole(to.meta.roles)) {
    // 权限不足,清空栈回到首页
    return { location: { name: 'home' }, mode: 'relaunch' }
  }
})

异步守卫

守卫支持 async 函数和返回 Promise:

ts
router.beforeEach(async (to, from) => {
  // 异步校验 token 有效性
  const valid = await checkToken()
  if (!valid) {
    return { name: 'login' }  // 重定向到登录页
  }
  // 放行
})

Promise reject 中止导航

ts
router.beforeEach(async (to, from) => {
  try {
    await fetchUserProfile()
    // 放行
  } catch (err) {
    // reject 会中止导航(NAVIGATION_CANCELLED)
    throw err
  }
})

返回值 vs 异常抛出

  • return falseNAVIGATION_ABORTED(用户主动中止)
  • throw / rejectNAVIGATION_CANCELLED(异常导致取消)

建议用 return false 表达"主动中止",用异常表达"意外错误"。

超时保护

守卫可能因异步操作卡住(如网络请求无响应)。Uni Router 提供超时保护:

ts
const router = createRouter({
  routes,
  guardTimeout: 10000 // 默认 10 秒
})
守卫执行
  → 10 秒内未返回结果也未抛出异常
  → 输出警告: "Navigation guard did not resolve within 10s"
  → 自动中止导航 (NAVIGATION_CANCELLED)

调整超时

守卫中有耗时请求时调大超时:

ts
const router = createRouter({
  routes,
  guardTimeout: 30000 // 30 秒
})

设为 0 可禁用超时保护(不推荐,可能导致导航永久挂起)。

守卫执行细节

执行顺序

同一类型的多个守卫按注册顺序执行:

ts
router.beforeEach(guard1) // 先执行
router.beforeEach(guard2) // 后执行
router.beforeEach(guard3) // 最后执行
guard1 → guard2 → guard3 → beforeEnter → beforeResolve1 → beforeResolve2 → API

中止/重定向的短路效应

任一守卫中止或重定向,后续守卫不再执行

ts
router.beforeEach((to, from) => {
  return false // 中止
})

router.beforeEach((to, from) => {
  console.log('不会执行')
})

重定向重新触发守卫链

ts
router.beforeEach((to, from) => {
  if (to.name === 'a') {
    return { name: 'b' } // 重定向到 b
  }
})

router.beforeEach((to, from) => {
  // 重定向到 b 时,此守卫会再次执行
  console.log(to.name) // 'b'
})
push(a) → beforeEach[1] 重定向到 b
        → beforeEach[1] 再次执行(to=b)→ 放行
        → beforeEach[2] 执行(to=b)→ 放行
        → ... → navigateTo(b)

避免无限重定向

重定向深度上限为 10。A→B→A→B... 循环会在第 10 次后抛出 NAVIGATION_CANCELLED。务必在重定向条件中加入终止判断。

afterEach 后置钩子

afterEach 在导航完成后执行,无法改变导航结果(不接受 next 参数),但接收第三个参数 failure 获取导航失败信息:

ts
router.afterEach((to, from, failure) => {
  if (failure) {
    // 导航失败时记录错误
    console.error('导航失败:', failure.message)
    return
  }

  // 设置页面标题
  if (to.meta.title) {
    uni.setNavigationBarTitle({ title: to.meta.title as string })
  }

  // 埋点
  trackPageView(to.path, from.path)
})

afterEach 不触发的场景

状态同步不触发 afterEach

afterEach 仅在完整导航(经过前置守卫)完成后触发。以下场景不触发 afterEach

  1. syncRoute() / syncCurrentRoute() 的状态同步

物理返回键、浏览器后退会经返回守卫链执行,守卫放行后 afterEach 正常触发(见返回守卫)。 如需监听所有路由变化(包括状态同步),使用 onRouteChange

ts
router.onRouteChange((to, from) => {
  // 完整导航和状态同步都会触发
  if (to._synced) {
    console.log('状态同步(非完整导航)')
  }
})

实战模式

模式 1:登录鉴权

ts
// 全局前置守卫
router.beforeEach((to, from) => {
  const isLoggedIn = !!uni.getStorageSync('token')

  if (to.meta.requireAuth && !isLoggedIn) {
    // 未登录 → 跳登录页,用 replace 避免登录页残留在页面栈中
    return { location: { name: 'login', query: { redirect: to.fullPath } }, mode: 'replace' }
  }
  if (to.name === 'login' && isLoggedIn) {
    // 已登录访问登录页 → 跳首页
    return { location: { name: 'home' }, mode: 'replace' }
  }
  // 放行
})

模式 2:权限控制

ts
// 扩展 RouteMeta
declare module '@meng-xi/uni-router' {
  interface RouteMeta {
    roles?: string[]
  }
}

router.beforeEach((to, from) => {
  const userRoles = getUserRoles()

  if (to.meta.roles && !to.meta.roles.some(r => userRoles.includes(r))) {
    // 权限不足 → 清栈回首页
    return { location: { name: 'home' }, mode: 'relaunch' }
  }
})

模式 3:离开确认

ts
// 标记页面为"脏"状态
const routes = [
  {
    path: 'pages/edit/edit',
    name: 'edit',
    meta: { dirty: false } // 运行时动态修改
  }
]

router.beforeEach((to, from) => {
  if (from.meta.dirty) {
    // 离开确认需要异步对话框,使用 Promise 包装
    return new Promise((resolve) => {
      uni.showModal({
        title: '提示',
        content: '有未保存的修改,确认离开?',
        success: (res) => {
          if (res.confirm) {
            from.meta.dirty = false // 重置
            resolve(true) // 放行
          } else {
            resolve(false) // 中止
          }
        }
      })
    })
  }
})

模式 4:数据预取

ts
// beforeResolve 中预取(所有前置校验已通过)
router.beforeResolve(async (to) => {
  try {
    switch (to.name) {
      case 'detail':
        await store.fetchDetail(to.query.id)
        break
      case 'list':
        await store.fetchList(to.queryInt('page', 1))
        break
    }
    // 放行
  } catch (err) {
    uni.showToast({ title: '加载失败', icon: 'none' })
    return false // 数据加载失败,中止导航
  }
})

模式 5:页面标题自动设置

ts
router.afterEach((to) => {
  const title = to.meta.title as string | undefined
  if (title) {
    uni.setNavigationBarTitle({ title })
  } else {
    uni.setNavigationBarTitle({ title: '默认标题' })
  }
})

模式 6:路由级独享校验

ts
const routes = [
  {
    path: 'pages/order/order',
    name: 'order',
    beforeEnter: [
      // 必须先选择地址
      (to, from) => {
        if (!store.selectedAddress) {
          uni.showToast({ title: '请先选择地址', icon: 'none' })
          return false
        }
      },
      // 必须有商品
      (to, from) => {
        if (store.cart.length === 0) {
          return { name: 'cart' }
        }
      }
    ]
  }
]

返回守卫 onBeforeBack

onBeforeBack 是全局返回守卫,在返回操作触发时执行,可用于离开确认、返回拦截等场景。

ts
// 注册返回守卫(返回 false 阻止返回,true / undefined 放行)
router.onBeforeBack((to, from) => {
  if (hasUnsavedChanges) {
    uni.showToast({ title: '有未保存的修改', icon: 'none' })
    return false // 阻止返回
  }
  // 不返回值或 return true 放行
})

// 移除守卫
const remove = router.onBeforeBack(guard)
remove()

返回守卫链

返回操作与前进导航共用守卫链:

返回触发
  → 1. onBeforeBack   返回守卫(可多个)
  → 2. beforeEach     全局前置守卫
  → 3. beforeResolve  全局解析守卫
  → 4. uni.navigateBack(守卫放行后执行)
  → 5. afterEach      后置钩子(放行成功 / 被阻止均触发)

onBeforeBack 返回 false 阻止返回;true / undefined 放行;支持异步(Promise)。

各平台支持情况

返回场景AppH5小程序
物理返回键 / 导航栏返回 / uni.navigateBackonBackPress 接入
浏览器后退按钮 / 后退手势popstate 接入
iOS 边缘滑动返回⚠️ 需配合 setSideSlipGesture('none')
router.back() / 程序化 uni.navigateBack✅(需注册 InterceptorPlugin

小程序原生返回无法拦截

小程序右上角/左上角返回、物理返回键、滑动返回由宿主控制,无 onBackPress 生命周期、无 popstate 事件,onBeforeBack 无法拦截。这是平台能力边界。

配合 iOS 侧滑手势

iOS 边缘滑动返回默认绕过守卫链。通过 app.setSideSlipGesture 可按页面动态控制手势:

ts
const router = createRouter({
  routes,
  app: {
    setSideSlipGesture(to) {
      // 需要拦截的页面禁用侧滑,使返回走守卫链
      return to.meta.requireLeaveConfirm ? 'none' : 'close'
    }
  }
})
  • 'none':禁用 iOS 侧滑返回(返回操作走守卫链,onBeforeBack 生效)
  • 'close':开启原生侧滑返回(保留原生手势,侧滑绕过守卫)

仅 iOS 生效;Android 使用物理返回键,由 onBackPress 接入守卫链。

与 onBeforeRouteLeave 的关系

onBeforeRouteLeave 通过 beforeEach 实现,而返回守卫链包含 beforeEach,因此在返回操作中同样会执行 onBeforeRouteLeave,返回 false 同样可阻止返回。

状态同步仍自动处理

返回守卫放行后由路由器完成返回,页面 onShow 时全局 mixin 仍会自动 syncRoute() 同步路由状态,无需手动调用

ts
import { onShow } from '@dcloudio/uni-app'
import { useRoute } from '@meng-xi/uni-router'

const route = useRoute()

onShow(() => {
  // currentRoute 已被 mixin 自动同步
  console.log(route.value.path, route.value.params)
})

如需监听所有路由变化(包括状态同步),使用 onRouteChange

ts
router.onRouteChange((to, from) => {
  if (to._synced) {
    // 状态同步(未经过守卫链,如小程序原生返回)
    handleBackNavigation(to, from)
  }
})

详见平台兼容性

冷启动守卫检查

问题:冷启动绕过守卫

当用户通过以下方式直接进入某个页面时,页面由 uni-app 框架直接加载,不经过路由器导航,守卫(beforeEach 等)未执行:

场景平台
直接访问 URLH5
扫码进入 / 场景值小程序
Deeplink / URL SchemeApp
用户访问 https://example.com/#/pages/about/about
  → uni-app 直接加载 about 页
  → 路由器守卫未执行(未经过 router.push)
  → 未登录用户直接进入了 requireAuth 页面

解决方案:guardRoute()

router.guardRoute() 对当前(或指定)路由补执行守卫链,按守卫结果决定是否重定向:

ts
// App.vue
import { onLaunch } from '@dcloudio/uni-app'
import { useRouter } from '@meng-xi/uni-router'

const router = useRouter()

onLaunch((options) => {
  router.isReady().then(() => {
    // onLaunch 时页面栈可能为空(Page.onLoad 尚未触发),currentRoute 仍是 START_LOCATION。
    // 优先从 launch options.path 获取真实入口路径传给 guardRoute,确保守卫校验的是实际页面。
    const launchPath = options?.path ? `/${options.path}` : undefined
    router.guardRoute(launchPath, {
      onAbort: (failure) => {
        // 守卫中止(如未登录),跳转到安全页面
        console.warn('冷启动守卫中止:', failure.code)
        router.relaunch({ name: 'home' })
      }
    })
  })
})

必须传入 options.path

onLaunch 触发时页面栈为空,router.currentRoute 仍是 START_LOCATION(path 为 /)。若调用 guardRoute(undefined),守卫会校验 / 而非真实入口页面,导致基于 to.path / to.name / to.meta 的守卫逻辑失效。

options.path 由 uni-app 框架在 onLaunch 时传入(不含前导 /,需手动补全),各端均可用。

守卫结果处理

守卫结果行为
放行(return undefined / return true不执行导航,resolve 目标路由
重定向(return location按守卫指定的方式(默认 relaunch)跳转到重定向目标
中止(return false调用 onAbort 回调,并 reject NavigationFailure

冷启动无法真正"阻止进入"

冷启动场景下页面已加载,guardRoute() 无法真正阻止页面显示。当守卫中止时,通过 onAbort 回调执行 router.relaunch() 跳转到安全页面是推荐的应对方式。

与 syncRoute 的区别

方法作用执行守卫
syncRoute()同步 currentRoute 为真实页面栈状态
guardRoute()对当前路由补执行守卫链

两者可配合使用:

  • syncRoute:物理返回后的状态同步
  • guardRoute:冷启动时的守卫补执行

详见 Router 实例 - guardRoute()

守卫类型定义

ts
// 守卫返回值类型
type NavigationGuardReturn = void | undefined | boolean | RouteLocationRaw | NavigationRedirect | Error | null

// 可控重定向结果
interface NavigationRedirect {
  location: RouteLocationRaw
  mode?: NavigationRedirectMode
}

// 前置守卫(返回值模式,推荐)
type NavigationGuard = (
  to: RouteLocation,
  from: RouteLocation,
) => NavigationGuardReturn | Promise<NavigationGuardReturn>

// 重定向方式
type NavigationRedirectMode = 'push' | 'replace' | 'relaunch'

// 后置钩子(接收 failure 参数)
type PostNavigationGuard = (
  to: RouteLocation,
  from: RouteLocation,
  failure?: NavigationFailure | null
) => void

// 组件内离开守卫
type RouteLeaveGuard = (
  to: RouteLocation,
  from: RouteLocation,
) => NavigationGuardReturn | Promise<NavigationGuardReturn>

// 返回守卫的返回值类型(false 阻止返回,true / undefined 放行)
type BackGuardReturn = boolean | void | Promise<boolean | void>

// 返回守卫函数类型
type BackGuard = (to: RouteLocation, from: RouteLocation) => BackGuardReturn

onBeforeRouteLeave 组件内离开守卫

onBeforeRouteLeave 是一个组合式 API,用于在组件内部注册离开守卫,适用于需要在离开当前页面时进行确认或清理的场景。

基本用法

在组件的 <script setup> 中使用:

ts
import { onBeforeRouteLeave } from '@meng-xi/uni-router'

onBeforeRouteLeave((to, from) => {
  // 不返回值或 return true 表示放行
  // return false 中止导航
  // return { name: '...' } 重定向
})

实现原理

onBeforeRouteLeave 内部通过 router.beforeEach 注册守卫,并在组件卸载时自动移除,因此不会造成内存泄漏。

示例:离开确认对话框

ts
import { ref } from 'vue'
import { onBeforeRouteLeave } from '@meng-xi/uni-router'

const isDirty = ref(false)

onBeforeRouteLeave((to, from) => {
  if (isDirty.value) {
    return new Promise((resolve) => {
      uni.showModal({
        title: '提示',
        content: '有未保存的修改,确认离开?',
        success: (res) => {
          if (res.confirm) {
            isDirty.value = false
            resolve(true)  // 放行
          } else {
            resolve(false)  // 中止
          }
        }
      })
    })
  }
})

示例:离开时保存数据

ts
import { onBeforeRouteLeave } from '@meng-xi/uni-router'

onBeforeRouteLeave(async (to, from) => {
  if (store.hasUnsavedChanges) {
    try {
      await store.save()
      uni.showToast({ title: '已保存', icon: 'success' })
    } catch (err) {
      uni.showToast({ title: '保存失败', icon: 'none' })
      return false  // 保存失败,阻止离开
    }
  }
})

注意事项

  • onBeforeRouteLeave 仅对当前组件所在页面生效,不会影响其他页面或全局导航
  • 守卫会通过 router.beforeEach 实现,因此遵循返回值模式(不支持 next() 回调)
  • 组件卸载时守卫自动移除,无需手动清理

最佳实践

1. 守卫职责单一

ts
// ✅ 每个守卫只做一件事
router.beforeEach(checkAuth)
router.beforeEach(checkPermission)
router.beforeEach(checkMaintenance)

// ❌ 一个守卫做所有事
router.beforeEach((to, from) => {
  // 100 行混合逻辑...
})

2. 使用返回值模式

ts
// ✅ 推荐:返回值模式,清晰简洁
router.beforeEach(async (to, from) => {
  const ok = await check()
  if (!ok) return { name: 'login' }
})

3. 重定向加终止条件

ts
// ✅ 避免循环
router.beforeEach((to, from) => {
  if (to.name === 'login' && isLoggedIn()) {
    return { name: 'home' } // 已登录访问登录页 → 跳首页
  }
  if (to.meta.requireAuth && !isLoggedIn()) {
    return { name: 'login' } // 未登录访问受保护页 → 跳登录页
  }
})

4. 数据预取放 beforeResolve

ts
// ✅ 前置校验通过后再预取
router.beforeResolve(async (to) => {
  await preloadData(to)
})

// ❌ 放 beforeEach 会阻塞其他守卫

下一步

Released under the MIT License.