v2.1.0 将导航守卫全面升级为返回值模式(与 Vue Router 4.x 一致),通过
return undefined/return false/return RouteLocationRaw控制导航行为,无需调用next()回调。旧版next()回调模式保持兼容,标记为已弃用。
前言
@meng-xi/uni-router 的守卫系统自 v1.0 起一直使用 next() 回调模式控制导航行为。Vue Router 4.x 已全面移除 next() 回调,改为返回值模式,代码更简洁、更符合 async/await 风格。
v2.1.0 将守卫升级为返回值模式,同时保持对旧版 next() 回调的完全兼容,提供平滑迁移路径。
一、问题分析
1. next() 回调容易忘记调用
typescript
// v2.0.x --- 忘记调用 next() 导致导航挂起
router.beforeEach((to, from, next) => {
const valid = await checkToken()
if (!valid) {
// 忘记调用 next(),导航永久挂起
}
next()
})
2. 回调嵌套使代码冗长
typescript
// v2.0.x --- 回调嵌套,可读性差
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth) {
checkAuth(result => {
if (result) {
next()
} else {
next({ name: 'login' })
}
})
} else {
next()
}
})
3. afterEach 无法区分导航成功/失败
typescript
// v2.0.x --- afterEach 不知道导航是否成功
router.afterEach((to, from) => {
// 无法判断导航是否被守卫中止
// 无法判断 uni API 调用是否失败
})
二、新增能力
1. 守卫返回值模式
v2.1.0 引入 Vue Router 4.x 风格的返回值模式,守卫通过返回值控制导航行为:
typescript
// v2.1.0 --- 返回值模式
router.beforeEach((to, from) => {
if (to.meta.requireAuth && !isLoggedIn()) {
return { name: 'login' } // 重定向
}
// 不返回值或 return true 表示放行
})
// 异步守卫
router.beforeEach(async (to, from) => {
const valid = await checkToken()
if (!valid) return false // 中止
})
返回值对照表
| 返回值 | 行为 |
|---|---|
undefined / void / true |
放行,继续执行下一个守卫 |
false |
中止导航(NAVIGATION_ABORTED) |
string(如 '/login') |
重定向到路径 |
RouteLocationRaw(如 { name: 'login' }) |
重定向到路由位置 |
Error 对象 |
取消导航(NAVIGATION_CANCELLED) |
| 抛出异常 | 取消导航(NAVIGATION_CANCELLED) |
2. 可控重定向的返回值写法
typescript
// v2.1.0 --- 通过返回值中的 mode 字段指定重定向方式
router.beforeEach((to, from) => {
if (to.meta.requireAuth && !isLoggedIn()) {
return { location: { name: 'login' }, mode: 'replace' }
}
if (to.meta.roles && !hasRole(to.meta.roles)) {
return { location: { name: 'home' }, mode: 'relaunch' }
}
})
3. afterEach 接收 failure 参数
typescript
// v2.1.0 --- afterEach 可区分导航成功/失败
router.afterEach((to, from, failure) => {
if (failure) {
console.error('导航失败:', failure.message)
return
}
// 导航成功,设置页面标题
if (to.meta.title) {
uni.setNavigationBarTitle({ title: to.meta.title as string })
}
})
4. NavigationGuardReturn 类型
typescript
type NavigationGuardReturn = void | undefined | boolean | RouteLocationRaw | Error | null
三、Bug 修复
1. next() 未调用导致导航挂起
修复前 :next() 回调模式中,忘记调用 next() 会导致导航永久挂起,需要超时机制兜底,但超时后中止导航而非放行。
修复后 :返回值模式中,不返回值等同于 return undefined,自动放行。旧版回调模式保持超时保护。
2. 守卫中止后 afterEach 缺少失败信息
修复前 :守卫中止导航时,afterEach 无法获取 NavigationFailure 信息。
修复后 :守卫中止、uni API 调用失败等场景,afterEach 的第三个参数 failure 会传入对应的 NavigationFailure 实例。
四、架构设计
守卫模式自动检测
通过函数参数个数 guard.length 自动识别守卫模式:
vbnet
guard.length >= 3 → (to, from, next) → 回调模式(兼容旧版)
guard.length < 3 → (to, from) → 返回值模式(推荐)
typescript
function runGuard(guard, to, from, timeout) {
const useNextCallback = guard.length >= 3
if (useNextCallback) {
return runGuardWithNext(guard, to, from, timeout)
}
return runGuardWithReturn(guard, to, from, timeout)
}
返回值模式执行流程
javascript
守卫执行
├── 返回值 = undefined / true / null → 放行
├── 返回值 = false → 中止(NAVIGATION_ABORTED)
├── 返回值 = RouteLocationRaw → 重定向
├── 返回值 = Error → 取消(NAVIGATION_CANCELLED)
├── 抛出异常 → 取消(NAVIGATION_CANCELLED)
└── 超时 → 取消(NAVIGATION_CANCELLED)
混用检测
同时使用 next() 回调和返回值的守卫,会在控制台输出警告:
scss
Navigation guard "guardName" called next() and also returned a value.
Use either next() callback or return value, not both.
五、完整示例
基础导航守卫
typescript
import { createRouter } from '@meng-xi/uni-router'
const router = createRouter({
routes: [
{ path: 'pages/index/index', name: 'home' },
{ path: 'pages/login/login', name: 'login' },
{ path: 'pages/protected/protected', name: 'protected', meta: { requireAuth: true } }
]
})
// 返回值模式(推荐)
router.beforeEach((to, from) => {
if (to.meta.requireAuth && !isLoggedIn()) {
return { name: 'login' }
}
})
// 异步守卫
router.beforeEach(async (to, from) => {
const user = await fetchUser()
if (to.meta.roles && !user.roles.includes(to.meta.roles)) {
return { name: '403' }
}
})
// 后置钩子(接收 failure 参数)
router.afterEach((to, from, failure) => {
if (failure) {
console.error('导航失败:', failure.message)
return
}
console.log(`导航成功: ${from.path} → ${to.path}`)
})
可控重定向
typescript
router.beforeEach((to, from) => {
if (to.name === 'protected' && !isLoggedIn()) {
// replace 模式:登录后不保留受保护页面的历史
return { location: { name: 'login' }, mode: 'replace' }
}
if (to.meta.roles && !hasRole(to.meta.roles)) {
// relaunch 模式:清空栈回到首页
return { location: { name: 'home' }, mode: 'relaunch' }
}
})
离开确认
typescript
router.beforeEach((to, from) => {
if (from.meta.dirty) {
return new Promise(resolve => {
uni.showModal({
title: '提示',
content: '有未保存的修改,确认离开?',
success: res => {
resolve(res.confirm ? true : false)
}
})
})
}
})
六、升级指南
v2.1.0 完全向后兼容,无需修改现有代码即可升级。
推荐迁移
推荐逐步将守卫从 next() 回调模式迁移到返回值模式:
typescript
// 迁移前
router.beforeEach((to, from, next) => {
if (condition) {
next({ name: 'login' })
} else {
next()
}
})
// 迁移后
router.beforeEach((to, from) => {
if (condition) {
return { name: 'login' }
}
})
新旧对照表
| 场景 | 旧版 next() 回调 |
新版返回值 |
|---|---|---|
| 放行 | next() |
return undefined 或不写 |
| 放行(显式) | next() |
return true |
| 中止 | next(false) |
return false |
| 重定向 | next({ name: 'login' }) |
return { name: 'login' } |
| 重定向+方式 | next({ name: 'login' }, { mode: 'replace' }) |
return { location: { name: 'login' }, mode: 'replace' } |
| 抛出错误 | next(new Error('msg')) |
throw new Error('msg') |
| 返回错误 | --- | return new Error('msg') |
不需要改动
- 使用
next()回调的旧守卫代码无需修改,保持完全兼容 - 守卫注册 API(
router.beforeEach/beforeResolve/afterEach/beforeEnter)签名不变 - 守卫移除函数(返回值)不受影响
- 超时配置(
guardTimeout)不受影响
版本兼容性
| 功能 | v2.0.x | v2.1.0 |
|---|---|---|
next() 回调模式 |
支持 | 支持(已弃用) |
| 返回值模式 | 不支持 | 支持 |
afterEach 接收 failure |
不支持 | 支持 |
| 混用检测警告 | 无 | 有 |