v2.4.0 为守卫返回值模式补全了重定向方式控制能力,通过返回
{ location, mode }对象可显式指定重定向使用的导航方式,与 v1.xnext(location, { mode })时代的行为对齐,同时保持与 vue-router 4.x 返回值风格一致。
前言
v2.2.0 守卫系统全面采用返回值模式并移除 next() 回调后,可控重定向能力也随之移除------守卫重定向只能沿用触发导航的原始方式。v2.4.0 在返回值模式下重新引入可控重定向,运行时底层基础设施(GuardResult.mode、路由器 result.mode 处理、guardRoute 冷启动处理)一直保留,本次仅补全类型定义与返回值解析两处,属于"低成本、高价值"的功能补全。
一、API 设计
1. 新增 NavigationRedirect 接口
typescript
export type NavigationRedirectMode = 'push' | 'replace' | 'relaunch'
export interface NavigationRedirect {
/** 重定向目标路由位置 */
location: RouteLocationRaw
/** 重定向使用的导航方式,不指定时沿用原始导航方式 */
mode?: NavigationRedirectMode
}
2. 扩展 NavigationGuardReturn 类型
typescript
export type NavigationGuardReturn = void | undefined | boolean | RouteLocationRaw | NavigationRedirect | Error | null
3. 用法示例
typescript
router.beforeEach((to, from) => {
if (to.meta.requireAuth && !isLoggedIn()) {
// 用 replace 跳转登录页,避免登录页残留在页面栈中
return { location: { name: 'login', query: { redirect: to.fullPath } }, mode: 'replace' }
}
})
二、行为规则
1. 重定向方式优先级
显式 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 无法跳转栈外目标,回退) |
2. mode 取值对照
| mode | 对应 uni API | 适用场景 |
|---|---|---|
'push' |
uni.navigateTo |
登录后需返回原页面,保留目标页在栈中 |
'replace' |
uni.redirectTo |
替换当前页,不留历史(如登录页) |
'relaunch' |
uni.reLaunch |
清空栈(如权限不足回首页) |
3. 边界情况
mode缺省:等价于现有行为(沿用原始导航方式),完全向后兼容mode: 'back'不允许 :NavigationRedirectMode仅含push/replace/relaunchlocation为字符串 :{ location: '/login', mode: 'replace' }同样合法- TabBar 页面 :目标为 TabBar 页面时,最终仍由 uni API 的 TabBar 检测逻辑自动切换为
uni.switchTab(现有机制) - 重定向深度限制 :
MAX_REDIRECT_DEPTH = 10依然生效,防止无限循环 guardRoute冷启动 :自动启用可控重定向(handleGuardRouteResult已处理result.mode ?? 'relaunch')
三、实现细节
1. 类型可区分性
NavigationRedirect 与 RouteLocationRaw 通过顶层字段判别:
| 类型 | 结构 | 顶层字段 |
|---|---|---|
RouteLocationPathRaw |
{ path, query?, ... } |
必须有 path |
RouteLocationNamedRaw |
{ name, query?, ... } |
必须有 name |
NavigationRedirect |
{ location, mode? } |
必须有 location |
2. 运行时检测
typescript
function isRedirect(value: unknown): value is NavigationRedirect {
return typeof value === 'object' && value !== null && 'location' in value
}
3. 返回值解析
typescript
function resolveGuardReturn(value: NavigationGuardReturn): GuardResult {
if (value === false) {
return { type: 'abort', code: RouterErrorCode.NAVIGATION_ABORTED }
}
if (value instanceof Error) {
return { type: 'abort', code: RouterErrorCode.NAVIGATION_CANCELLED }
}
if (value === true || value === undefined || value === null || value === void 0) {
return { type: 'next' }
}
// NavigationRedirect:重定向并指定导航方式
if (isRedirect(value)) {
return { type: 'next', redirect: value.location, mode: value.mode }
}
// 其他值视为 RouteLocationRaw(string 或对象),重定向
return { type: 'next', redirect: value as RouteLocationRaw }
}
4. 导出
types/index.ts透出NavigationRedirectsrc/index.ts导出NavigationRedirect(NavigationRedirectMode已导出,确认保留)
四、测试用例
单元测试(resolveGuardReturn)
| 用例 | 输入 | 期望 |
|---|---|---|
| 显式 mode | { location: { name: 'login' }, mode: 'replace' } |
{ type: 'next', redirect: { name: 'login' }, mode: 'replace' } |
| 缺省 mode | { location: { name: 'login' } } |
{ type: 'next', redirect: { name: 'login' }, mode: undefined } |
| 字符串 location | { location: '/login', mode: 'relaunch' } |
{ type: 'next', redirect: '/login', mode: 'relaunch' } |
| 普通对象 | { name: 'login' } |
{ type: 'next', redirect: { name: 'login' } }(mode 为 undefined) |
| 字符串 | '/login' |
{ type: 'next', redirect: '/login' } |
集成行为
| 场景 | 期望 |
|---|---|
| push 触发 + replace 重定向 | 实际调用 uni.redirectTo |
| back 触发 + 缺省 mode | 回退 uni.reLaunch |
| guardRoute 冷启动 + replace 重定向 | 实际调用 uni.redirectTo |
修复:字符串路径含 query 注入内部 key 产生双 ?
问题描述
当以字符串路径携带 query 导航(如 router.push('/detail?id=1'))且启用了 ChannelPlugin(useUniEventChannel: true)时,插件向 query 注入内部 key __nav_id 的 URL 会出现双 ?:
bash
// 期望
/detail?id=1&__nav_id=nav-...
// 实际
/detail?id=1?__nav_id=nav-...
根因
injectQueryKey 对字符串路径直接返回 { path: location, query: { [key]: value } },把整个字符串(含已有 query)当作 path,又单独附加 query 对象。后续 resolveFromPathRaw 将 ?id=1 视为路径一部分,再单独序列化 query,最终 buildFullPath 拼出双 ?。
修复方案
字符串路径若已含 ?,先按 ? 拆分为 path + 已有 query,再合并注入:
typescript
export function injectQueryKey(location: RouteLocationRaw, key: string, value: string): RouteLocationRaw {
if (typeof location === 'string') {
const queryIndex = location.indexOf('?')
if (queryIndex === -1) {
return { path: location, query: { [key]: value } }
}
// 字符串路径已含 query:拆分为 path + 已有 query,合并注入
const path = location.slice(0, queryIndex)
const existingQuery = parseQuery(location.slice(queryIndex + 1))
return { path, query: { ...existingQuery, [key]: value } }
}
// ...
}
影响范围
injectQueryKey为通用工具,同时惠及 ChannelPlugin(__nav_id)与 ParamsPlugin(__params_key)- 路径对象 / 命名对象分支不受影响(query 本就独立存放)
- 无 query 的字符串路径行为不变,完全向后兼容
五、升级指南
v2.4.0 完全向后兼容,无破坏性变更。现有写法(return { name: 'login' }、return '/login'、return false 等)行为完全不变。新增的 NavigationRedirect 是可选能力,仅在需要显式指定重定向导航方式时使用。
版本兼容性
| 功能 | v2.3.1 | v2.4.0 |
|---|---|---|
普通重定向(return RouteLocationRaw) |
支持 | 支持(行为不变) |
可控重定向(return { location, mode }) |
不支持 | 支持 |
| 守卫返回值模式 | 支持 | 支持 |
| 组合式 API | 支持 | 支持 |
| 插件系统 | 支持 | 支持 |