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

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 设计

typescript 复制代码
export type NavigationRedirectMode = 'push' | 'replace' | 'relaunch'

export interface NavigationRedirect {
	/** 重定向目标路由位置 */
	location: RouteLocationRaw
	/** 重定向使用的导航方式,不指定时沿用原始导航方式 */
	mode?: NavigationRedirectMode
}
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 / 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. 运行时检测

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 透出 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 会出现双 ?

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 支持 支持
插件系统 支持 支持
相关推荐
阿萨德528号1 小时前
npm 包发布实战指南:从零发布、更新迭代到版本治理
前端·npm·策略模式
IT_陈寒2 小时前
Vue的v-for为啥把我的渲染顺序搞乱套了?
前端·人工智能·后端
sir.山2 小时前
在 Vue 3 项目中使用 Mock 数据
前端·vue.js·vue3·vite
李剑一2 小时前
华为新上Pura X View阔直板手机,特殊屏幕比例设备下,前端应该怎么去适配更完美?送你一套完整工具代码
前端
0xBADCODE2 小时前
Flask SSTI读SECRET_KEY+伪造Session:税务系统渗透全流程
前端·后端·python·安全·web安全·网络安全·flask
01_ice3 小时前
html个人简历展示
前端·html
程序员爱钓鱼3 小时前
Rust where详解:让泛型与Trait约束更加清晰
前端·后端·rust
zzzzzz3104 小时前
react-bits 为什么会吸引前端开发者:别把动效当装饰,先把它当成页面能力
前端·react.js·动效
风月说与山鬼5 小时前
二、React入口文件(main.jsx)
前端·react.js