paint-brush
关于如何在 JavaScript 增强表单中取消重复获取请求的指南经过@austingil
2,570 讀數
2,570 讀數

关于如何在 JavaScript 增强表单中取消重复获取请求的指南

经过 Austin Gil8m2023/02/10
Read on Terminal Reader

太長; 讀書

您很可能不小心引入了重复请求/竞争条件错误。今天,我将向您介绍这个问题以及我为避免它而提出的建议。这里的关键问题是`event.preventDefault()`。此方法阻止浏览器执行加载新页面和提交表单的默认行为。
featured image - 关于如何在 JavaScript 增强表单中取消重复获取请求的指南
Austin Gil HackerNoon profile picture

如果您曾经使用 JavaScript fetch API 来增强表单提交,那么您很可能不小心引入了重复请求/竞争条件错误。今天,我将向您介绍这个问题以及我为避免它而提出的建议。


(如果你喜欢的话,视频在最后)


让我们考虑一个非常基本的HTML 表格带有一个输入和一个提交按钮。


 <form method="post"> <label for="name">Name</label> <input id="name" name="name" /> <button>Submit</button> </form> 


当我们点击提交按钮时,浏览器将刷新整个页面。


请注意单击提交按钮后浏览器如何重新加载。


页面刷新并不总是我们想要为用户提供的体验,因此一个常见的替代方法是使用JavaScript向表单的“提交”事件添加事件侦听器,防止默认行为,并使用fetch API 提交表单数据。


一种简单的方法可能类似于下面的示例。


页面(或组件)挂载后,我们抓取表单 DOM 节点,添加一个使用表单构造获取请求fetch事件监听器行动,方法, 和数据,在处理程序的末尾,我们调用事件的preventDefault()方法。


 const form = document.querySelector('form'); form.addEventListener('submit', handleSubmit); function handleSubmit(event) { const form = event.currentTarget; fetch(form.action, { method: form.method, body: new FormData(form) }); event.preventDefault(); }


现在,在任何 JavaScript 能手开始向我发推文之前,我会讨论 GET 与 POST 和请求正文以及内容类型不管其他什么,我只想说,我知道。我故意让fetch请求保持简单,因为那不是主要焦点。


这里的关键问题是event.preventDefault() 。此方法阻止浏览器执行加载新页面和提交表单的默认行为。


现在,如果我们查看屏幕并点击提交,我们可以看到页面没有重新加载,但我们确实在网络选项卡中看到了 HTTP 请求。


请注意,浏览器不会重新加载整个页面。


不幸的是,通过使用 JavaScript 来阻止默认行为,我们实际上引入了默认浏览器行为所没有的错误。


当我们使用纯HTML然后你很快地点击提交按钮很多次,你会注意到除了最近的一个之外所有的网络请求都变成了红色。这表明它们已被取消,只有最近的请求才会得到满足。


如果我们将其与 JavaScript 示例进行比较,我们将看到所有请求都已发送,并且所有请求都已完成且没有被取消。


这可能是一个问题,因为尽管每个请求可能需要不同的时间,但它们的解决顺序可能与发起时的顺序不同。这意味着如果我们为这些请求的解析添加功能,我们可能会出现一些意想不到的行为。


例如,我们可以创建一个变量来为每个请求递增(“ totalRequestCount ”)。每次我们运行handleSubmit函数时,我们都可以增加总计数并捕获当前数量以跟踪当前请求(“ thisRequestNumber ”)。


fetch请求解决时,我们可以将其相应的编号记录到控制台。


 const form = document.querySelector('form'); form.addEventListener('submit', handleSubmit); let totalRequestCount = 0 function handleSubmit(event) { totalRequestCount += 1 const thisRequestNumber = totalRequestCount const form = event.currentTarget; fetch(form.action, { method: form.method, body: new FormData(form) }).then(() => { console.log(thisRequestNumber) }) event.preventDefault(); }


现在,如果我们多次点击那个提交按钮,我们可能会看到打印到控制台的不同数字乱序:2、3、1、4、5。这取决于网络速度,但我想我们都同意这并不理想。


考虑这样一种情况,用户连续触发多个fetch请求,完成后,您的应用程序会用他们的更改更新页面。由于请求解析顺序不正确,用户最终可能会看到不准确的信息。


这在非 JavaScript 世界中不是问题,因为浏览器会取消任何先前的请求并在最近的请求完成后加载页面,加载最新版本。但是页面刷新并不那么性感。


对于 JavaScript 爱好者来说,好消息是我们可以同时拥有性感的用户体验和一致的用户界面!


我们只需要做更多的跑腿工作。


如果查看fetch API 文档,您会发现可以使用AbortControllerfetch选项的signal属性中止获取。它看起来像这样:


 const controller = new AbortController(); fetch(url, { signal: controller.signal });


