V1.9.0重磅更新:自动路由同步与架构优化

v1.9.0 通过全局 mixin 自动调用 syncRoute() 简化页面状态同步,修复 back() 参数丢失与 setCurrentRoute 执行时机问题,并对 router/index.tsinterceptor/index.ts 等拥挤文件进行模块化拆分,统一错误类组织

前言

@meng-xi/uni-router 在过去几个版本中持续完善守卫系统、错误类型与跨平台行为,但在「页面状态同步」与「核心模块职责划分」上仍存痛点:

  • 状态同步心智负担:v1.8.0 之前,开发者必须在每个页面的 onShow 生命周期中手动调用 router.syncRoute(),否则 currentRoute 会与真实页面脱节。一旦遗漏,守卫读取到的 from 便是旧值,重定向逻辑判断错误。
  • back() 参数丢失back() 走的是 uni.navigateBack 路径,没有 URL 可携带参数;若上一页依赖 params 继续渲染,pop 后会拿到空对象。
  • setCurrentRoute 执行时机偏后:原实现在 uni 导航 API 调用之后才更新 currentRoute,导致目标页的 onLoad / onShow 中读到的是旧路由,全局 mixin 的自动同步也由此失效。
  • 核心文件拥挤router/index.ts 一度膨胀至 824 行,interceptor/index.ts 达 309 行,函数职责混杂、可读性下降。
  • 错误类组织不一致RouterErrorNavigationFailure 位于 errors/,而 UniApiError 仍散落在 navigation/navigate.ts,且 index.ts 仅以 type 导出,消费者无法 instanceof

v1.9.0 围绕这些问题进行了五个方向的优化:新增全局 mixin 自动同步、修复 back() 参数保留、修正 setCurrentRoute 执行时机、拆分核心文件、统一错误类组织。


一、问题分析

1. 状态同步心智负担

旧版在每个页面都需要这样写:

// pages/detail/index.vue
export default {
	onShow() {
		// ❌ 忘记写这一行,currentRoute 便是旧值
		router.syncRoute()
	}
}

问题在于:

  • 遗漏风险:新增页面时容易忘记同步调用
  • 重复代码:N 个页面就要写 N 次 syncRoute()
  • 时机不确定onShow 触发顺序与 router.push Promise resolve 顺序无强保证,开发者难以判断何时读取 currentRoute 才是"正确"的

2. back() 参数丢失

back()uni.navigateBack,URL 不会被更新,因此 __params_key 也无处可挂。原 paramsManager.get() 在导航解析阶段会触发懒清理:被 pop 的页面对应的 params 会被回收,导致目标页(即上一页)onShow 重建路由时拿到空 params。

3. setCurrentRoute 执行时机偏后

原流程:

push(to)
  ├─ resolveLocation(to)
  ├─ runBeforeGuards(...)
  ├─ setCurrentRoute(to)        ← 原本在这里
  ├─ await uni.navigateTo(...)  ← uni 先于 setCurrentRoute 执行
  ├─ runAfterGuards(...)
  └─ resolve(to)

实际上 setCurrentRouteawait uni.navigateTo 之后才执行,但 uni-app 的 onLoad / onShow 会在 navigateTo 内部同步派发。结果:目标页的 onLoad 读到的 currentRoute 仍是 from

4. 核心文件拥挤

文件 行数 问题
router/index.ts 824 路由位置工具、同步逻辑、导航主流程混在一起
interceptor/index.ts 309 URL 解析、动画提取、位置构建、拦截分发混在一起
navigation/navigate.ts UniApiError class 错误类与导航实现耦合

5. 错误类组织不一致

// v1.8.0 src/index.ts
export { RouterError, NavigationFailure } from '@/errors'
export type { UniApiError } from '@/types' // ❌ 仅 type 导出

消费者无法:

import { UniApiError } from '@meng-xi/uni-router'

try { /* ... */ } catch (e) {
	if (e instanceof UniApiError) { /* ❌ v1.8.0 中 UniApiError 不是运行时值 */ }
}

二、新增能力

1. 全局 Mixin 自动 syncRoute

install() 行为变化

install() 在创建 Router 实例后,自动注册一个全局 mixin:

// packages/core/src/router/index.ts(install 简化示意)
app.mixin({
	onShow() {
		router.syncRoute()
	}
})

每个页面 onShow 触发时,路由器自动同步 currentRouteparamsManager,无需在业务页面中手写 syncRoute()

业务页面变化

// v1.8.0:每个页面都要手动同步
export default {
	onShow() {
		router.syncRoute()
	}
}

// v1.9.0:mixin 自动同步,业务页面无需任何处理
export default {
	onShow() {
		// 直接读取 router.currentRoute.value 即可
		console.log('当前路由:', router.currentRoute.value.fullPath)
	}
}

