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.vueexportdefault{onShow(){// ❌ 忘记写这一行currentRoute 便是旧值router.syncRoute()}}问题在于遗漏风险新增页面时容易忘记同步调用重复代码N 个页面就要写 N 次syncRoute()时机不确定onShow触发顺序与router.pushPromise resolve 顺序无强保证开发者难以判断何时读取currentRoute才是正确的2.back()参数丢失back()走uni.navigateBackURL 不会被更新因此__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.ts824路由位置工具、同步逻辑、导航主流程混在一起interceptor/index.ts309URL 解析、动画提取、位置构建、拦截分发混在一起navigation/navigate.ts含UniApiErrorclass错误类与导航实现耦合5. 错误类组织不一致// v1.8.0 src/index.tsexport{RouterError,NavigationFailure}from/errorsexporttype{UniApiError}from/types// ❌ 仅 type 导出消费者无法import{UniApiError}frommeng-xi/uni-routertry{/* ... */}catch(e){if(einstanceofUniApiError){/* ❌ v1.8.0 中 UniApiError 不是运行时值 */}}二、新增能力1. 全局 Mixin 自动 syncRouteinstall() 行为变化install()在创建 Router 实例后自动注册一个全局 mixin// packages/core/src/router/index.tsinstall 简化示意app.mixin({onShow(){router.syncRoute()}})每个页面onShow触发时路由器自动同步currentRoute与paramsManager无需在业务页面中手写syncRoute()。业务页面变化// v1.8.0每个页面都要手动同步exportdefault{onShow(){router.syncRoute()}}// v1.9.0mixin 自动同步业务页面无需任何处理exportdefault{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.navigateBackURL 不变。原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.tsfunctionsyncCurrentRoute():void{constcurrentPathgetCurrentPagePath()constcurrentQuerygetCurrentPageQuery()constparamsKeycurrentQuery.__params_keyasstring|undefined// peek 不触发懒清理避免 back() 上一页 params 丢失constparamsparamsKey?paramsManager.peek(paramsKey):{}constmatchedmatcher.match(currentPath)// ...构建 RouteLocation 并 setCurrentRoute}使用示例// 页面 Apush 到详情页携带复杂参数router.push({path:/pages/detail/index,params:{id:123,sku:{color:red,size:L}}})// 详情页返回页面 Arouter.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{awaituni.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_keyisSameRouteLocation(a, b)比较两个 RouteLocation 是否等价enrichLocationWithParams(location, paramsManager)将 params 注入 location新增router/sync.ts通过工厂模式注入依赖避免循环引用exportfunctioncreateRouteSync(routeState:RouteState,matcher:RouteMatcher,paramsManager:ParamsManager):RouteSync{functionsyncRoute():void{constfromrouteState.getCurrentRoute()constcurrentPathgetCurrentPagePath()constcurrentQuerygetCurrentPageQuery()if(currentPathfrom.pathisSameQuery(currentQuery,from.query))returnsyncCurrentRoute()paramsManager.cleanupStale()}functionsyncCurrentRoute():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 解析与位置构建工具exportinterfaceParsedUniUrl{path:stringquery:Recordstring,string}exportfunctionparseUniUrl(url:string):ParsedUniUrl{/* ... */}exportfunctionextractAnimationFromArgs(args:UniNavigateToOption):AnimationType|undefined{/* ... */}exportfunctionbuildLocation(path:string,query:Recordstring,string,animation?,events?):RouteLocationRaw{/* ... */}3. 公共函数提取到utils/去重 归位两处函数safeGetCurrentPages原本在navigation/context.ts与params/index.ts各有一份本地实现v1.9.0 统一提取到utils/general.ts// utils/general.tsexportfunctionsafeGetCurrentPages():UniPage[]{if(typeofgetCurrentPages!function)return[]returngetCurrentPages()}两处原文件改为import { safeGetCurrentPages } from /utils/general。isSameQuery原本误放在router/location.ts但它属于通用工具函数。v1.9.0 移至utils/query.ts// utils/query.tsexportfunctionisSameQuery(a:Recordstring,string,b:Recordstring,string):boolean{if(ab)returntrueconstkeysAObject.keys(a)constkeysBObject.keys(b)if(keysA.length!keysB.length)returnfalseif(keysA.length0)returntruereturnkeysA.every(keya[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.tsimporttype{UniApiCause,UniApiErrorasUniApiErrorType}from/types/errorexportclassUniApiErrorextendsError{readonlyapi:stringreadonlycause:UniApiCauseconstructor(api:string,cause:UniApiCause){super([uni-router] uni.${api}failed)this.nameUniApiErrorthis.apiapithis.causecause}}exportfunctionisUniApiError(error:unknown):errorisUniApiErrorType{returnerrorinstanceofUniApiError}index.ts导出变化// v1.8.0export{RouterError,NavigationFailure}from/errorsexporttype{UniApiError}from/types// v1.9.0UniApiError 改为运行时值导出export{RouterError,NavigationFailure,UniApiError}from/errors消费者现在可以import{UniApiError}frommeng-xi/uni-routertry{awaitrouter.push(/pages/detail/index)}catch(e){if(einstanceofUniApiError){console.log(e.api)// navigateToconsole.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 0tsup构建 ESM / CJS / DTS 三种产物成功/*路径别名经tsc-alias解析后产物可正确被消费者引用升级指南v1.9.0 是向后兼容版本绝大多数项目无需修改代码即可升级。行为变化场景v1.8.0v1.9.0页面onShow同步状态手动调用router.syncRoute()全局 mixin 自动调用手动调用仍安全back()后上一页 params丢失被懒清理通过peek重建完整保留目标页onLoad读取currentRoute旧值from正确值toUniApiError导出形式type导出运行时值导出支持instanceofrouter/index.ts行数824606interceptor/index.ts行数309237推荐清理升级后可移除业务页面中冗余的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新增safeGetCurrentPagessrc/utils/query.ts新增isSameQuery兼容性旧版router.syncRoute()调用仍然有效幂等旧版instanceof NavigationFailure仍可用旧版e.cause类型推断保持兼容v1.8.0 已收紧为UniApiError | undefinedv1.9.0 进一步让UniApiError可作为运行时值进行instanceof判断