在 Rails 实现返回按钮
Geeknote 遵循 Material Design。Material Design 经常使用返回按钮,我在实现返回按钮的过程发现一些问题,在此分享。
返回按钮跟浏览器的原生返回按钮类似,但又有所不同:
- 如果直接打开一个页面,应该有默认返回的上级页面。
- 如果一个页面有多个入口,应该返回对应的入口。
- 从外链进入一个页面,不应该返回网站外部。
先看看有什么实现方式。
探索
固定链接
最容易实现的就是固定链接,例如文章页返回网站首页,评论页返回文章页。但如果用户从作者个人页面进入文章页,返回网站首页就会很奇怪,因为用户可能还想阅读同一个作者的其他文章。
满足需求 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。
经过测试,这个方案实现了我需要的效果,并且也让我比较满意:
- 尽可能利用浏览器原生 API。
- 代码量很少。
- 如果没有 JavaScript,会自动降级。
是否还有其他解决方案?欢迎在评论区讨论。