注意事项

  • 全局 mixin 仅同步状态:不触发守卫、不执行导航,开销极小
  • 手动调用 syncRoute() 仍安全:内部做了同路径同 query 短路返回,重复调用是幂等的
  • App 平台兼容:App-vue / App-nvue 的 onShow 行为与小程序一致,mixin 同样生效

三、Bug 修复

1. back() 参数保留

问题

back()uni.navigateBack,URL 不变。原 paramsManager.get() 在导航解析时触发懒清理,pop 后上一页 params 被回收,导致 onShow 重建路由时拿到空对象。

修复策略

  • __params_key URL 保留push / replace 时将 __params_key 保留在实际导航 URL 中(而非仅挂在内存),保证 H5 刷新后仍可重建
  • back() 使用 peek 重建:从 paramsManager.peek(key) 读取(不触发清理),再注入到目标 RouteLocation.params
// packages/core/src/router/sync.ts
function syncCurrentRoute(): void {
	const currentPath = getCurrentPagePath()
	const currentQuery = getCurrentPageQuery()
	const paramsKey = currentQuery.__params_key as string | undefined

	// peek 不触发懒清理,避免 back() 上一页 params 丢失
	const params = paramsKey ? paramsManager.peek(paramsKey) : {}
	const matched = matcher.match(currentPath)
	// ...构建 RouteLocation 并 setCurrentRoute
}

使用示例

// 页面 A:push 到详情页,携带复杂参数
router.push({
	path: '/pages/detail/index',
	params: { id: 123, sku: { color: 'red', size: 'L' } }
})

// 详情页:返回页面 A
router.back()
// v1.8.0:页面 A onShow 后 currentRoute.params 为 {}
// v1.9.0:页面 A onShow 后 currentRoute.params 仍为 { id: 123, sku: {...} }

2. setCurrentRoute 执行时机修正

问题

原流程中 setCurrentRoute(to)await uni.navigateTo(...) 之后执行,但 uni-app 的 onLoad / onShownavigateTo 内部同步派发,目标页生命周期读到的 currentRoute 仍是 from

修复

setCurrentRoute(to) 提前到 uni API 调用之前,并在失败时回滚:

// 修正后的执行顺序
push(to)
  ├─ resolveLocation(to)
  ├─ runBeforeGuards(...)
  ├─ setCurrentRoute(to)        ← 提前到这里
  ├─ try { await uni.navigateTo(...) } catch { rollback(from) }
  ├─ runAfterGuards(...)
  └─ resolve(to)

目标页 onLoad / onShow 现在能读到完整的 currentRoute,全局 mixin 的自动同步也能基于正确状态做短路判断。


四、架构重构

1. router/index.ts 拆分

将 824 行的 router/index.ts 拆为三个文件,行数降至 606(-26%)。

新增 router/location.ts

抽取 6 个纯函数:

函数 职责
extractAnimation(location) 从 location 提取动画选项
extractEvents(location) 从 location 提取 EventChannel 事件
extractParamsKey(location) 提取 / 生成 __params_key
isSameRouteLocation(a, b) 比较两个 RouteLocation 是否等价
enrichLocationWithParams(location, paramsManager) 将 params 注入 location

新增 router/sync.ts

通过工厂模式注入依赖,避免循环引用:

export function createRouteSync(
	routeState: RouteState,
	matcher: RouteMatcher,
	paramsManager: ParamsManager
): RouteSync {
	function syncRoute(): void {
		const from = routeState.getCurrentRoute()
		const currentPath = getCurrentPagePath()
		const currentQuery = getCurrentPageQuery()
		if (currentPath === from.path && isSameQuery(currentQuery, from.query)) return
		syncCurrentRoute()
		paramsManager.cleanupStale()
	}

	function syncCurrentRoute(): void {
		// 使用 peek 读取 params(不触发清理)
		// ...
	}

	return { syncRoute, syncCurrentRoute }
}

Router 类内部通过 this.routeSync.syncRoute() / this.routeSync.syncCurrentRoute() 委托调用。

2. interceptor/index.ts 拆分

将 309 行的 interceptor/index.ts 拆为两个文件,行数降至 237(-23%)。

新增 interceptor/utils.ts

抽取 URL 解析与位置构建工具:

export interface ParsedUniUrl {
	path: string
	query: Record<string, string>
}

export function parseUniUrl(url: string): ParsedUniUrl { /* ... */ }
export function extractAnimationFromArgs(args: UniNavigateToOption): AnimationType | undefined { /* ... */ }
export function buildLocation(path: string, query: Record<string, string>, animation?, events?): RouteLocationRaw { /* ... */ }

3. 公共函数提取到 utils/

去重 + 归位两处函数:

safeGetCurrentPages

原本在 navigation/context.tsparams/index.ts 各有一份本地实现,v1.9.0 统一提取到 utils/general.ts

