平常使用浏览器的后退功能时,常常会发现返回的页面滚动到了之前的位置,而不是简单的回到顶部。这是由于浏览器在导航跳转和通过 History API 创建历史记录时,都会记录当前的垂直滚动距离,在重新访问时恢复这个距离。值得一提的是,在 HTML History Spec中并没有强制要求浏览器记录与恢复滚动距离。在传统的页面中,这一默认行为非常贴心,但是在 SPA 中存在以下问题:

  1. 浏览器试图恢复滚动距离时,页面可能还没有加载完毕,可能存在异步加载的部分。这样恢复之后,页面会出现跳动。
  2. 点击链接进入页面就不会应用恢复滚动这一行为。对于开发者而言,希望提供统一的恢复滚动实现,而不是后退等历史记录这部分依赖浏览器。
  3. 浏览器的滚动行为让开发者无法实现滚动过程的动画。而且对于容器内滚动的设计,浏览器是无法帮助恢复的。

按照《浏览器恢复滚动提案》的介绍,针对这一问题,现在常见的解决方案包括:

  1. 不使用 body 滚动,采用容器内滚动的方式。但显然,这样会丢失一部分浏览器默认行为,例如滑动隐藏顶部地址栏。
  2. 在浏览器恢复滚动行为之后,进行第二次滚动。缺点是会有明显的用户感知。

疑似方案?

话说我们能监听到浏览器恢复滚动的行为么?

浏览器恢复滚动时会触发 scroll 事件。但这里涉及到 scroll 和 popState 事件的触发顺序。Chrome 在恢复滚动之前会先触发 popState 事件,这就意味着我们可以在此时记录下页面的滚动距离,然后触发 scroll 时我们就能使用类似window.scrollTo来手动恢复了。但是 Firefox 刚好相反,这样在 popState 中我们已经无法获取旧的滚动距离了,因为浏览器已经自动恢复了。

事实上,在前端路由的设计中,通常都包含 hash 和 History 模式。在 react-router的这个 ISSUE中,列举了 hashchange/popState 与 scroll 事件在 Chrome/Firefox 中的触发顺序,可以看出并不统一。 所以这个方案至少在兼容性上并不可行。

引入新的 API

为了让开发者能够通过编程方式关闭这一浏览器行为,《浏览器恢复滚动提案》引入了新的 API:

if ('scrollRestoration' in history) {
    // 默认值为'auto'
    history.scrollRestoration = 'manual';
}

根据 Google 开发者文档,Chrome 46 以上已经实装。

我自己也写了一个 demo 测试了一下确实可行,这或许才是最好的解决方案,等待其他浏览器厂商支持吧。