如何使用HTML5 pushstate检索popstate事件是来自于后退还是前进操作?

89

我正在开发一个网页,根据用户点击的“下一页”或“上一页”来做相应的动画。但是当使用pushstate时会出现问题。当我接收到事件时,如何知道用户是通过Pushstate API点击了后退或前进历史记录按钮呢?还是我需要自己实现一些东西?


popstate事件表示“请切换到此状态”。它似乎假定您知道当前所处的状态,因此知道需要执行哪些操作来更改状态。 - Neil
问题在于历史记录是一个栈,所以如果我有一个列表并向前、向前、向前、向后、向前、向后、向前移动,而列表是:[1,2,3,4,5],那么历史记录将是:[1,2,3,4,3,4,3,4]。用数字很容易,但对于URL来说,知道哪个是下一个URL,哪个是上一个URL并不那么容易。 - Davsket
bennedich下面的评论对我帮助很大。将其接受,可以更有效地帮助他人。 - Gavin Anderegg
2个回答

75

你必须自己实现,这很容易。

  • 在调用pushState时,给数据对象一个唯一的递增id(uid)。
  • 当触发onpopstate处理函数时,将状态uid与包含上一个状态uid的持久变量进行比较。
  • 更新持久变量为当前状态uid。
  • 根据状态uid是大于还是小于上一个状态uid做出不同的行动。

18
当页面/视图层次结构清晰定义且某个页面总是在另一个页面之后时,这种方法非常有效。但如果导航更加动态,你不能依赖递增的UID。我通过使用sessionStorage来维护最近X个页面的堆栈来解决这个问题。当出现popstate事件时,可以从sessionStorage中检查堆栈,以查看新页面URL是否与N-2页面的URL相同。请使用sessionStorage而不是普通变量,以便在页面边界之间保存数据。 - frontendbeauty
26
我认为你应该将你的提议和代码片段作为答案发布。 - andilabs
2
此外,您可以编写一个状态更改管理器来检查ID以查看当前状态和正在移动到的状态,而不是检查值是否更大/更小,然后执行相应的动画。 - Changbai Li
如果这么容易,为什么你不写实现呢?我根本看不出如何使其对于任何可能的用例都没有漏洞。无论我将当前状态保留在哪里,我都能想出如何滥用它。 - Atomosk
2
@frontendbeauty 如果你在页面上,而且你在中间的页面b,你的堆栈看起来像这样 [a, b, a],你会收到 popstate 事件,那么你如何知道它是向后还是向前? - david_adler
这个代码片段相当简单,但你忘了加上“相当简单”的部分 :) - undefined

12

这个答案适用于单页应用程序、多页应用程序或两者的组合。(为了修复Mesqualito评论中提到的History.length错误,已进行更正。)

工作原理

我们可以轻松地监听历史记录堆栈中的新条目。 我们知道,对于每个新条目,规范要求浏览器执行以下操作:

  1. “在当前条目之后删除浏览上下文的会话历史记录中的所有条目”
  2. “在结尾处追加一个新条目”

因此,在进入时刻:

新条目位置=上次显示位置+1

解决方案如下:

  1. 使用其自己在堆栈中的位置标记每个历史记录条目
  2. 在会话存储中跟踪上次显示的位置
  3. 通过比较两者来确定前进方向

示例代码

function reorient() // After travelling in the history stack
{
    const positionLastShown = Number( // If none, then zero
      sessionStorage.getItem( 'positionLastShown' ));
    let position = history.state; // Absolute position in stack
    if( position === null ) // Meaning a new entry on the stack
    {
        position = positionLastShown + 1; // Top of stack

        // (1) Stamp the entry with its own position in the stack
        history.replaceState( position, /*no title*/'' );
    }

    // (2) Keep track of the last position shown
    sessionStorage.setItem( 'positionLastShown', String(position) );

    // (3) Discover the direction of travel by comparing the two
    const direction = Math.sign( position - positionLastShown );
    console.log( 'Travel direction is ' + direction );
      // One of backward (-1), reload (0) and forward (1)
}

addEventListener( 'pageshow', reorient );
addEventListener( 'popstate', reorient ); // Travel in same page

此外还可以查看代码的演示版本

限制

该解决方案忽略了与应用程序无关的外部页面的历史记录条目,就好像用户从未访问过它们一样。它仅根据最后一个显示的应用程序页面来计算行进方向,而不考虑之间访问的任何外部页面。如果您希望用户将外部条目推送到堆栈中(请参见Atomosk的评论),则可能需要一种解决方法。


6
没错,我永远无法适应前端 API 多么贫瘠的现实。检查浏览器的前进/后退功能本应只需要一行代码,但某些人决定采用不同的方式。 - Atomosk
2
浏览器的后退/前进历史记录有一个固定的限制,在Chrome中为50。当你达到这个限制时,history.length总是会产生相同的数字(在我的情况下为50),因此使得这个解决方案始终返回0作为方向,因为所有状态条目都具有相同的位置。 - Mesqalito
我将这段代码复制到了我的页面中,我试图为一个包含单页和内部导航的网站解决合理的前进/后退行为,但是我得到的只是“Travel direction is NaN”(旅行方向不是数字)。这里是否有一些上下文假设? - Britton Kerin
看起来所有的NaN可能与如何强制转换初始getItem结果有关。 - Britton Kerin
它需要一个初始的history.pushState来启动一些东西(处理程序注册已显示,因此可能也应该显示)。 - Britton Kerin
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接