// utils/general.ts
export function safeGetCurrentPages(): UniPage[] {
	if (typeof getCurrentPages !== 'function') return []
	return getCurrentPages()
}

两处原文件改为 import { safeGetCurrentPages } from '@/utils/general'

isSameQuery

原本误放在 router/location.ts,但它属于通用工具函数。v1.9.0 移至 utils/query.ts

// utils/query.ts
export function isSameQuery(a: Record<string, string>, b: Record<string, string>): boolean {
	if (a === b) return true
	const keysA = Object.keys(a)
	const keysB = Object.keys(b)
	if (keysA.length !== keysB.length) return false
	if (keysA.length === 0) return true
	return keysA.every(key => a[key] === b[key])
}

router/location.tsrouter/sync.ts 改为 import { isSameQuery } from '@/utils/query'

4. UniApiError 移至 errors/

移动前

src/
├─ errors/
│  ├─ router-error.ts        (class RouterError)
│  ├─ navigation-failure.ts  (class NavigationFailure)
│  └─ index.ts
├─ navigation/
│  └─ navigate.ts            (class UniApiError)  ← 散落在此
└─ types/
   └─ error.ts               (interface UniApiError)

移动后

src/
├─ errors/
│  ├─ router-error.ts        (class RouterError)
│  ├─ navigation-failure.ts  (class NavigationFailure)
│  ├─ uni-api-error.ts       (class UniApiError)  ← 新增
│  └─ index.ts
└─ types/
   └─ error.ts               (interface UniApiError)

新增 errors/uni-api-error.ts

import type { UniApiCause, UniApiError as UniApiErrorType } from '@/types/error'

export class UniApiError extends Error {
	readonly api: string
	readonly cause: UniApiCause
	constructor(api: string, cause: UniApiCause) {
		super(`[uni-router] uni.${api} failed`)
		this.name = 'UniApiError'
		this.api = api
		this.cause = cause
	}
}

export function isUniApiError(error: unknown): error is UniApiErrorType {
	return error instanceof UniApiError
}

index.ts 导出变化

// v1.8.0
export { RouterError, NavigationFailure } from '@/errors'
export type { UniApiError } from '@/types'

// v1.9.0:UniApiError 改为运行时值导出
export { RouterError, NavigationFailure, UniApiError } from '@/errors'

消费者现在可以:

import { UniApiError } from '@meng-xi/uni-router'

try {
	await router.push('/pages/detail/index')
} catch (e) {
	if (e instanceof UniApiError) {
		console.log(e.api)        // navigateTo
		console.log(e.cause.errMsg) // navigateTo:fail ...
	}
}

五、其他优化

1. 类型声明一致性

types/error.ts 中的 UniApiError 接口与 errors/uni-api-error.ts 中的 class 保持结构兼容,isUniApiError 以 interface 作为类型注解、class 仅用于实例化,规避私有 / 受保护成员导致的类型不兼容问题。

2. 构建验证

  • tsc --noEmit 通过(exit code 0)
  • tsup 构建 ESM / CJS / DTS 三种产物成功
  • @/* 路径别名经 tsc-alias 解析后产物可正确被消费者引用

升级指南

v1.9.0 是向后兼容版本,绝大多数项目无需修改代码即可升级。

行为变化

场景 v1.8.0 v1.9.0
页面 onShow 同步状态 手动调用 router.syncRoute() 全局 mixin 自动调用(手动调用仍安全)
back() 后上一页 params 丢失(被懒清理) 通过 peek 重建,完整保留
目标页 onLoad 读取 currentRoute 旧值(from 正确值(to
UniApiError 导出形式 type 导出 运行时值导出(支持 instanceof
router/index.ts 行数 824 606
interceptor/index.ts 行数 309 237

推荐清理

升级后可移除业务页面中冗余的 router.syncRoute() 调用:

 export default {
-	onShow() {
-		router.syncRoute()
-	}
+	onShow() {
+		// 已由全局 mixin 自动同步,可按需移除
+	}
 }

若你的项目在 onShow 中除了 syncRoute() 还有其他业务逻辑,仅需删除 syncRoute() 这一行即可,其余逻辑保留。

新增导出

  • UniApiError(运行时 class,支持 instanceof

新增文件

  • src/router/location.ts
  • src/router/sync.ts
  • src/interceptor/utils.ts
  • src/errors/uni-api-error.ts
  • src/utils/general.ts(新增 safeGetCurrentPages
  • src/utils/query.ts(新增 isSameQuery

兼容性

  • 旧版 router.syncRoute() 调用仍然有效(幂等)
  • 旧版 instanceof NavigationFailure 仍可用
  • 旧版 e.cause 类型推断保持兼容(v1.8.0 已收紧为 UniApiError | undefined,v1.9.0 进一步让 UniApiError 可作为运行时值进行 instanceof 判断)
posted @ 2026-07-07 01:44  PedroQue99  阅读(3)  评论(0)    收藏  举报