通过向fetch请求提供AbortContoller的信号,我们可以在触发AbortContollerabort方法时随时取消请求。


您可以在 JavaScript 控制台中看到更清晰的示例。尝试创建一个AbortController ,发起fetch请求,然后立即执行abort方法。


 const controller = new AbortController(); fetch('', { signal: controller.signal }); controller.abort()


您应该立即看到一个异常打印到控制台。在 Chromium 浏览器中,它应该说,“未捕获(承诺)DOMException:用户中止了请求。”如果您浏览“网络”选项卡,您应该会看到状态文本为“(已取消)”的失败请求。


Chrome 开发工具在打开 JavaScript 控制台的情况下向网络开放。在控制台中是代码“const controller = new AbortController();fetch('', { signal: controller.signal });controller.abort()”,后面是异常“Uncaught (in promise) DOMException: The用户中止了一个请求。”在网络中,有一个到“本地主机”的请求,状态文本为“(已取消)”

考虑到这一点,我们可以将AbortController添加到表单的提交处理程序中。逻辑如下:


  • 首先,检查任何先前请求的AbortController 。如果存在,则中止它。


  • 接下来,为当前请求创建一个AbortController ,它可以在后续请求中中止。


  • 最后,当请求解决时,删除其对应的AbortController


有几种方法可以做到这一点,但我将使用WeakMap来存储每个提交的<form> DOM 节点与其各自的AbortController之间的关系。提交表单时,我们可以相应地检查和更新WeakMap


 const pendingForms = new WeakMap(); function handleSubmit(event) { const form = event.currentTarget; const previousController = pendingForms.get(form); if (previousController) { previousController.abort(); } const controller = new AbortController(); pendingForms.set(form, controller); fetch(form.action, { method: form.method, body: new FormData(form), signal: controller.signal, }).then(() => { pendingForms.delete(form); }); event.preventDefault(); } const forms = document.querySelectorAll('form'); for (const form of forms) { form.addEventListener('submit', handleSubmit); }


关键是能够将中止控制器与其相应的表单相关联。使用表单的 DOM 节点作为WeakMap的键是一种方便的方法。


有了它,我们可以将AbortController的信号添加到fetch请求,中止任何以前的控制器,添加新控制器,并在完成后删除它们。


希望这一切都有意义。


现在,如果我们多次点击该表单的提交按钮,我们可以看到除了最近的请求之外的所有 API 请求都被取消了。


这意味着响应该 HTTP 响应的任何函数都将按照您的预期运行。


现在,如果我们使用与上面相同的计数和日志记录逻辑,我们可以点击提交按钮七次,并且会在控制台中看到六个异常(由于AbortController )和一个“7”的日志。


如果我们再次提交并留出足够的时间来解决请求,我们会在控制台中看到“8”。如果我们多次按下提交按钮,我们将继续以正确的顺序看到异常和最终请求计数。


如果您想添加更多逻辑以避免在请求中止时在控制台中看到 DOMExceptions,您可以在fetch请求之后添加一个.catch()块并检查错误名称是否与“ AbortError ”匹配:


 fetch(url, { signal: controller.signal, }).catch((error) => { // If the request was aborted, do nothing if (error.name === 'AbortError') return; // Otherwise, handle the error here or throw it back to the console throw error });

关闭

整篇文章的重点是 JavaScript 增强的表单,但在您创建fetch请求时包含一个AbortController可能是个好主意。真的太糟糕了,它还没有内置到 API 中。但希望这向您展示了一个包含它的好方法。


还值得一提的是,这种方法并不能防止用户多次点击提交按钮。按钮仍然可以点击,请求仍然被触发,它只是提供了一种更一致的处理响应的方式。


不幸的是,如果用户向提交按钮发送垃圾邮件,这些请求仍会转到您的后端,并且可能会消耗大量不必要的资源。


一些天真的解决方案可能是禁用提交按钮,使用去抖动,或者仅在先前的请求解决后创建新请求。我不喜欢这些选项,因为它们依赖于减慢用户体验并且只在客户端工作。


他们不会通过脚本请求解决滥用问题。


为了解决对服务器的过多请求造成的滥用,您可能需要设置一些速率限制.这超出了本文的范围,但值得一提。


还值得一提的是,速率限制并不能解决重复请求、竞争条件和不一致的 UI 更新等原始问题。理想情况下,我们应该同时使用两者来覆盖两端。


无论如何,这就是我今天的全部内容。如果您想观看涵盖同一主题的视频,请观看此视频。

非常感谢您的阅读。如果您喜欢这篇文章,请分享它.这是支持我的最好方式之一。你也可以注册我的时事通讯或者在推特上关注我如果您想知道新文章何时发布。


最初发表于austingil.com .