toxic-johann / toxic-johann.github.io

my blog
6 stars 0 forks source link

多页应用上使用popState监听系统回退的一个坑 #34

Open toxic-johann opened 7 years ago

toxic-johann commented 7 years ago

问题所在

这几天弄一个需求,需要监听用户的回退操作。马上就想起了popstate.

但是在多页应用里,每一页的回退都没有触发 popState 事件。

查阅规范

这是为什么呢,我们翻一下规范

首先我们查一下 history.go 做了些什么。

window . history . go( [ delta ] )

Goes back or forward the specified number of steps in the joint session history.

A zero delta will reload the current page.

If the delta is out of range, does nothing.

可以看到他是调用了一个 joint session history 方法。

于是我们直接跳到 joint session history 章节,可以看到有 To traverse the history by a delta delta 的步骤描述。

这里我们可以获取到几个有用的定义:

Let specified entry be the entry in the joint session history whose index is the sum of delta and the index of the current entry of the joint session history.

所以 specified entry 就是我们想要前往的页面。

在规范的 5.6.10 的章节我们可以查到相关的历史遍历步骤。

接着我们在第六步的第三小步发现需要执行浏览器的历史遍历,我们继续探查

Traverse the history of the specified browsing context to the specified entry.

通过history traversal跳转到目的地。

在第14步的第一小步骤,我们终于发现了 popState 的身影:

  1. If state changed is true, fire a trusted event with the name popstate at the Window object of the Document, using the PopStateEvent interface, with the state attribute initialized to the value of state. This event must bubble but not be cancelable and has no default action.

所以popState 是否触发的本质是,state changed 这个变量是否为 true

那这个变量怎么判断的呢,我们可以看到第 12 步

12 . Let state changed be true if the Document of the specified entry has a latest entry, and that entry is not the specified entry; otherwise let it be false.

这个 latest entry 是什么东西呢,我们看看第13步。

Let the latest entry of the Document of the specified entry be the specified entry.

就是每次遍历的时候推入的最新堆栈。另外latest entry我们可以看到:

Each Document in a browsing context can also have a latest entry. This is the entry or that Document that was most the recently traversed to. When a Document is created, it initially has no latest entry.

问题分析

结合这两句我们可以知道。

综合以上规范我们可以得知到以下几种情形。

1) 进入一个新页面A,再跳转到新页面B,然后点击回退。

因为A页面没有进行任何的历史遍历,所有没有推入任何 history entry。故,这个 document A上没有 latest entry 存在。因此 state changedfalse。所以不会执行 popState。

2) 进入新页面 A ,利用 replaceState更换当前的历史栈,再跳转到页面 B,然后点击回退。

此时回退到页面 A,A 存在 latest entry, 但是 latest entry 和 specified entry 是一样的,所以不会触发变化。

3) 进入新页面 A,利用 pushState 填入 A 的历史条目 a,然后回退。

因为 latest entry 是 a,且和 specified entry 不等。所以会触发 popState 事件,虽然 history 有回退,但是页面没有变化。

解决方案

因此如果我们要监听用户的回退操作,应该主动 push 一个新的 state。

那么当用户点击回退时,该 state 会弹出,触发 popState 事件,此时我们获得了用户后退行为,再相应的帮助用户回退即可。

当然这样会带来一些麻烦,就是主动回退时要多回退一层,还有理清楚所有层数的关系。

可以考虑封装一个小工具出来。

toxic-johann commented 7 years ago

另外,在某些 webView 里如果回退的数目大于 history 的栈,会只回退一页。。。

而按照规范来说,应该是什么都不做的。。。