在发出 AJAX HTTP GET 请求后,如何处理浏览器刷新按钮的这种中断行为?

How to handle this breaking behavior of the refresh button of the browser after making an AJAX HTTP GET request?

我已经用了大约一个星期的时间来编写我的第一个单页网站,依靠“AJAX 调用”,认为这些会给我一个“SPA”,然而,在某些时候,当我点击[幸运] 顶部的刷新按钮,我看到的是最新 AJAX 调用的字符串结果,在浏览器的白页上,而不是我网站的 HTML 页面!

实际上,原因是我将浏览器的 URL 更改为 window.history.pushState,然后请求直接发送到后端,而不是通过我的 JavaScript 代码。

到目前为止我自己尝试过的事情(盲目尝试)(TL;DR-Caution:None 有效):

那么有没有办法用 vanilla JavaScript 来处理这个问题?或者也许我误会了一些简单的事情?或者更好的解决方案是使用 React、VueJS 等工具来“更轻松”地完成我的 SPA?这些工具都没有问题,我只是更喜欢更简单的方法。我想要的 SPA 类似于 MongoDB 网站的 the 文档页面。

编辑:代码:

xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
          if (xhr.status === 200) {
            data = JSON.parse(xhr.responseText);
            content_box.innerHTML = data.content;
            window.history.pushState({"content":data.content, "title": data.title + " | App Manual"}, data.title + " | App Manual", href); 

TL;DR: 转到最后一段

我做了一个简单的概念证明来说明如何实现您想要的,您可以看到它 here

在这个版本中,我使用最后一个 URL 参数来确定应该加载什么。这是标准做法,但它需要 server-side 配置才能将所有传入请求路由到单个文件。

如评论中所述,另一种选择是使用锚片段。我的脚本支持这两种选择,您可以看到 here 使用锚点而不是 URL 重写的上传版本。

代码非常简单(尽管可以通过使用 fetch 或更简洁的 XHR 实现来进一步简化)。

HTML

这里只有两件事与我们有关。一个是 #content-box,我们将从 API 加载的任何内容放置在其中。在我的版本中,它看起来像这样:

<section id="content_box"></section>

另一个是用于内部路由的 a 元素,它必须有 class link 与之关联,如下所示:

<ul>
  <li><a class="link" href="/section-1">Link #1</a></li>
  <li><a class="link" href="/section-2">Link #2</a></li>
</ul>

JS

我们从一般变量的一些基本初始化开始:

const useHash = true;
const apiUrl = 'https://lucasreta.com/stack-overflow/spa-vanilla-js/api';
const routes = ['section-1', 'section-2'];
const content_box = document.getElementById("content_box");

useHash 将决定我们是否应该使用 URL 的锚点(散列),或者我们内部路由​​的最后一个参数。

apiUrl 设置简单 API.

的基数 URL

routes 定义了我们应用程序的有效路径。

content_box 是我们将在没有数据的情况下更新的 DOM 元素。

然后我们定义我们的异步 getter 信息,它仍然是一个非常标准的 XHR 调用,类似于您已经拥有的 (缺少错误处理):

function get(page) {
  const xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      data = JSON.parse(xhr.responseText);
      content_box.innerHTML = data.content;
      const title = `${data.title} | App Manual`;
      window.history.pushState(
        { 'content': data.content, 'title': title},
        title,
        useHash ?
          `#${page}` :
          page
      );
    }
  };
  xhr.open('GET', `${apiUrl}/${page}`, true);
  xhr.send();
}

在这里,我们向 get 函数发送一个名为 page 的参数,该参数与我们将使用的 API 的端点以及我们将在我们的状态中使用的名称和 URL 来确定我们必须显示的内容。

鉴于您在代码中显示的响应对象的简单性,我认为将整个 content-and-title 对象推入我们的历史并稍后从那里使用它是合适的。在更复杂的场景中,我们可能只需要存储 page 参数并对 API.

进行新请求

现在我们必须处理修改单页应用程序状态的三个事件:

// add event listener to links
const links = document.getElementsByClassName('link');
for(let i = 0; i < links.length; i++) {
  links[i].addEventListener('click', function(event) {
    event.preventDefault();
    get(links[i].href.split('/').pop());
  }, false);
}

// add event listener to history changes
window.addEventListener("popstate", function(e) {
  const state = e.state;
  content_box.innerHTML = state.content;
});

// add ready event for initial load of our site
(function(fn = function() {
  const page = useHash ?
    window.location.hash.split('#').pop() :
    window.location.href.split('/').pop();
  get(routes.indexOf(page) >= 0 ? page : routes[0]);
}) {
  if (document.readyState != 'loading'){
    fn();
  } else {
    document.addEventListener('DOMContentLoaded', fn);
  }
})();

首先,我们获取所有带有 class .link 的元素,并为它们附加一个事件侦听器,以便在单击它们时停止默认事件,取而代之的是我们的 get 函数使用 href 的最后一个参数调用。

因此,当我们单击上面列出的第一个 link 时,我们将对 api.com/section-1 执行 GET 请求,并将应用程序的 URL 更新为 app.com/section-1app.com/#section-1.

我的实现有两个局限性:

  • API 和应用路由必须匹配。
  • 路由不能有多个参数。

两者都是可修复的,我不会详细说明,因为它脱离了简单 POC 的要点,但我必须指出这一点。第一个可以通过使用某种字典来修复,该字典将我们的路由匹配到它们应该获取的端点。第二个问题可以通过在 links 的事件监听器中使逻辑更复杂一些来解决,扩展简单的 links[i].href.split('/').pop() 以包含所有预期的参数。

接下来我们有历史变化的事件侦听器。由于我们将 API 返回的内容存储在历史状态本身中,所以当历史发生变化时,我们所要做的就是用我们的 state.content.

重新填充 content_box

最后,我们有 ready 函数,在 DOM 最初结束加载时调用:

我们检查 URL 以获取最后一个参数或 hash/anchor 的值。然后我们验证我们从 URL 得到的内容是否存在于我们定义的内部路由数组中。如果是,我们调用 get 函数并将其作为参数。如果没有,我们从数组中获取第一条路线并用它来调用它。