如果您曾经使用 JavaScript fetch
API 来增强表单提交,那么您很可能不小心引入了重复请求/竞争条件错误。今天,我将向您介绍这个问题以及我为避免它而提出的建议。
(如果你喜欢的话,视频在最后)
让我们考虑一个非常基本的
<form method="post"> <label for="name">Name</label> <input id="name" name="name" /> <button>Submit</button> </form>
当我们点击提交按钮时,浏览器将刷新整个页面。
请注意单击提交按钮后浏览器如何重新加载。
页面刷新并不总是我们想要为用户提供的体验,因此一个常见的替代方法是使用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 来阻止默认行为,我们实际上引入了默认浏览器行为所没有的错误。
当我们使用纯
如果我们将其与 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 文档,您会发现可以使用AbortController
和fetch
选项的signal
属性中止获取。它看起来像这样:
const controller = new AbortController(); fetch(url, { signal: controller.signal });
通过向fetch
请求提供AbortContoller
的信号,我们可以在触发AbortContoller
的abort
方法时随时取消请求。
您可以在 JavaScript 控制台中看到更清晰的示例。尝试创建一个AbortController
,发起fetch
请求,然后立即执行abort
方法。
const controller = new AbortController(); fetch('', { signal: controller.signal }); controller.abort()
您应该立即看到一个异常打印到控制台。在 Chromium 浏览器中,它应该说,“未捕获(承诺)DOMException:用户中止了请求。”如果您浏览“网络”选项卡,您应该会看到状态文本为“(已取消)”的失败请求。
考虑到这一点,我们可以将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 更新等原始问题。理想情况下,我们应该同时使用两者来覆盖两端。
无论如何,这就是我今天的全部内容。如果您想观看涵盖同一主题的视频,请观看此视频。
非常感谢您的阅读。如果您喜欢这篇文章,请
最初发表于