乐趣区

关于前端:极致用户体验-网页里的返回应该用-historyback-还是-push

我是 HullQin,公众号 线下团聚游戏 的作者(欢送关注公众号,发送加微信,交个敌人),转发本文前需取得作者 HullQin 受权。我独立开发了《联机桌游合集》,是个网页,能够很不便的跟敌人联机玩斗地主、五子棋等游戏,不免费没广告。还开发了《Dice Crush》加入 Game Jam 2022。喜爱能够关注我 HullQin 噢~我有空了会分享做游戏的相干技术。

1. 什么是「返回」按钮?

这里不是浏览器的「返回」按钮,咱们没方法批改它的行为。

而是网页代码中的「返回」按钮,咱们能够定义它的行为。

举个例子

比方我的五子棋小游戏:

点开链接,会呈现文章结尾图片的的页面——游戏主页,「进入房间」后,左上角有个「来到房间」按钮,点击后,会返回主页。

这种须要返回下层页面的按钮,在本文中,称之为「返回」按钮。

2. 什么是 push、back、replace?

push back replace
浏览器行为 页面会产生跳转,并在以后浏览记录新增一条记录(之后你能够按浏览器「返回」,回到跳转前的页面)。 页面返回上一条浏览记录(之后你能够按浏览器「后退」,从新回到返回前的页面)。若浏览器没有上一条记录,则什么都不会产生。 页面会产生跳转,笼罩以后的浏览记录。(你按浏览器「返回」,无奈回到跳转前的页面)
HTML DOM API: History History.pushState() History.back() History.replaceState()
history@4 或 React Router@4 或 @5 history.push() history.goBack() history.replace()
React Router@6 navigate(url, { state, replace: false}) navigate(-1) navigate(url, { state, replace: true})

这 3 种,都能够实现页面跳转,对于用户体验也是有差别的。

3.「返回」按钮的难题

「返回」按钮,做好用户体验,挺难的。这里列举一些容易想到的、但不完满的计划。

3.1 计划一:用 back 实现「返回」

存在的问题:

  • 如果用户间接从 URL 进入该页面,点「返回」有效。
  • 同一个页面,如果起源不同,点「返回」,回到的页面也不同,会让用户困惑。

其实,如果用 back 实现「返回」按钮,这个按钮元素会有点多余,因为它与浏览器原生的「返回」能力一样。

3.2 计划二:用 push 实现「返回」

这种形式解决了 back 导致的 2 个问题,但并不完满。

存在的问题:

  • 页面浏览记录栈收缩迅速,剥夺了用户应用原生「返回」按钮的权力。

我解释一下。比方有个 初始页面 H ,用户从 初始页面 H 跳转到了 列表页 A ,用户通过点击 列表页 A 外面的 详情 Ax 链接 (x 代表一个正整数,列表页通常有多个详情链接),能够进入 详情页 Ax。在 详情页 Ax中,能够点 网页「返回」按钮 ,回到 列表页 A

当用户在 列表页 A 详情页 Ax之间屡次通过 详情 Ax 链接 网页「返回」按钮 来回切换时,页面浏览记录曾经累积很多了,用户若想通过浏览器 原生「返回」按钮 ,再返回 初始页面 H ,是须要按很屡次返回的。

但用户没有这个急躁。

所以你不得不在 列表页 A 减少一个 网页「返回」按钮 ,用于跳转 初始页面 H 。这就诞生了新的问题:

  • 如果一个 列表页 A 的起源,不止 初始页面 H ,还有多个页面能够跳转 列表页 A ,那么 列表页 A 网页「返回」按钮,应该返回到哪里呢?

除此之外,我想强调一句:

剥夺用户应用原生「返回」按钮的权力,不是一件坏事。

尤其是对于安卓端用户,重度依赖原生「返回」操作(在屏幕边缘左滑或右滑)。网页突破了他们的操作习惯,只能表明网页用户体验做的不够好。

4. 网页「返回」按钮,什么成果才是合乎用户认知的?

这里,我想先提出「页面层级」的概念。

4.1 页面层级

假如网站有这样的构造:

它是一个树状构造,每个页面、模块划分十分清晰。

什么是页面层级?

同一层子结点,称之为同一个「页面层级」。(例如图中模块 A、B、C 就是同一层级)

4.2 基于此定义,咱们能够提出这样的产品准则:

  • 页面跳转 (push) 或后退 (forward),只容许 相邻页面层级 从左往右跳转
  • 网页里的「返回」按钮 (back),只容许 相邻页面层级 从右往左返回
  • 对于 同一页面层级 的跳转:能够限度,必须先返回某结点的父结点,再进入该结点的兄弟结点。如果的确有疾速跳转的诉求,只能用 replace 实现。
  • 不容许 跨模块的跳转(如模块 A 某页面跳模块 B 某页面)。如果肯定须要这种跳转,只能在新标签页关上。
  • 不容许 跨层级的跳转(如第 2 层级间接跳转第 4 层级、或第 4 层级跳到第 2 层级)。如果肯定须要这种跳转,只能在新标签页关上。

这样,页面整体跳转逻辑,是十分清晰的,对于用户而言,也容易了解你的逻辑。

4.3 为什么这样定义产品准则?

产品准则的指标:让浏览器的历史记录栈与网页构造保持一致

  • 用户进入更深的页面层级,浏览器的历史记录栈就增 1。
  • 用户返回更浅的页面层级,浏览器的历史记录栈就减 1。

而浏览器原生的「返回」,正是使浏览器的历史记录栈回退 1 个。这样两种「返回」就归一了。

这件就解决了「3.2 计划二」中的问题,达到这样的成果:

  • 保留用户应用原生「返回」的权力。
  • 使网页「返回」按钮具备惟一目的地。

但网页「返回」按钮还有个问题必须解决:若浏览器以后历史记录栈为空,或历史记录栈的上个页面并非该网页的页面,点「返回」,应该也能返回它的父页面。

当初我通知你,这个技术难点,是有解的!

4.4 实现计划

「返回」按钮,逻辑如下

  1. 判断历史记录栈的上个页面,是不是我的父页面。
  2. 如果是我的父页面,我就用history.back(),应用浏览器原生返回行为。
  3. 如果不是我的父页面,我就用history.replace(),使以后页面替换为我的父页面。(不能用 push,否则在父页面返回,回到了子页面,是反直觉的)

难点:如何判断历史记录栈的上个页面,是不是我的父页面。

问题:浏览器基于安全性,不容许你读取历史记录栈。

解决方案

只有父页面跳转到子页面时,携带个「标识」,告知子页面,跳转起源。子页面就晓得了。

跳转时的「标识」,刚好能够用 history.pushState() 中的 state 来实现。

实现跳转链接(即我上篇文章提到的 Link Button)

只有是外部跳转,都封装一个对立的组件。该组件容许定义跳转目的地,而且会在 state 中携带「标识」(如果你的网页有带自定义 state 的诉求,则还须要在该组件中组装一下参数中的 state 和「标识」,变成新的state)。

实现返回链接(比方叫 BackLinkButton)

获取以后页面的state,如果蕴含了「标识」,则间接history.back();否则,用history.replaceState(留神 replace 时不必带「标识」)。

其它问题

理论应用中,发现一个问题,我间接举实在案例。

我的五子棋,联机对战模式,页面分为 3 个层级:首页、对战房间、单机演练。依照如下流程操作:

  1. 用户间接输出网址进入第 2 层级(对战房间),此时没「标识」。
  2. 用户点「单机演练」,携带「标识」,进入第 3 层级。
  3. 用户点「返回房间」,发现此页面 state 有「标识」,触发浏览器原生返回,返回第 2 层级。
  4. 用户点「来到房间」(此页面 state 没「标识」,会通过 replace 进入第 1 层级)。
  5. 用户点「后退」,会间接到第 3 层级。不合乎预期。

为了解决这个状况,我做了兼容解决:

如果以后页面 state 没「标识」,如果以后浏览器历史记录栈长度为 1,间接 replace 是没问题的,不会呈现上述问题;但如果以后浏览器历史记录栈长度大于 1,我调用 replace 后,须要间断调用一次 push 和一次 back,目标是清空浏览器「后退」的历史记录栈。

关上网址 https://game.hullqin.cn/wzq/b…,会间接进入第 2 层级。你能够按上述流程操作下。你不会遇到问题,因为这个问题曾经被解决了,体验好很多。

代码片段参考

这是 LinkButton 逻辑,其中 back 参数,true示意是返回按钮,false示意是跳转按钮。我的 state 中「标识」叫做keepSession

if (back) {
  return (<BackLink to={to}>
      {children}
    </BackLink>
  );
}
return (<Link to={to} state={{...state, keepSession: true}} onClick={handleClick}>
    {children}
  </Link>
);

这是 BackLink 外围逻辑(注:navigate是 React Router@6 提供的函数)

const handleClick = (event) => {if (event.button !== 0) return;
  if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
  event.preventDefault();
  if (keepSession) {navigate(-1);
  } else if (window.history.length === 1) {navigate(to, { replace: true});
  } else {navigate(to, { replace: true});
    // 通过上面形式刷新浏览器 "后退" 记录,免得通过 "后退" 进入不符预期的页面
    navigate(to);
    navigate(-1);
  }
};
return (<Link to={to} onClick={handleClick}>
    {children}
  </Link>
);

如果你好奇 event.xxxKeyevent.preventDefault() 那 3 行代码,请肯定要看下这篇文章:《你的 Link Button 能让用户抉择新页面关上吗?》

5. 结束语

另一个问题:多页面利用

如果你的父页面和子页面,不是同一套前端代码,而是两套前端代码。也就是说,它整体不是单页面利用(SPA),而是多页面利用(MPA),该怎么办呢?

请持续浏览:《多页面利用里,「网页内返回」按钮,何时用 history.back 何时用 replaceState?》。

聊聊天

只有你的页面里,没有「返回」按钮,那啥事都没有 😁

如果你的页面,不谋求挪动端的极致用户体验,那也没啥事,PC 端用户对原生「返回」的依赖没那么重,你想剥夺就剥夺吧 😁

而我要做挪动端页面,有些状况下,原生「返回」是无奈返回上一层级的(例如用户间接从 url 进入了第 2 层级,原生返回只能敞开页面,不能返回第 1 层级),所以我在网页加了「返回」按钮。与此同时,我还没剥夺用户应用原生「返回」的权力。总算是实现了令我称心的「返回」😎

如果你想体验我的游戏,看看「返回」的交互,欢送关上我这篇文章,结尾有游戏地址哦:《我做了个《联机桌游合集: UNO+ 斗地主 + 五子棋》无需下载,点开即玩!叫上敌人,即刻开局!不看广告,不做工作,享受「纯正」的游戏!》

6. 写在最初

我是 HullQin,公众号 线下团聚游戏 的作者(欢送关注公众号,发送加微信,交个敌人),转发本文前需取得作者 HullQin 受权。我独立开发了《联机桌游合集》,是个网页,能够很不便的跟敌人联机玩斗地主、五子棋等游戏,不免费没广告。还开发了《Dice Crush》加入 Game Jam 2022。喜爱能够关注我 HullQin 噢~我有空了会分享做游戏的相干技术。

退出移动版