页面跳转规则会变化:缓存候选范围,确认时重读规则
一个常见的需求:某个入口需要临时跳转到新页面。最省事的写法是判断一个固定的业务 ID,再拼一个新地址。
可是入口一多、规则有了生效日期、页面又长时间不关闭之后,这些判断就会开始互相矛盾:
- 规则已经到期了,一直开着的旧页面还在按旧规则跳转;
- 新加入规则的对象,刚开始一段时间没有生效;
- 用户已经离开了页面,过一会儿却弹出一个属于上一个页面的提示;
- 规则接口出错时,用户被卡住,什么都做不了。
本文来自一个原生微信小程序近期的导航改造,核对日期为 2026-10-23。业务对象、编号、地址和接口均已省略,只保留可以复用的控制流程。本次没有启动小程序或调用业务接口。
读完这篇你会
- 把“哪些对象需要检查”和“此刻规则是否生效”拆成两层,分别缓存
- 理解候选缓存带来的短暂遗漏窗口,并根据规则性质做取舍
- 保存用户的意图而不是拼好的 URL
- 让迟到的异步结果不会弹出属于旧操作的提示
1. 原理:登机前再看一眼屏幕
在机场,你早早地在手机上看到了登机口是 A12,于是走到了那附近(缓存的候选信息)。但真正登机之前,你还会抬头再看一眼登机口的屏幕(确认时重新读取规则),因为登机口可能临时变更了。
如果只看一次手机就去登机,就可能跑错登机口;如果每走一步都去问工作人员,又太慢了。合理的做法是:平时用缓存的信息快速判断方向,在做出最终决定的那一刻再确认一次。
当前的实现把规则分成两层:
| 数据 | 用途 | 缓存策略 |
|---|---|---|
| 候选对象列表 | 哪些对象可能需要特殊处理 | 可以缓存,后台更新 |
| 当前生效的配置 | 此刻是否启用、目标是哪里 | 点击时读取,用户确认时再读取一次 |
这样,普通的入口不需要每次都等待规则请求;同时,有时效性的最终决定也不会被长期缓存。
text
页面显示 → 预取候选列表
↓
用户点击 → 是否属于候选范围
├─ 否 → 原流程
└─ 是 → 读取当前配置 → 显示提示
↓
用户确认时再读取
↓
按新规则或原流程继续2. 坑一:页面开着跨过了生效日期,还在按旧规则跳转
现象:规则在今天零点到期了,但一个从昨天就开着的页面,用户点击后还是跳到了旧的目标。
原因:最终是否生效的配置被缓存在了页面里。页面可能跨过日期边界,提示弹窗也可能停留很久。
修复:用户确认时再读取一次配置。这解决的是“用户看到提示时的规则”和“真正执行时的规则”可能不一样的问题。
3. 坑二:新加入规则的对象,刚开始没生效
现象:服务端刚把一个新对象加入了特殊跳转规则,但用户点击它时,还是走了原来的流程,过一会儿才正常。
原因:当前的候选请求用同一个进行中的 Promise 合并并发请求;缓存过期时在后台更新;第一次没有缓存时只等待有限的时间,失败了就沿用原流程。使用旧的候选列表,代价是:服务端刚加入的对象不在旧列表里,在刷新完成之前,可能仍然走原流程。
候选缓存并不是一个“毫无影响的性能优化”,它带来了一个短暂的遗漏窗口。适合的策略取决于规则的性质:
- 只是改善体验的跳转:可以接受短暂地沿用原流程;
- 涉及权限、支付资格或者必须执行的业务限制:应该由服务端的最终动作再次校验;
- 规则变化要求立刻生效:需要额外的版本号、失效通知或者更严格的读取策略。
这些增强是建议,不是当前客户端代码已经提供的保障。
4. 坑三:提前拼好的 URL,规则一变就不对了
现象:用户在原页面选好了一个规格,点击后弹出“即将跳转到新页面”。用户点了确认,跳过去之后,刚才选好的规格丢了,因为新地址是按旧规则提前拼好的,没有带上用户的选择。
修复:保存用户的意图,而不只是一个 URL。当前的实现记录原对象、可选的上下文和原流程的回调;用户确认后,再按照最新的规则构造目标地址,有原来的上下文就一起带过去,而不是无条件地回到一个空白的入口。
通用的意图模型可以这样表达:
ts
type NavigationIntent = {
sourceId: string;
selectedResourceId?: string;
resumeOriginal: () => void;
};意图比提前拼好的 URL 更容易重新解释:规则失效了,可以继续走原流程;规则更新了,可以换一个目标,同时保留用户已经做出的选择。
当前代码还会校验:配置是否启用、来源是否一致、目标的格式是否合法、目标是不是等于来源(避免跳回自己)。对字符串做 URL 编码只是为了正确地构造地址,不能代替这些业务条件的检查。
5. 坑四:离开页面后,弹出了旧的提示
现象:用户在页面 A 点击了一个对象,规则请求还没回来,用户就离开去了页面 B。过了一会儿,页面 B 上弹出了一个“即将跳转”的提示,内容是关于 A 页面那个对象的。
原因:异步请求返回的时候,用户可能已经离开了当前的页面;确认请求返回时,弹窗也可能已经被关闭或替换了。
修复:当前的实现会检查页面是否仍然处于页面栈的顶部,并在确认阶段检查保存的上下文是否还是同一个对象。旧请求不能重新弹出属于上一轮操作的提示。
注意:“页面还存在”和“页面仍然是当前操作的目标”是两回事。只判断页面对象不为空,无法排除一个已经被隐藏到后面的旧页面。
6. 失败回退要说明自己的适用范围
当前的实现中:规则接口不可用或者配置无效时,沿用原流程;跳转失败时,如果上下文仍然有效,就恢复提示并允许重试;按钮锁用来避免重复进入检查流程。
这是一种偏重可用性的选择,不适合直接套用在安全授权上。如果原流程本身已经无法完成业务,静默回退只会把问题推迟到下一步;这时应该展示明确的“暂时不可用”状态。
客户端的提示只负责导航体验,不应该成为业务规则唯一的执行点。旧版本的客户端、缓存、直接进入目标页面的情况,都需要服务端的最终行为保持一致。
排查思路
- 跳转结果不符合规则:确认当时用的是缓存的候选列表还是刚读取的配置,确认时有没有重新读取。
- 新规则没生效:查看候选缓存的更新时间,判断是否处在遗漏窗口内。
- 弹窗出现在错误的页面:在请求返回的地方打日志,记录当时的页面栈顶和保存的上下文,确认校验是否生效。
- 规则接口出错:确认回退行为是否符合这个规则的性质(体验类可以回退,强制类应该提示不可用)。
建议验证
| 场景 | 应该核对的结果 |
|---|---|
| 普通对象不在候选范围内 | 不会被额外的请求长时间阻塞 |
| 候选列表过期,且新增了对象 | 记录为可以接受的短暂遗漏,不能当作实时保证 |
| 弹窗停留期间跨过了生效边界 | 确认时按新的配置处理 |
| 加载过程中离开页面 | 旧的响应不会重新打开弹窗 |
| 连续点击、关闭后重新打开 | 提交锁和上下文分别管理 |
| 跳转失败或规则接口失败 | 保留必要的意图,回退行为符合业务约定 |
检查清单
- [ ] 候选范围和当前生效配置分两层,最终决定在确认时重新读取
- [ ] 明确了候选缓存的遗漏窗口,强制性规则由服务端再次校验
- [ ] 保存的是用户的意图,而不是提前拼好的 URL
- [ ] 异步结果返回时,核对页面仍是栈顶、上下文仍是同一个
- [ ] 失败回退的方式和规则的性质相匹配