cover

在 Rails 实现返回按钮

Geeknote 遵循 Material Design。Material Design 经常使用返回按钮,我在实现返回按钮的过程发现一些问题,在此分享。

返回按钮跟浏览器的原生返回按钮类似,但又有所不同:

  1. 如果直接打开一个页面,应该有默认返回的上级页面。
  2. 如果一个页面有多个入口,应该返回对应的入口。
  3. 从外链进入一个页面,不应该返回网站外部。

先看看有什么实现方式。

探索

固定链接

最容易实现的就是固定链接,例如文章页返回网站首页,评论页返回文章页。但如果用户从作者个人页面进入文章页,返回网站首页就会很奇怪,因为用户可能还想阅读同一个作者的其他文章。

满足需求 1、3,不满足 2。

浏览器 API

如果将链接地址设为 javascript:history.back(),点击的时候就可以通过 JavaScript 调用浏览器 API 返回历史记录的上一页,作用类似于用户点击浏览器返回按钮。

这个方案的问题是如果没有历史记录就没有默认的返回地址,并且如果是外链进入有可能返回到外站。

满足需求 2,不满足 1、3。

借助 referrer

可以用 referrer 作为上一页的链接。实际上 Rails 有个 helper link_to :back 就实现了这个功能。如果请求带了 referrer,helper 会将 referrer 作为返回链接;如果请求没带 referrer,helper 会将 javascript:history.back() 作为返回链接。

这个方案的问题是会造成两个页面返回循环,例如从 A 访问 B,A 成为了 B 的上一页,如果这时返回 A,这时候 B 又成为了 A 的上一页。问题的原因是 referrer 是一个字符串,不是堆栈。自己实现历史记录堆栈又有点过度设计了。

解决

经过一番摸索我觉得这个问题应该在前端解决。浏览器的 History API 其实已经完成大部分功能,只需要在返回前做一些逻辑判断:如果上一页在站内,则返回上一页;如果不在站内,则返回默认路径。

接下来的问题是如何判断 history 的上一条内容是在站内,原生 History API 没有提供这个功能。后来我发现 Turbo 有一个属性 Turbo.navigator.history.currentIndex,这个属性在刚打开网站时是 0,每访问一个页面 +1,所以只需要判断这个属性是否大于 0 就行了。

于是我写了一个 Stimulus 控制器:

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="back-link"
export default class extends Controller {
  connect() {
    this.element.addEventListener("click", this.handleClick)
  }

  handleClick(event) {
    if (window.Turbo.navigator.history.currentIndex > 0) {
      event.preventDefault()
      window.history.back()
    }
  }
}

使用的时候只要在返回按钮的链接处加上:

<a href="/default/url" class="icon-button" data-controller="back-link">
  ...
</a>

这样在有上一页的时候会调用 history.back(),没有上一页的时候访问默认 URL。

经过测试,这个方案实现了我需要的效果,并且也让我比较满意:

  1. 尽可能利用浏览器原生 API。
  2. 代码量很少。
  3. 如果没有 JavaScript,会自动降级。

是否还有其他解决方案?欢迎在评论区讨论。

2
所有评论 0