V1.9.0重磅更新:自动路由同步与架构优化
v1.9.0 通过全局 mixin 自动调用
syncRoute()简化页面状态同步,修复back()参数丢失与setCurrentRoute执行时机问题,并对router/index.ts、interceptor/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 行,函数职责混杂、可读性下降。 - 错误类组织不一致:
RouterError、NavigationFailure位于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.pushPromise 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)
实际上 setCurrentRoute 在 await 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 触发时,路由器自动同步 currentRoute 与 paramsManager,无需在业务页面中手写 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_keyURL 保留: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 / onShow 在 navigateTo 内部同步派发,目标页生命周期读到的 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.ts 与 params/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.ts 与 router/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.tssrc/router/sync.tssrc/interceptor/utils.tssrc/errors/uni-api-error.tssrc/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判断)

浙公网安备 33010602011771号