uni-router v1.8.0新增冷启动守卫补执行

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 中能获得完整的类型提示。


一、问题分析

冷启动守卫盲区

以登录拦截为例,典型的全局守卫如下:

typescript 复制代码
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

  1. uni-app 框架直接加载 /pages/protected/index 页面(不经过 router.push
  2. 路由器的 beforeEach 守卫从未被调用
  3. isLoggedIn() 校验逻辑未执行
  4. 用户成功进入受保护页面 ------ 权限校验完全失效

这种问题在小程序的扫码进入、App 的 deeplink 跳转中同样存在。

旧版 API 错误类型的局限

v1.7.0 及之前,NavigationFailure.cause 类型为 unknown

typescript 复制代码
export interface NavigationFailure extends RouterError {
	readonly to: RouteLocation
	readonly from: RouteLocation
	readonly cause?: unknown // ❌ 无法获得类型提示
}

uni.navigateTo 等 API 调用失败时,cause 实际上是内部 UniApiError 类的实例(包含 apicause.errMsg),但类型为 unknown,开发者无法获得字段提示,需要手动断言:

typescript 复制代码
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() 冷启动守卫检查

方法签名

typescript 复制代码
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() 执行完整的守卫链:beforeEachbeforeEnterbeforeResolve(不执行 afterEach,因为未发生实际导航)。

核心实现

typescript 复制代码
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 类型导出

新增类型

typescript 复制代码
/**
 * 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
}
typescript 复制代码
export interface NavigationFailure extends RouterError {
	readonly to: RouteLocation
	readonly from: RouteLocation
	/**
	 * 原始错误原因
	 *
	 * 仅当 code 为 NAVIGATION_API_ERROR 时存在,包含失败的 API 名称和原始错误信息。
	 */
	readonly cause?: UniApiError // ✅ 从 unknown 收紧为 UniApiError
}

isUniApiError() 改为类型守卫

typescript 复制代码
// 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 中执行冷启动守卫检查

最典型的应用场景。在应用启动时对当前已加载页面补执行守卫链,未通过则重定向到首页:

vue 复制代码
<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 中处理:

typescript 复制代码
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 参数检查指定路由,而非当前页面:

typescript 复制代码
// 检查某个特定路由的守卫状态(不导航)
const result = await router.guardRoute('/pages/protected/index', {
	onAbort: () => {
		console.log('该路由守卫未通过')
	}
})
console.log('守卫放行:', result.fullPath)

场景四:完整捕获 API 错误信息

v1.8.0 收紧类型后,catch 块中可直接获得 apierrMsg 的类型提示:

typescript 复制代码
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 及之前):

typescript 复制代码
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 定义的对象类型:

typescript 复制代码
// 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 属性)。

将 location 计算逻辑提取为 computed,无附加选项(animation / events / persistent 均未传)时直接使用 to,避免无谓的对象包装:

typescript 复制代码
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

typescript 复制代码
// v1.8.0
interface UniNavigateToOption {
	// ...
	fail?: (err: UniApiCause) => void // ✅ 从 unknown 收紧
}

4. 守卫混用模式警告

当守卫同时调用 next() 并返回 Promise 时输出警告:

typescript 复制代码
// ❌ 不推荐的混用模式
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
isUniApiError() 返回值 boolean boolean(实现为类型守卫,类型收窄)
守卫同时用 next() 和 Promise 静默吞掉错误 输出警告
ParamValue 对象分支 递归 ParamObject object(兼容 interface 对象)

新增类型导出

  • GuardRouteOptions --- guardRoute() 方法的选项类型
  • UniApiCause --- uni API fail 回调的错误原因类型
  • UniApiError --- uni API 调用失败的错误信息类型

新增 API

  • router.guardRoute(location?, options?) --- 冷启动守卫检查

冷启动守卫检查最佳实践

建议在 App.vueonLaunch 中统一处理冷启动守卫检查:

typescript 复制代码
// 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() 同步路由状态)。

相关推荐
xiaok1 小时前
部署之后,本地浏览器还在读取旧缓存导致页面一直显示loading中
前端
用户059540174461 小时前
Redis缓存一致性踩坑实录:线上故障排查6小时,我用pytest+内存快照把它永久关进了笼子
前端·css
星栈1 小时前
我用 Rust + Dioxus 做了个全栈跨平台笔记应用:第一版先把列表和详情跑通
前端·rust·前端框架
用户1733598075371 小时前
Vue 3 SPA 首屏优化:从 3s 到 1.2s 的 5 个实践
前端·vue.js
咖啡无伴侣1 小时前
基础骨架:30 分钟搭好 pnpm workspace,完成双项目 Monorepo 迁入
前端
谷无姜2 小时前
Webpack5 进阶思考:那些官方文档没讲清楚的事
前端·webpack
weedsfly2 小时前
还在用 Axios?你可能需要重新理解 XHR 与 Fetch
前端·javascript·面试
CoderWeen2 小时前
从零实现一个 Vue3 流程图编辑器:节点拖拽、贝塞尔连线与框选
前端·javascript
森鹿2 小时前
express中间件原理以及大致实现
前端·express