v2.7.1 为关键修复版本,解决 H5 端「返回时页面在相邻页面之间反复切换闪烁、无法正常返回」的问题(issue #39)。问题源于
onBeforeBack的 H5 popstate 返回守卫在「撤销后退 + 重放」过程中的自触发死循环。本版本通过返回进行中标记 + 目标 URL 命中判定彻底断开自触发链路,且完全向后兼容。
前言
- App 端 :
onBeforeBack通过全局 mixin 的onBackPress接入物理返回键 / 导航栏返回 /navigateBack,守卫放行后手动返回,backGuardRunning标记避免递归------无此问题。 - H5 端 :
onBeforeBack通过浏览器popstate事件接入后退;其「撤销 → 守卫 → 重放」策略在特定时序下会自我触发,形成死循环。这正是 v2.7.1 要修复的对象。
v2.7.1 不改动 public API 与守卫语义,仅重构 H5 返回守卫的防递归逻辑。
一、问题现象与影响(issue #39)
H5 端按 首页 → 二级页 → 三级页 导航后,在三级页执行返回(router.back() 或浏览器后退),页面在二级页与三级页之间每几十毫秒高频反复切换 (test ↔ p3p),无法正常返回。控制台无 JS 异常(纯历史/popstate 副作用驱动),会使页面持续重渲染、占用资源。
复现环境依赖:@meng-xi/uni-router ^2.7.0、@meng-xi/vite-plugin ^1.4.0。
二、根因
H5 返回守卫原实现(router/back-guard.ts 的 handleH5PopState)在收到一次后退 popstate 时执行:
go
1. history.go(1) // 撤销此次后退,恢复到当前页
2. 运行返回守卫链 // onBeforeBack → beforeEach → beforeResolve
3. 守卫放行 → executeBack // uni.navigateBack 重新后退
问题在于 uni.navigateBack 在 H5 上会触发多次 popstate,且当该 popstate 的派发时机晚于 backGuardRunning 标记的复位时,handleH5PopState 会把「路由器自己发起的这次返回」误判为新的外部后退,再次进入「撤销 + 重放」分支,进而在相邻页面间形成死循环闪烁。
三、修复方案
改动集中在 router/back-guard.ts 与 router/index.ts,不涉及 public API:
1. H5 返回进行中标记(时间窗口兜底)beginH5Back()
router.back() 与守卫放行后的返回(executeBack)在发起 navigateBack 前,置位 h5ReturningBack;handleH5PopState 若检测到该标记,则放行本次导航产生的所有 popstate(不再进入「撤销 + 重放」分支),并持续更新基准 URL。窗口(500ms)结束后自动复位,避免标记永久残留。
2. 返回目标命中判定(确定性结束)
beginH5Back(targetPath) 同时记录本次返回的目标路径(如 /pages/test/test)。handleH5PopState 在 h5ReturningBack 期间,若 location.hash 命中该目标路径,即视为本次返回确定完成,清除标记(无需再等窗口)。
这样无论 navigateBack 在 H5 产生多少次、多晚到达的 popstate,只要最终落在目标页即停;时间窗口仅作极端兜底。
3. 关键代码路径
typescript
// back-guard.ts
beginH5Back(targetPath?: string): void {
if (!getPlatform().isH5) return
this.h5ReturningBack = true
this.h5ReturnTarget = targetPath ?? null
// ... 窗口兜底定时器
}
handleH5PopState(): void {
if (this.h5ReturningBack) {
this.h5BackUrl = location.href
// 命中返回目标 URL,视为本次返回完成
if (this.h5ReturnTarget && getHashPath(location.hash) === this.h5ReturnTarget) {
this.h5ReturningBack = false
this.h5ReturnTarget = null
// ... 清理兜底定时器
}
return
}
// ... 原逻辑(外部后退进入守卫链)
}
typescript
// router/index.ts:back() 在 goBack 前置位
this.backGuard.setRouterBackRunning(true)
this.backGuard.beginH5Back(to.path)
await goBack(delta, animation)
四、修复效果与回归验证
在完整 首页(index) → 二级(test) → 三级(p3p) H5 导航栈下回归,三组场景均通过:
| 场景 | 修复前 | 修复后 |
|---|---|---|
守卫放行 + router.back() |
死循环 | 一次回二级并稳定 |
守卫中止 + router.back() |
死循环 | 停留当前页,无死循环 |
| 守卫放行 + 浏览器原生后退 | 死循环 | 回二级并稳定 |
控制台 popstate/go 日志量级由"爆量"降为个位数,[TheGuard] onBeforeBack 阻止返回 在守卫中止场景正常触发。
五、升级指南
v2.7.1 为 PATCH 版本,完全向后兼容,无破坏性变更:
- 未改动任何公开 API 签名、选项、守卫返回值约定
- 修复仅在
router.back()与 H5 原生返回(popstate)路径生效,push/replace/relaunch不受影响 - 建议升级后重新验证 H5 平台的返回行为(原生后退 / 应用内
back()/ 存在onBeforeBack守卫时)
版本兼容性
| 功能 | v2.7.0 | v2.7.1 |
|---|---|---|
H5 router.back() 返回 |
可能死循环闪烁 | 正常返回 ✅ |
| H5 浏览器原生后退 | 可能死循环闪烁 | 正常返回 ✅ |
| 守卫放行 / 中止语义 | 受死循环影响 | 保持一致 ✅ |
| H5 push/back CSS 动画 | 支持 | 支持 |
| App 原生动画 / 返回拦截 | 支持 | 支持 |