Vue-Router 2.4.0 新增可控重定向功能

0 阅读4分钟

v2.4.0 为守卫返回值模式补全了重定向方式控制能力,通过返回 { location, mode } 对象可显式指定重定向使用的导航方式,与 v1.x next(location, { mode }) 时代的行为对齐,同时保持与 vue-router 4.x 返回值风格一致。

前言

v2.2.0 守卫系统全面采用返回值模式并移除 next() 回调后,可控重定向能力也随之移除——守卫重定向只能沿用触发导航的原始方式。v2.4.0 在返回值模式下重新引入可控重定向,运行时底层基础设施(GuardResult.mode、路由器 result.mode 处理、guardRoute 冷启动处理)一直保留,本次仅补全类型定义与返回值解析两处,属于"低成本、高价值"的功能补全。


一、API 设计

1. 新增 NavigationRedirect 接口

export type NavigationRedirectMode = 'push' | 'replace' | 'relaunch'

export interface NavigationRedirect {
	/** 重定向目标路由位置 */
	location: RouteLocationRaw
	/** 重定向使用的导航方式,不指定时沿用原始导航方式 */
	mode?: NavigationRedirectMode
}

2. 扩展 NavigationGuardReturn 类型

export type NavigationGuardReturn = void | undefined | boolean | RouteLocationRaw | NavigationRedirect | Error | null

3. 用法示例

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 / relaunch
  • location 为字符串{ location: '/login', mode: 'replace' } 同样合法
  • TabBar 页面:目标为 TabBar 页面时,最终仍由 uni API 的 TabBar 检测逻辑自动切换为 uni.switchTab(现有机制)
  • 重定向深度限制MAX_REDIRECT_DEPTH = 10 依然生效,防止无限循环
  • guardRoute 冷启动:自动启用可控重定向(handleGuardRouteResult 已处理 result.mode ?? 'relaunch'

三、实现细节

1. 类型可区分性

NavigationRedirectRouteLocationRaw 通过顶层字段判别:

类型结构顶层字段
RouteLocationPathRaw{ path, query?, ... }必须有 path
RouteLocationNamedRaw{ name, query?, ... }必须有 name
NavigationRedirect{ location, mode? }必须有 location

2. 运行时检测

function isRedirect(value: unknown): value is NavigationRedirect {
	return typeof value === 'object' && value !== null && 'location' in value
}

3. 返回值解析

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 透出 NavigationRedirect
  • src/index.ts 导出 NavigationRedirectNavigationRedirectMode 已导出,确认保留)

四、测试用例

单元测试(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 会出现双 ?

// 期望
/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,再合并注入:

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.1v2.4.0
普通重定向(return RouteLocationRaw支持支持(行为不变)
可控重定向(return { location, mode }不支持支持
守卫返回值模式支持支持
组合式 API支持支持
插件系统支持支持