修复手机 Chrome 前进/后退触发 beforeunload 事件带来的问题(往返缓存的弊端)

发表于 更新于

前言

最近我给博客实现加载进度条动画时,注意到手机 Chrome 的一个很诡异的现象。当用户后退或前进时,会触发上一个页面的 beforeunload 事件。也就是说我从 A 页面进入 B 页面时,A 页面的 beforeunload 事件会正常触发一次,因为即将离开 A 页面。但当我从 B 页面后退时,上一次的 A 页面的 beforeunload 事件会被再次触发。

这个现象在桌面版 Chrome 上或 DevTools 中模拟手机视图都不存在,甚至真实手机访问开发中的页面也不存在。它疑似和往返缓存(bfcache)有关系,但我目前还无法完全确认。

进度条实现

本博客的进度条是在 beforeunload 事件中实现的,这是模拟的动画,不表示真实的连接状态变化。

使用手机 Chrome 的返回按钮回到我博客的任意页面时,该页面的进度条动画会被错误的再次播放。并且由于没有新页面加载,进度条动画将持续播放和存在。经过一番测试,我发现是 beforeunload 事件被再次调用导致的。

假设 A 页面的进度条被修改到 55% 后,B 页面加载了。由于新页面加载,上一个页面(即 A 页面)的进度条自然就不存在了。诡异之处在于,当从 B 页面后退,A 页面的进度条居然再次以 55% 的长度继续。因为我在实现进度条时,始终从 DOM 的当前宽度开始。也就是说后退的 A 页面其实还是那个导航到 B 页面之前的 A 页面,它的状态仍然存在,就好像一直后台运行一般。

往返缓存

往返缓存(bfcache)是 Chrome 的一个特性。在符合往返缓存的页面中,用户通过导航按钮(即前进/后退)跳转时,浏览器会从内存中恢复页面。包括恢复 JavasScript 栈。所以严格来说触发 bfcache 特性的网页,会被 bfcache “恢复”,而不是简单的从内存中重新载入。

经过一系列的手动测试,我发现当我使用桌面版 Chrome(包括 F12 手机视图),以及真实手机访问开发页面时,往返缓存都是失效的。但一旦线上部署并使用手机 Chrome 访问,往返缓存就会生效。这个现象跟开发中的页面被植入 WebSocket 代码有关,因为 Hugo 的 Server 或与之类似的开发工具需要用 WebSocket 通知页面刷新。而 bfcache 无法恢复包含 WebSocket 的页面,故不会生效。

至于为什么桌面版 Chrome 访问线上的页面也不会生效,往返缓存功能报告使用了 WebAuthetication API,不符合条件。这可能跟 Chrome 区别对待手机版和桌面版的 bfcache 资格有关。

打开 Chrome 的 DevTools(F12),进入「应用」-「后台服务」-「往返缓存」功能,可以自行测试。

解决方案

为了避免 bfcache 恢复页面时导致进度条错误显示。我是这样做的:

window.addEventListener("pagehide", (_event) => {
  progress.classList.add("progress-none");
  progressBar.style.width = "20%";
});

因为我同时发现 bfcache 恢复的页面,其 pagehide 事件也会被触发。所以我在 pagehide 的代码中将进度条隐藏,并将宽度重置为 20%。这样就可以避免进度条的错误显示,同时让进度条达到初始状态。

当然,这个最简化的代码只是用于“解决问题”的展示。实际上我的进度条利用了这个特性,模拟了手机 Chrome 的进度条。后退后,会一瞬间达到 100% 然后隐藏。

简而言之,如果你遇到了和我一样的情况,可以尝试添加 pagehide 事件,来抵消 beforeunload 不当触发带来的影响。当然你也可以通过一些条件,在 beforeunload 中判断是否是 bfcache 恢复页面,来决定是否执行你的代码。

结束语

这就是有关 beforeunload 事件和往返缓存的诡异现象的全部了。同时我注意到还有一些用 beforeunload 实现进度条动画的博客也长期存在这个问题,只是他们可能不使用手机 Chrome,一直未意识到。

如果你遇到了,不妨参考我的方法来解决。由于前端的规范和 Chrome 等浏览器总在变化,我暂时可能不会深入调查这背后的原因(我真的很忙)。如果你有更好的做法,或知道此现象的原因,也可以联系告知我。

作者头像 一点点入门知识 打赏作者
本文由作者按照 CC BY 4.0 进行授权
分享: