关于前端:极致用户体验-多页面应用里网页内返回按钮何时用-historyback-何时用-replaceState

3次阅读

共计 4793 个字符,预计需要花费 12 分钟才能阅读完成。

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

背景

上篇文章《网页里的「返回」应该用 history.back 还是 push?》论证了单页面利用(Single-Page Application,简称 SPA)如何实现网页内的「返回」按钮,本篇文章将会论证多页面利用(Multi-Page Application,简称 MPA)如何实现网页内的「返回」按钮。

何谓「极致用户体验」

上文我提到,网站应该是有页面层级的:

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

如果要谋求极致用户体验,用户在浏览器点击「后退」或「返回」时,应该遵循这样的规定:

  • 点浏览器的「后退」按钮 (forward,右箭头),只容许 相邻页面层级 从左往右跳转
  • 点浏览器的「返回」按钮 (back,左箭头),只容许 相邻页面层级 从右往左返回

实现计划

要实现这样的规定,开发者必须管制好浏览器的历史记录栈:

  • 用户进入更深的页面层级,浏览器的历史记录栈就增 1。
  • 用户返回更浅的页面层级,浏览器的历史记录栈就减 1。但历史记录栈无奈减 1 时,能够让历史记录栈数量放弃不变。

我解释一下,开发者怎么管制历史记录栈?

什么时候历史记录栈增一? 当咱们调用 history.pushState() 时,浏览器历史记录栈就会新增一个历史记录,次要存了 URL 等信息。此时,用户点击「浏览器返回」和「浏览器后退」,就能够在「上一个页面」和「以后页面」重复横跳。

什么时候历史记录栈减一? 当咱们调用 history.back() 时,就能够让浏览器历史记录栈减一。其实严格来说不算减一,只是页面回退到了上一条记录,这相当于用户点了「浏览器返回」按钮。

有时候点「网页返回」按钮,不能间接调用 history.back,为什么? 如果调用 history.back() 会返回其它界面(或者用户是间接关上了咱们的某个页面,没有上一条历史记录了,「浏览器返回」按钮也是灰色),即调用 history.back() 无奈返回咱们本人网站的上一页面层级,就应该调用history.replaceState(),跳到上一页面层级。留神不能用history.push,如果用了 push 会突破咱们的准则,那时候再点「浏览器返回」就从左往右导航了,违反了咱们的网站页面层级。

回顾单页面利用计划

只有父页面跳转到子页面时,携带个「标识」,告知子页面,跳转起源是你亲爸爸。子页面就晓得了,自页面的「网页返回」按钮,能够间接触发 history.back() 返回。

如果子页面发现没有「标识」,阐明不是亲爸爸跳转到该子页面的,通过 history.back() 无奈返回亲爸爸页面。不得不通过 history.replaceState() 返回亲爸爸页面,并且去的时候,不能带「标识」,因为子页面不是父页面的亲爸爸。

跳转时的「标识」,能够用 history.pushState() 中的 state 来实现。绝不能用 URL 中的参数来实现。因为 URL 太容易伪造了,可能用户点个珍藏、复制个网址,就把标识给带上了。然而 state 相对足够荫蔽。

多页面利用计划

问题形容

我的父页面 game.hullqin.cn 和 子页面 game.hullqin.cn/wzq 是部署了两套前端代码,他们是 MPA。

在子页面有个「游戏列表」按钮,相当于我的「网页返回」按钮。我冀望这两个页面合乎网站页面层级规范:

  • 如果能够通过 hittory.back() 返回首页就用它。
  • 如果 hittory.back() 无奈返回首页,就用history.replaceState()

难点 1

间接调用 history.pushState(),能够传递state 标识,但这只会批改 URL,并不会触发浏览器刷新,网页仍然停留在父页面。

