在发出 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 有效):
我想到的第一件事就是删除历史记录!但是,这似乎不可能(单个重定向除外)。否则,如果一个网站可以欺负用户其他网站的历史记录,这可能会导致明显的隐私问题!
下一个想法是改变浏览器的历史。我认为 可能 只将主页(包含为 AJAX 响应创建的所有 DOM 元素的页面)注入 window.history
会使浏览器忽略接下来发生的任何事情。那我错了。
我还想到了将整个 DOM 元素存储在一个变量中并将其推送到 window.history
的丑陋解决方案,然后检测刷新事件并区分它来自直接访问将是下一个挑战。
我也试过监听 beforeunload 事件,试图阻止刷新然后自己更改位置(这样听起来像是实际刷新),但更改了浏览器的 url发生 在 刷新按钮被点击后,因此当刷新按钮被点击时,新的 URL 不会影响浏览器已经向其发送请求的路由。
那么有没有办法用 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-1
或 app.com/#section-1
.
我的实现有两个局限性:
- API 和应用路由必须匹配。
- 路由不能有多个参数。
两者都是可修复的,我不会详细说明,因为它脱离了简单 POC 的要点,但我必须指出这一点。第一个可以通过使用某种字典来修复,该字典将我们的路由匹配到它们应该获取的端点。第二个问题可以通过在 links 的事件监听器中使逻辑更复杂一些来解决,扩展简单的 links[i].href.split('/').pop()
以包含所有预期的参数。
接下来我们有历史变化的事件侦听器。由于我们将 API 返回的内容存储在历史状态本身中,所以当历史发生变化时,我们所要做的就是用我们的 state.content
.
重新填充 content_box
最后,我们有 ready 函数,在 DOM 最初结束加载时调用:
我们检查 URL 以获取最后一个参数或 hash/anchor 的值。然后我们验证我们从 URL 得到的内容是否存在于我们定义的内部路由数组中。如果是,我们调用 get
函数并将其作为参数。如果没有,我们从数组中获取第一条路线并用它来调用它。
我已经用了大约一个星期的时间来编写我的第一个单页网站,依靠“AJAX 调用”,认为这些会给我一个“SPA”,然而,在某些时候,当我点击[幸运] 顶部的刷新按钮,我看到的是最新 AJAX 调用的字符串结果,在浏览器的白页上,而不是我网站的 HTML 页面!
实际上,原因是我将浏览器的 URL 更改为 window.history.pushState
,然后请求直接发送到后端,而不是通过我的 JavaScript 代码。
到目前为止我自己尝试过的事情(盲目尝试)(TL;DR-Caution:None 有效):
我想到的第一件事就是删除历史记录!但是,这似乎不可能(单个重定向除外)。否则,如果一个网站可以欺负用户其他网站的历史记录,这可能会导致明显的隐私问题!
下一个想法是改变浏览器的历史。我认为 可能 只将主页(包含为 AJAX 响应创建的所有 DOM 元素的页面)注入
window.history
会使浏览器忽略接下来发生的任何事情。那我错了。我还想到了将整个 DOM 元素存储在一个变量中并将其推送到
window.history
的丑陋解决方案,然后检测刷新事件并区分它来自直接访问将是下一个挑战。我也试过监听 beforeunload 事件,试图阻止刷新然后自己更改位置(这样听起来像是实际刷新),但更改了浏览器的 url发生 在 刷新按钮被点击后,因此当刷新按钮被点击时,新的 URL 不会影响浏览器已经向其发送请求的路由。
那么有没有办法用 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.
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-1
或 app.com/#section-1
.
我的实现有两个局限性:
- API 和应用路由必须匹配。
- 路由不能有多个参数。
两者都是可修复的,我不会详细说明,因为它脱离了简单 POC 的要点,但我必须指出这一点。第一个可以通过使用某种字典来修复,该字典将我们的路由匹配到它们应该获取的端点。第二个问题可以通过在 links 的事件监听器中使逻辑更复杂一些来解决,扩展简单的 links[i].href.split('/').pop()
以包含所有预期的参数。
接下来我们有历史变化的事件侦听器。由于我们将 API 返回的内容存储在历史状态本身中,所以当历史发生变化时,我们所要做的就是用我们的 state.content
.
content_box
最后,我们有 ready 函数,在 DOM 最初结束加载时调用:
我们检查 URL 以获取最后一个参数或 hash/anchor 的值。然后我们验证我们从 URL 得到的内容是否存在于我们定义的内部路由数组中。如果是,我们调用 get
函数并将其作为参数。如果没有,我们从数组中获取第一条路线并用它来调用它。