路由守卫
路由守卫是 Uni Router 的核心能力,允许在导航过程中插入自定义逻辑:鉴权、日志、数据预取、离开确认等。本章将深入讲解守卫的执行机制和返回值模式(推荐)。
守卫全景
Uni Router 提供五类守卫。前进导航的执行顺序:
导航触发
│
├─ 1. beforeEach 全局前置守卫(可多个)
│ └─ 可中止 / 重定向 / 放行
│
├─ 2. beforeEnter 路由独享守卫(配置在 RouteConfig)
│ └─ 可中止 / 重定向 / 放行
│
├─ 3. beforeResolve 全局解析守卫(可多个)
│ └─ 可中止 / 重定向 / 放行
│
├─ 4. uni 导航 API 调用 navigateTo / redirectTo / ...
│
└─ 5. afterEach 全局后置钩子(可多个)
└─ 仅观察,无法改变导航结果返回操作(物理返回键 / 浏览器后退 / router.back())先执行 onBeforeBack 返回守卫,放行后复用 beforeEach → beforeResolve → afterEach 链路,详见返回守卫。
各守卫的定位
| 守卫 | 注册方式 | 典型场景 |
|---|---|---|
beforeEach | router.beforeEach(fn) | 登录鉴权、权限检查、全局日志 |
beforeEnter | RouteConfig.beforeEnter | 某路由的专属校验(如需读取特定数据) |
beforeResolve | router.beforeResolve(fn) | 数据预取完成后的最终确认 |
afterEach | router.afterEach(fn) | 设置标题、埋点、清理状态,接收失败信息 |
onBeforeBack | router.onBeforeBack(fn) | 返回拦截、离开确认(物理返回键 / 浏览器后退 / router.back()) |
beforeResolve 的定位
beforeResolve 在 beforeEnter 之后执行,此时所有前置校验已通过。适合放"所有守卫都同意后"的最终逻辑,如确认数据已加载完毕。它与 beforeEach 的区别仅在于执行时机。
注册守卫
全局守卫
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()路由独享守卫
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
router.beforeEach((to, from) => {
return true // 放行,继续执行下一个守卫
})
// 不写 return 也等同于放行
router.beforeEach((to, from) => {
// 默认放行
})2. 中止:return false
router.beforeEach((to, from) => {
if (isOffline()) {
uni.showToast({ title: '网络不可用', icon: 'none' })
return false // 中止导航,停留在当前页
}
})中止会抛出 NavigationFailure(NAVIGATION_ABORTED)。
3. 重定向:return RouteLocationRaw
router.beforeEach((to, from) => {
if (to.meta.requireAuth && !isLoggedIn()) {
// 重定向到登录页,携带原目标用于登录后跳回
return { name: 'login', query: { redirect: to.fullPath } }
}
})重定向会重新触发完整的守卫链(从 beforeEach 开始),并增加重定向深度计数。
4. 抛出错误中止
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 } 对象,可显式指定重定向使用的导航方式。
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 取值
type NavigationRedirectMode = 'push' | 'replace' | 'relaunch'| mode | 对应 uni API | 适用场景 |
|---|---|---|
'push' | navigateTo | 登录后需返回原页面,保留目标页在栈中 |
'replace' | redirectTo | 替换当前页,不留历史(如登录页) |
'relaunch' | reLaunch | 清空栈(如权限不足时回到首页) |
实战:登录重定向
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)
}实战:权限不足清栈
router.beforeEach((to, from) => {
if (to.meta.roles && !hasRole(to.meta.roles)) {
// 权限不足,清空栈回到首页
return { location: { name: 'home' }, mode: 'relaunch' }
}
})异步守卫
守卫支持 async 函数和返回 Promise:
router.beforeEach(async (to, from) => {
// 异步校验 token 有效性
const valid = await checkToken()
if (!valid) {
return { name: 'login' } // 重定向到登录页
}
// 放行
})Promise reject 中止导航
router.beforeEach(async (to, from) => {
try {
await fetchUserProfile()
// 放行
} catch (err) {
// reject 会中止导航(NAVIGATION_CANCELLED)
throw err
}
})返回值 vs 异常抛出
return false→NAVIGATION_ABORTED(用户主动中止)throw/reject→NAVIGATION_CANCELLED(异常导致取消)
建议用 return false 表达"主动中止",用异常表达"意外错误"。
超时保护
守卫可能因异步操作卡住(如网络请求无响应)。Uni Router 提供超时保护:
const router = createRouter({
routes,
guardTimeout: 10000 // 默认 10 秒
})守卫执行
→ 10 秒内未返回结果也未抛出异常
→ 输出警告: "Navigation guard did not resolve within 10s"
→ 自动中止导航 (NAVIGATION_CANCELLED)调整超时
守卫中有耗时请求时调大超时:
const router = createRouter({
routes,
guardTimeout: 30000 // 30 秒
})设为 0 可禁用超时保护(不推荐,可能导致导航永久挂起)。
守卫执行细节
执行顺序
同一类型的多个守卫按注册顺序执行:
router.beforeEach(guard1) // 先执行
router.beforeEach(guard2) // 后执行
router.beforeEach(guard3) // 最后执行guard1 → guard2 → guard3 → beforeEnter → beforeResolve1 → beforeResolve2 → API中止/重定向的短路效应
任一守卫中止或重定向,后续守卫不再执行:
router.beforeEach((to, from) => {
return false // 中止
})
router.beforeEach((to, from) => {
console.log('不会执行')
})重定向重新触发守卫链
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 获取导航失败信息:
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:
syncRoute()/syncCurrentRoute()的状态同步
物理返回键、浏览器后退会经返回守卫链执行,守卫放行后 afterEach 正常触发(见返回守卫)。 如需监听所有路由变化(包括状态同步),使用 onRouteChange。
router.onRouteChange((to, from) => {
// 完整导航和状态同步都会触发
if (to._synced) {
console.log('状态同步(非完整导航)')
}
})实战模式
模式 1:登录鉴权
// 全局前置守卫
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:权限控制
// 扩展 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:离开确认
// 标记页面为"脏"状态
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:数据预取
// 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:页面标题自动设置
router.afterEach((to) => {
const title = to.meta.title as string | undefined
if (title) {
uni.setNavigationBarTitle({ title })
} else {
uni.setNavigationBarTitle({ title: '默认标题' })
}
})模式 6:路由级独享校验
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 是全局返回守卫,在返回操作触发时执行,可用于离开确认、返回拦截等场景。
// 注册返回守卫(返回 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)。
各平台支持情况
| 返回场景 | App | H5 | 小程序 |
|---|---|---|---|
物理返回键 / 导航栏返回 / uni.navigateBack | ✅ onBackPress 接入 | — | ❌ |
| 浏览器后退按钮 / 后退手势 | — | ✅ popstate 接入 | — |
| iOS 边缘滑动返回 | ⚠️ 需配合 setSideSlipGesture('none') | — | — |
router.back() / 程序化 uni.navigateBack | ✅ | ✅ | ✅(需注册 InterceptorPlugin) |
小程序原生返回无法拦截
小程序右上角/左上角返回、物理返回键、滑动返回由宿主控制,无 onBackPress 生命周期、无 popstate 事件,onBeforeBack 无法拦截。这是平台能力边界。
配合 iOS 侧滑手势
iOS 边缘滑动返回默认绕过守卫链。通过 app.setSideSlipGesture 可按页面动态控制手势:
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() 同步路由状态,无需手动调用:
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:
router.onRouteChange((to, from) => {
if (to._synced) {
// 状态同步(未经过守卫链,如小程序原生返回)
handleBackNavigation(to, from)
}
})详见平台兼容性。
冷启动守卫检查
问题:冷启动绕过守卫
当用户通过以下方式直接进入某个页面时,页面由 uni-app 框架直接加载,不经过路由器导航,守卫(beforeEach 等)未执行:
| 场景 | 平台 |
|---|---|
| 直接访问 URL | H5 |
| 扫码进入 / 场景值 | 小程序 |
| Deeplink / URL Scheme | App |
用户访问 https://example.com/#/pages/about/about
→ uni-app 直接加载 about 页
→ 路由器守卫未执行(未经过 router.push)
→ 未登录用户直接进入了 requireAuth 页面解决方案:guardRoute()
router.guardRoute() 对当前(或指定)路由补执行守卫链,按守卫结果决定是否重定向:
// 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:冷启动时的守卫补执行
守卫类型定义
// 守卫返回值类型
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) => BackGuardReturnonBeforeRouteLeave 组件内离开守卫
onBeforeRouteLeave 是一个组合式 API,用于在组件内部注册离开守卫,适用于需要在离开当前页面时进行确认或清理的场景。
基本用法
在组件的 <script setup> 中使用:
import { onBeforeRouteLeave } from '@meng-xi/uni-router'
onBeforeRouteLeave((to, from) => {
// 不返回值或 return true 表示放行
// return false 中止导航
// return { name: '...' } 重定向
})实现原理
onBeforeRouteLeave 内部通过 router.beforeEach 注册守卫,并在组件卸载时自动移除,因此不会造成内存泄漏。
示例:离开确认对话框
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) // 中止
}
}
})
})
}
})示例:离开时保存数据
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. 守卫职责单一
// ✅ 每个守卫只做一件事
router.beforeEach(checkAuth)
router.beforeEach(checkPermission)
router.beforeEach(checkMaintenance)
// ❌ 一个守卫做所有事
router.beforeEach((to, from) => {
// 100 行混合逻辑...
})2. 使用返回值模式
// ✅ 推荐:返回值模式,清晰简洁
router.beforeEach(async (to, from) => {
const ok = await check()
if (!ok) return { name: 'login' }
})3. 重定向加终止条件
// ✅ 避免循环
router.beforeEach((to, from) => {
if (to.name === 'login' && isLoggedIn()) {
return { name: 'home' } // 已登录访问登录页 → 跳首页
}
if (to.meta.requireAuth && !isLoggedIn()) {
return { name: 'login' } // 未登录访问受保护页 → 跳登录页
}
})4. 数据预取放 beforeResolve
// ✅ 前置校验通过后再预取
router.beforeResolve(async (to) => {
await preloadData(to)
})
// ❌ 放 beforeEach 会阻塞其他守卫