间接调用 window.location.href = 'game.hullqin.cn/wzq' 会使浏览器刷新,然而不能传递 state 标识。可能还须要借助 sessionStorage 计划来保留、传递「标识」,但这又引入了更高的复杂度,因为它是跟历史记录栈无关的,咱们不得不在 sessionStorage 中存一些路由信息,能力正确传递「标识」。

解决难点 1

先调用 history.pushState(),传递state 标识,再调用 window.location.reload() 触发刷新。这会放弃 state 给下一个页面。

难点 2

如果咱们是通过调用 history.pushState() 来减少浏览器历史记录栈的,那么咱们调用 history.back() 时,页面不会刷新,只扭转 URL。

兴许你就说:像方才一样,调用 history.back() 后再调用 window.location.reload() 触发刷新,不就解决了吗?

但这里还有一点:用户点击「浏览器返回」按钮时,只会改 URL,页面不会刷新。尽管网址曾经是首页了,然而界面仍然是在 game.hullqin.cn/wzq 这个子页面。

相似的,用户在父页面点「浏览器后退」按钮筹备进入 game.hullqin.cn/wzq 这个子页面时,也会只改 URL,页面不刷新。

解决难点 2

监听 window.onpopstate 事件,这个事件会在用户点「浏览器返回」按钮或「浏览器后退」按钮时触发。咱们监听该事件,判断以后页面 URL 是否合乎以后页面的路由规定。如果有差别,就调用 window.location.reload() 触发刷新。

代码

父页面外围代码

你能够参考 game.hullqin.cn 的网页源码,这是一个十分简洁的门户页面。

<div style="flex-grow:1;display:flex;flex-direction:column;justify-content:center">
  <a class="game" href="/uno"><img alt=""class="logo"src="https://fe-1255520126.file.myqcloud.com/uno/logo.svg"/><span>UNO</span></a>
  <a class="game" href="/ddz"><img alt=""class="logo"src="https://fe-1255520126.file.myqcloud.com/ddz/logo.svg"/><span> 斗地主 </span></a>
  <a class="game" href="/wzq"><img alt=""class="logo"src="https://fe-1255520126.file.myqcloud.com/wzq/logo.svg"/><span> 五子棋 </span></a>
</div>

<script>
Array.from(document.getElementsByClassName('game')).forEach(game => {game.addEventListener('click', (event) => {if (event.button !== 0) return;
    if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
    event.preventDefault();
    window.history.pushState({key: Math.random().toString(36).substring(2, 10), usr: {keepSession: true}}, '', game.getAttribute('href'));
    window.location.reload();});
});
window.addEventListener('popstate', () => {if (window.location.pathname !== '/') window.location.reload();});
</script>

留神点:调用 history.pushState() 时,传递的 state 标识是:

{key: Math.random().toString(36).substring(2, 10),
  usr: {keepSession: true},
}

这是为了合乎子页面 React-Router state的标准,须要蕴含一个随机字符串 key,标记一次会话,用usr 存储开发者自定义数据。

正如我上篇文章提到的,我为了标识子页面来自亲爸爸,是用了 keepSession 这个名字。

子页面外围代码

子页面我应用了 React 框架和 React-Router。「网页返回」按钮外围逻辑如下:

import {Link, useLocation, useNavigate} from 'react-router-dom';

function BackLink(props: BackLinkProps) {
  const {to, children, className,} = props;
  const navigate = useNavigate();
  const {state} = useLocation();
  const keepSession = state.keepSession;
  const handleClick = (event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => {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} className={className} onClick={handleClick}>
      {children}
    </Link>
  );
}
// 另外还有以下逻辑,能够写在另一个 JS 文件或间接写在 html 中:window.addEventListener('popstate', () => {if (!window.location.pathname.startsWith("/wzq")) window.location.reload();});

兴许你会好奇 if (event.button !== 0) return;if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;这两行代码,欢送阅读文章:《你的 Link Button 能让用户抉择新页面关上吗?》。

写在最初

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

正文完
 0