前言
事情是这样的,测试反馈了一个问题,小程序中使用webview
打开了某个h5页,出现了在苹果手机中进入h5页面后无法返回的现象,排查的过程学到一个新知识,叫 历史修剪(history pruning)
,很多人应该都没听说过,这里跟大家分享一下。
排查思路
这个 h5 页是其他部门的团队在维护,我们只是在小程序中嵌入一下,所以没办法直接看代码。
1. 给出初步思路,协调沟通
出现这个问题,我首先想到的就是这个页面应该是存在重定向的,webview
打开 https://aaa
时,该地址会立即重定向到 https://bbb
,用户点击返回回到 https://aaa
时,页面又自动跳转到 https://bbb
,如此循环往复,导致最终用户无法退出H5,当然如果连续快速点返回有概率在重定向之前退出页面的,但是如果是这样的话应该所有手机表现都是一致的,而现状是只有苹果手机能够复现。
和对方团队交涉后,对方表示他们的 h5 在app里和浏览器里都没这个问题,让我们自行排查小程序的兼容性问题,虽然明显不是小程序端的问题,但是拿出有理有据的证据来比互相扯皮强,那就开查呗。
2. 尝试复现
上面的逻辑是没问题的,他们确实使用 replace
重定向了,我首先找了另外几个存在重定向的h5,在小程序中的 webview
打开,结果都没有复现,无论苹果还是安卓都可以正常返回。
于是我就写了个极简的网页,初始化就执行重定向到另一个网页,这样方便排查和调试。
3. 从页面栈入手
直接写一个 replace 肯定是无法复现了,我们都知道浏览器有个叫页面栈(Page Stack)的东西,它是浏览器用来管理用户在网页浏览过程中历史记录的一种数据结构,具有后进先出的特性。每次用户访问一个新页面,浏览器会将这个页面的信息推入栈顶,当用户点击“后退”按钮时,浏览器会从栈顶弹出当前页面,并显示下一个页面。
那么先在 chrome 打开测试页面,注意要在新Tab打开,确保页面栈无其他历史记录,然后打开控制台,输入 history
,可以看到 length 为2,即正常情况下,从新Tab页正常打开一个页面后,当前页面栈的历史记录应为2,由于我们使用了 replace 重定向,所以长度仍然是2。
历史记录为2意味着你可以返回一层,即返回你的浏览器的初始页
那么我们的测试页面没啥问题,符合预期,然后再看一下对方有问题的 h5 页,一样的操作,嘿!您猜怎么着,历史记录长度为3!
看样子找到问题的根源了,多了一个历史记录,理论上正常认知下点一次就是应该无法返回到初始页的,因为毕竟多了一个记录,但是为啥有的能返回有的返回不了呢,包括点击浏览器的返回也是可以直接返回到初始页的,这说明是不是有历史记录被浏览器忽略了,我们来验证一下。
4. 加一个历史记录来验证
在测试页面中使用 window.history.pushState(null, '', window.location.href);
添加一个历史记录,这样在完成重定向后就也是3个历史记录了,然后在小程序中试一下。
OK,不出所料,在 IOS 中复现了,出现了返回重定向返回重定向...,在安卓中则没问题,包括安卓手机中的浏览器(三星浏览器),然后我又试了一下在钉钉中打开这个h5,也出现了返回重定向返回重定向...的现象,看来其实也不是平台问题,就是某些浏览器内核的机制,具体什么机制还不得而知。
5. 用 history.back() 验证
之前我们怀疑由于浏览器的某些策略忽略了某些页面栈中的历史记录,现在我们用 history.back()
来验证一下,因为 back 是编程方式,肯定会严格遵循页面栈顺序的嘛,它不会忽略历史记录。
在浏览器的控制台中输入 history.back()
,嘿,您猜又怎么着?果然不出所料,使用 history.back()
在chrome 中完美复现鬼打墙,但是在chrome中手动点返回就不行。
OK,到这里,我们真正的找到了症结所在,就是浏览器的返回按钮包含某些策略会忽略掉一些历史记录,接下来只需要找出这个策略就好了。
所以,这个问题实际上是和小程序没有任何关系的
6. 问 AI
这种没有明确目标的用Google肯定不太好搜,问AI肯定效率更高。
问 claude
实际上同样的问题我问了好几个AI,包括 Gemini 、必应copilot、ChatGPT,最后只有claude提出了 history pruning 的概念,不愧是 claude
claude
提出了 history pruning
的概念,看着挺靠谱,不知道真假,问别的 AI 验证一下
看着依旧很靠谱,由于 history pruning
的策略,浏览器会在用户回退时忽略某些瞬态或者它认为不重要(重复等)的历史记录。
毕竟是 AI,还是得抱着将信将疑的心态,此时我们就有明确的搜索目标了,使用 Perplexity
搜索一下
Perplexity 是一个聚合搜索工具,它能自动帮你整理整合搜索结果,不用自己一个一个点进去详情查看
从Perplexity整合的结果来看,浏览器确实有这种策略来避免 “回退陷阱”,我们点进去来源页看看
大概意思就是,如果有淘气的开发人员添加了N多无意义的历史记录,那么用户就需要点N多下才能返回到初始页,这样用户体验极差,这个策略一定程度上防止了这种情况,即允许浏览器自动使用 history.go(-N)
来优化。
大家感兴趣可以自行找找其他官方说法
延伸:关于挽留弹窗
因为 pushState
方法只会改变页面栈的历史记录,并不会进行实际的跳转操作,所以我们经常会用添加页面栈历史记录的方式来实现“挽留弹窗”的功能
React Vue 等框架的路由库就是基于pushState、onpopstate等来实现的,细节很多,大致最终目标就是在不刷新浏览器的情况下维护应用的路由以及更新或局部更新页面数据。
当用户点击返回时回退一个页面同时展示弹窗,因为添加了一个和当前页面地址相同的记录,所以用户是无感的,对于用户来说相当于没有回退,类似下面这样实现:
window.history.pushState(null, '', window.location.href); const historyReturnCb = () => { // 触发挽回弹窗 window.removeEventListener('popstate', historyReturnCb); }; window.addEventListener('popstate', historyReturnCb);
那么我们思考一下,这样添加的记录会被上面所说的优化策略作为无用记录给优化掉吗?从而导致挽留弹窗无法生效?
答案是肯定的,只要你用的浏览器启用了优化策略,你的挽留弹窗必定不会生效,那这样肯定不行啊,这个属于是正当诉求啊,不能 "误杀" 啊。
对于这种情况,浏览器也是有对应的解决策略的,那就是当用户点击了你的页面时,即用户主动与你的页面发生了交互,那么浏览器就会认为这个记录是有意义的,从而不会被优化。
现在大家明白为什么我们之前实现挽留弹窗时需要用户点击一下才生效了吧。
总结
部分浏览器存在 history pruning
优化策略:
目的:
减少用户需要点击返回按钮的次数
避免用户在无关紧要的中间页面上浪费时间
提高整体浏览体验的流畅度
常见的
可能
被优化的情况:重定向页面: 浏览器可能会跳过HTTP重定向的中间页面
短暂停留的页面: 如果用户在一个页面上停留时间很短就导航到下一个页面,这个短暂停留的页面可能会被剪枝
动态生成的中间页面: 例如,一些表单提交后的确认页面
相似页面: 如果连续访问的几个页面非常相似,浏览器可能会合并它们
其他性能考虑:内存、层级过深等
注意,以上情况只是可能,具体表现因浏览器不同而各异,而且可能还有其他未知的策略,总之,具体策略细节其实不太重要,重要的是大家知道存在这么个事,以后遇到类似问题时就不会一脸懵了。