v1.8.0 新增
guardRoute()方法解决冷启动场景下守卫未执行的问题,同时导出UniApiError/UniApiCause类型并收紧NavigationFailure.cause类型定义
前言
uni-router 的路由守卫(beforeEach / beforeEnter / beforeResolve / afterEach)在通过 router.push / replace / relaunch / back 触发的导航中能完整执行,确保权限校验、登录拦截等逻辑生效。
然而在冷启动场景下,守卫链存在盲区:
- H5 平台:用户直接在浏览器地址栏输入 URL 访问某个页面
- 小程序平台:用户通过分享链接、扫码、场景值等直接打开某个页面
- App 平台:用户通过 deeplink / scheme 直接跳转到某个页面
这些场景下,页面由 uni-app 框架直接加载,不经过路由器的导航流程,守卫未执行。这意味着:用户可能直接进入一个需要登录的页面,而 beforeEach 中的登录校验完全失效。
v1.8.0 通过新增 guardRoute() 方法解决了这一问题:在应用启动后对当前已加载页面补执行一次守卫链,按守卫结果决定是否重定向到安全页面。
同时,本次更新还完善了 uni API 调用失败时的错误类型定义,将原本内部的 UniApiError / UniApiCause 类型导出,并将 NavigationFailure.cause 类型从 unknown 收紧为 UniApiError,让开发者在 catch
中能获得完整的类型提示。
一、问题分析
冷启动守卫盲区
以登录拦截为例,典型的全局守卫如下:
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth && !isLoggedIn()) {
next({ name: 'login', query: { redirect: to.fullPath } })
} else {
next()
}
})
在正常导航(router.push('/pages/protected/index'))中,守卫会检查 requireAuth,未登录时重定向到登录页。
但若用户在 H5 端直接在浏览器地址栏输入 https://example.com/#/pages/protected/index:
- uni-app 框架直接加载
/pages/protected/index页面(不经过router.push) - 路由器的
beforeEach守卫从未被调用 isLoggedIn()校验逻辑未执行- 用户成功进入受保护页面 —— 权限校验完全失效
这种问题在小程序的扫码进入、App 的 deeplink 跳转中同样存在。
旧版 API 错误类型的局限
v1.7.0 及之前,NavigationFailure.cause 类型为 unknown:
export interface NavigationFailure extends RouterError {
readonly to: RouteLocation
readonly from: RouteLocation
readonly cause?: unknown // ❌ 无法获得类型提示
}
当 uni.navigateTo 等 API 调用失败时,cause 实际上是内部 UniApiError 类的实例(包含 api 和 cause.errMsg),但类型为 unknown,开发者无法获得字段提示,需要手动断言:
try {
await router.push('/pages/detail/index')
} catch (e) {
if (e instanceof NavigationFailure && e.code === RouterErrorCode.NAVIGATION_API_ERROR) {
const cause = e.cause as UniApiError // ❌ 需要手动断言
console.log(cause.api) // navigateTo
console.log(cause.cause.errMsg) // navigateTo:fail ...
}
}
二、新增能力
1. guardRoute() 冷启动守卫检查
方法签名
interface GuardRouteOptions {
/**
* 守卫中止时的回调
*
* 冷启动场景下页面已加载,无法真正"阻止进入"。
* 当守卫调用 next(false) 中止时,将调用此回调并传入 NavigationFailure 对象。
* 用户可在此回调中执行 router.relaunch() 等操作跳转到安全页面。
*/
onAbort?: (failure: NavigationFailure) => void
}
class Router {
guardRoute(location?: RouteLocationRaw, options?: GuardRouteOptions): Promise<RouteLocation>
}
行为说明
| 守卫结果 | 行为 |
|---|---|
守卫放行(next()) | 不执行任何导航(页面已加载),resolve 目标路由 |
守卫重定向(next(location)) | 按守卫指定方式(默认 relaunch,清空栈避免返回受保护页面)跳转到重定向目标 |
守卫中止(next(false)) | 调用 onAbort 回调(若提供),并 reject NavigationFailure |
guardRoute() 执行完整的守卫链:beforeEach → beforeEnter → beforeResolve(不执行 afterEach,因为未发生实际导航)。
核心实现
async guardRoute(location?: RouteLocationRaw, options?: GuardRouteOptions): Promise<RouteLocation> {
const target = location ? this.matcher.resolve(location) : this.routeState.getCurrentRoute()
const from = this.routeState.getCurrentRoute()
// beforeEach
const beforeResult = await this.guardManager.runBeforeGuards(target, from)
const handled = this.handleGuardRouteResult(beforeResult, target, from, options)
if (handled) return handled
// beforeEnter
const config = this.matcher.getRouteConfig(target.path)
if (config?.beforeEnter) {
const beforeEnterResult = await this.guardManager.runBeforeEnterGuards(target, from, config)
const handledEnter = this.handleGuardRouteResult(beforeEnterResult, target, from, options)
if (handledEnter) return handledEnter
}
// beforeResolve
const beforeResolveResult = await this.guardManager.runBeforeResolveGuards(target, from)
const handledResolve = this.handleGuardRouteResult(beforeResolveResult, target, from, options)
if (handledResolve) return handledResolve
// 所有守卫放行,不导航(页面已加载)
return target
}
与 handleGuardResult 不同,handleGuardRouteResult 在守卫放行时不执行导航(页面已加载),仅在重定向时委托给 push / replace / relaunch 执行实际跳转,且重定向默认方式为
relaunch(而非沿用原始导航方式,因为冷启动场景下没有"原始导航方式")。
2. UniApiError / UniApiCause 类型导出
新增类型
/**
* uni-app API 失败时的错误原因
*
* uni-app 导航 API(navigateTo / redirectTo 等)的 fail 回调始终传入此结构的错误对象。
*/
export interface UniApiCause {
/** 错误描述信息 */
errMsg: string
}
/**
* uni-app API 调用失败的错误信息
*
* 包含失败的 API 名称和原始错误原因,作为 NavigationFailure.cause 传递。
*/
export interface UniApiError {
/** 调用失败的 API 名称(如 navigateTo / redirectTo) */
readonly api: string
/** 原始错误原因 */
readonly cause: UniApiCause
}
NavigationFailure.cause 类型收紧
export interface NavigationFailure extends RouterError {
readonly to: RouteLocation
readonly from: RouteLocation
/**
* 原始错误原因
*
* 仅当 code 为 NAVIGATION_API_ERROR 时存在,包含失败的 API 名称和原始错误信息。
*/
readonly cause?: UniApiError // ✅ 从 unknown 收紧为 UniApiError
}
isUniApiError() 改为类型守卫
// v1.7.0:仅返回 boolean
export function isUniApiError(error: unknown): boolean {
return error instanceof UniApiError
}
// v1.8.0:类型守卫,便于 instanceof 后的类型收窄
export function isUniApiError(error: unknown): error is UniApiError {
return error instanceof UniApiError
}
三、使用示例
场景一:App.vue onLaunch 中执行冷启动守卫检查
最典型的应用场景。在应用启动时对当前已加载页面补执行守卫链,未通过则重定向到首页:
<script>
import router from './router'
import { RouterErrorCode } from '@meng-xi/uni-router'
export default {
onLaunch() {
// 等待路由器初始化完成
router.isReady().then(() => {
router
.guardRoute(undefined, {
onAbort: failure => {
console.warn('[guardRoute] 冷启动守卫中止:', failure.code)
if (failure.code === RouterErrorCode.NAVIGATION_ABORTED) {
// 守卫中止(如未登录),跳转到首页
router.relaunch('/pages/index/index')
}
}
})
.catch(() => {
// guardRoute 中止时 reject,已在 onAbort 中处理
})
})
}
}
</script>
场景二:守卫重定向自动跳转
当守卫调用 next(location) 重定向时,guardRoute() 会自动执行跳转,无需在 onAbort 中处理:
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth && !isLoggedIn()) {
// guardRoute() 检测到重定向时,自动用 relaunch 跳转到登录页
next({ name: 'login', query: { redirect: to.fullPath } })
} else {
next()
}
})
// App.vue
router.isReady().then(() => {
router.guardRoute() // 未登录用户进入受保护页面时,自动 relaunch 到登录页
})
场景三:检查指定路由(非当前页面)
guardRoute() 可传入 location 参数检查指定路由,而非当前页面:
// 检查某个特定路由的守卫状态(不导航)
const result = await router.guardRoute('/pages/protected/index', {
onAbort: () => {
console.log('该路由守卫未通过')
}
})
console.log('守卫放行:', result.fullPath)
场景四:完整捕获 API 错误信息
v1.8.0 收紧类型后,catch 块中可直接获得 api 和 errMsg 的类型提示:
import { NavigationFailure, RouterErrorCode } from '@meng-xi/uni-router'
try {
await router.push('/pages/detail/index')
} catch (e) {
if (e instanceof NavigationFailure && e.code === RouterErrorCode.NAVIGATION_API_ERROR) {
const cause = e.cause // ✅ 类型为 UniApiError | undefined,无需手动断言
if (cause) {
console.log('失败的 API:', cause.api) // navigateTo
console.log('错误信息:', cause.cause.errMsg) // navigateTo:fail ...
}
}
}
对比旧版(v1.7.0 及之前):
try {
await router.push('/pages/detail/index')
} catch (e) {
if (e instanceof NavigationFailure && e.code === RouterErrorCode.NAVIGATION_API_ERROR) {
// ❌ cause 类型为 unknown,无法获得字段提示
const cause = e.cause as { api: string; cause: { errMsg: string } }
console.log('失败的 API:', cause.api)
}
}
四、其他优化与修复
1. ParamValue 类型兼容性增强
ParamValue 的对象分支从递归 ParamObject 改为 object,兼容 interface 定义的对象类型:
// v1.7.0
export type ParamValue = string | number | boolean | null | ParamObject | ParamValue[]
// ❌ interface 定义的对象没有索引签名,无法赋值给 ParamObject
// v1.8.0
export type ParamValue = string | number | boolean | null | undefined | ParamValue[] | object
// ✅ object 分支兼容所有对象类型,包括 interface 定义的对象
同时添加 undefined 分支,兼容含可选属性的对象(JSON.stringify 会自动忽略 undefined 属性)。
2. RouterLink 组件重构
将 location 计算逻辑提取为 computed,无附加选项(animation / events / persistent 均未传)时直接使用 to,避免无谓的对象包装:
const location = computed<RouteLocationRaw>(() => {
// 无附加选项时直接使用 to,避免无谓的对象包装
if (!props.animation && !props.events && !props.params && props.persistent === undefined) {
return props.to
}
// ...合并附加选项
})
3. uni API fail 回调类型收紧
env.d.ts 中各导航 API(navigateTo / redirectTo / switchTab / reLaunch / navigateBack)的 fail 回调参数类型从 unknown 收紧为 UniApiCause:
// v1.8.0
interface UniNavigateToOption {
// ...
fail?: (err: UniApiCause) => void // ✅ 从 unknown 收紧
}
4. 守卫混用模式警告
当守卫同时调用 next() 并返回 Promise 时输出警告:
// ❌ 不推荐的混用模式
router.beforeEach((to, from, next) => {
someAsyncOperation().then(() => {
next() // next() 在 Promise 中调用
})
return somePromise // 同时返回 Promise
})
// 控制台警告:Navigation guard "..." called next() and also returned a Promise.
// Use either next() callback or async/await, not both.
next() 之后的异步错误会被静默吞掉,开发者应选择其中一种解析模式:next() 回调或 async/await,不可混用。
5. syncCurrentRoute 参数清理
移除 syncRoute() 内部未使用的 _from 参数。
升级指南
v1.8.0 是向后兼容的新功能版本,无需修改任何现有代码即可升级。
行为变化
| 场景 | v1.7.0 及之前 | v1.8.0 |
|---|---|---|
| 冷启动进入受保护页面 | 守卫未执行(盲区) | 可通过 guardRoute() 补执行守卫链 |
NavigationFailure.cause 类型 | unknown | UniApiError | undefined |
isUniApiError() 返回值 | boolean | boolean(实现为类型守卫,类型收窄) |
守卫同时用 next() 和 Promise | 静默吞掉错误 | 输出警告 |
ParamValue 对象分支 | 递归 ParamObject | object(兼容 interface 对象) |
新增类型导出
GuardRouteOptions—guardRoute()方法的选项类型UniApiCause— uni APIfail回调的错误原因类型UniApiError— uni API 调用失败的错误信息类型
新增 API
router.guardRoute(location?, options?)— 冷启动守卫检查
冷启动守卫检查最佳实践
建议在 App.vue 的 onLaunch 中统一处理冷启动守卫检查:
// App.vue
onLaunch() {
router.isReady().then(() => {
router.guardRoute(undefined, {
onAbort: failure => {
// 守卫中止时,重定向到首页或其他安全页面
if (failure.code === RouterErrorCode.NAVIGATION_ABORTED) {
router.relaunch('/pages/index/index')
}
}
})
})
}
注意:guardRoute() 仅在应用启动时调用一次即可,无需在每次 onShow 中调用(onShow 触发时可使用 router.syncRoute() 同步路由状态)。