发布于 2026-01-06 6 阅读
0

用几行原生 JavaScript 代码实现异步表单提交

用几行原生 JavaScript 代码实现异步表单提交

在本教程中,我们将编写一个简单的 JavaScript 事件处理程序,用于提交 HTML 表单,fetch而不是传统的同步重定向表单提交方式。我们构建的解决方案基于渐进增强策略:如果 JavaScript 加载失败,用户仍然可以提交表单;但如果 JavaScript 可用,表单提交过程将更加流畅。在构建此解决方案的过程中,我们将探索 JavaScript DOM API、实用的 HTML 结构以及与辅助功能相关的主题。

我们先来设置一个表单。

本文最初发表于我的个人博客。

设置 HTML

我们来创建一个新闻邮件订阅表单。

我们的表单将包含一个可选的姓名字段和一个必填的电子邮件required地址字段。我们为电子邮件地址字段设置了属性,这样如果该字段为空,表单将无法提交。此外,我们还设置了字段类型,以便email在移动设备上触发电子邮件验证并显示美观的电子邮件键盘布局。

<form action="subscribe.php" method="POST">

  Name
  <input type="text" name="name"/>

  Email
  <input type="email" name="email" required/>

  <button type="submit">Submit</button>

</form>
Enter fullscreen mode Exit fullscreen mode

我们的表单将发布到一个subscribe.php页面上,在我们的例子中,该页面只不过是一个包含一段文字的页面,用于向用户确认她已订阅新闻通讯。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Successfully subscribed!</title>
  </head>
  <body>
    <p>Successfully subscribed!</p>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

让我们快速回到<form>标签部分,做一些小的改进。

如果我们的样式表加载失败,目前渲染效果如下:

所有字段都显示在一行中

对于我们这个小表单来说,这还不算太糟糕,但想象一下,如果表单更大一些,所有字段都挤在同一行,就会显得非常杂乱。让我们把每个标签和字段组合都用一个<div>.

<form action="subscribe.php" method="POST">

  <div>
    Name
    <input type="text" name="name"/>
  </div>

  <div>
    Email
    <input type="email" name="email" required/>
  </div>

  <button type="submit">Submit</button>

</form>
Enter fullscreen mode Exit fullscreen mode

现在每个字段都显示在新的一行上。

所有字段都显示在单独的行上。

另一个改进方案是将字段名称包裹在一个<label>元素中,这样我们就可以将每个标签显式地链接到其对应的输入字段。这不仅允许用户点击标签来聚焦字段,还能在字段获得焦点时触发屏幕阅读器等辅助技术朗读字段标签。

<form action="subscribe.php" method="POST">

  <div>
    <label for="name">Name</label>
    <input type="text" name="name" id="name"/>
  </div>

  <div>
    <label for="email">Email</label>
    <input type="email" name="email" id="email" required/>
  </div>

  <button type="submit">Submit</button>

</form>
Enter fullscreen mode Exit fullscreen mode

只需付出一点点努力,就能显著提升用户体验和易用性。太棒了!

表单填写完毕后,让我们编写一些 JavaScript 代码。

编写表单提交处理程序

我们将编写一个脚本,将页面上的所有表单转换为异步表单。

我们不需要访问页面上的所有表单来进行设置,只需监听表单'submit'提交事件document,并在一个事件处理程序中处理所有表单提交即可。事件目标始终是已提交的表单,因此我们可以使用以下方式访问表单元素:e.target

为了防止发生经典的表单提交,我们可以使用对象preventDefault上的方法event,这将阻止浏览器执行默认操作。

如果只想处理单个表单,可以通过将事件监听器附加到该特定表单元素来实现。

document.addEventListener('submit', e => {

  // Store reference to form to make later code easier to read
  const form = e.target;

  // Prevent the default form submit
  e.preventDefault();

});
Enter fullscreen mode Exit fullscreen mode

好了,我们现在可以发送表单数据了。

此操作分为两部分,发送部分和数据部分。

我们可以使用 API 来发送数据fetch,我们可以使用一个名为 的非常方便的 API 来收集表单数据FormData

document.addEventListener('submit', e => {

  // Store reference to form to make later code easier to read
  const form = e.target;

  // Post data using the Fetch API
  fetch(form.action, {
    method: form.method,
    body: new FormData(form)
  })

  // Prevent the default form submit
  e.preventDefault();

});
Enter fullscreen mode Exit fullscreen mode

是的,我没开玩笑,就是这么简单。

第一个参数fetch是 URL,所以我们传递form.action包含 URL 的属性subscribe.php。然后我们传递一个配置对象,其中包含要使用的 URL ,该 URL 从属性method中获取。最后,我们需要传递属性中的数据。我们可以直接将元素作为参数传递给构造函数,它会为我们创建一个类似于经典表单提交的对象,并以表单提交form.methodPOSTbodyformFormDatamultipart/form-data

Michael Scharnagl建议将调用移到preventDefault()最后,这样可以确保只有在我们所有的 JavaScript 都运行时才会阻止经典的提交操作。

我们搞定了!去酒吧!

咱们去温彻斯特酒吧,喝杯冰镇啤酒,等这一切过去吧。

当然,我们漏掉了一些细节,以上基本上是流程最顺畅的部分,所以先别急着下结论,也别急着喝啤酒。我们该如何处理连接错误?如何通知用户订阅成功?以及在请求订阅页面时会发生什么?

边缘案例

我们先来处理如何通知用户订阅成功的问题。

展现成功状态

我们可以通过从 subscribe.php 页面提取消息并将其显示在表单元素上方来实现这一点。接下来,我们继续fetch处理该语句之后的调用结果fetch

首先,我们需要将响应转换为text基于文本的响应。然后,我们可以使用 API 将这种基于文本的响应转换为实际的 HTML 文档DOMParser。我们告诉 API 解析我们的文本并将其视为text/htmlHTML 文档,然后返回结果,以便在下一个过程中使用。then

现在我们有了一个可以处理的 HTML 文档(doc),终于可以把表单替换成成功状态了。我们将复制该状态body.innerHTML到我们的 `<div>` 标签中result.innerHTML,然后用新创建的结果元素替换表单。最后,我们将焦点移到结果元素上,这样屏幕阅读器用户可以朗读结果,键盘用户可以从页面上的该位置继续导航。

document.addEventListener('submit', e => {

  // Store reference to form to make later code easier to read
  const form = e.target;

  // Post data using the Fetch API
  fetch(form.action, {
      method: form.method,
      body: new FormData(form)
    })
    // We turn the response into text as we expect HTML
    .then(res => res.text())

    // Let's turn it into an HTML document
    .then(text => new DOMParser().parseFromString(text, 'text/html'))

    // Now we have a document to work with let's replace the <form>
    .then(doc => {

      // Create result message container and copy HTML from doc
      const result = document.createElement('div');
      result.innerHTML = doc.body.innerHTML;

      // Allow focussing this element with JavaScript
      result.tabIndex = -1;

      // And replace the form with the response children
      form.parentNode.replaceChild(result, form);

      // Move focus to the status message
      result.focus();

    });

  // Prevent the default form submit
  e.preventDefault();

});
Enter fullscreen mode Exit fullscreen mode

连接故障

如果我们的连接失败,fetch呼叫将被拒绝,我们可以通过以下方式处理这种情况:catch

首先,我们在 HTML 表单中添加一条消息,以便在连接失败时显示。我们将其放在提交按钮上方,以便在出现问题时能够清楚地看到它。

<form action="subscribe.php" method="POST">

  <div>
    <label for="name">Name</label>
    <input type="text" name="name" id="name"/>
  </div>

  <div>
    <label for="email">Email</label>
    <input type="email" name="email" id="email" required/>
  </div>

  <p role="alert" hidden>Connection failure, please try again.</p>

  <button type="submit">Submit</button>

</form>
Enter fullscreen mode Exit fullscreen mode

通过使用该hidden属性,我们已经<p>对所有人隐藏了段落。我们还role="alert"为段落添加了一个事件,这样当段落可见时,屏幕阅读器就会朗读段落内容。

现在我们来处理 JavaScript 部分。

我们在拒绝处理程序中放入的代码fetchcatch)将选择我们的警告段落并将其显示给用户。

document.addEventListener('submit', e => {

  // Store reference to form to make later code easier to read
  const form = e.target;

  // Post data using the Fetch API
  fetch(form.action, {
      method: form.method,
      body: new FormData(form)
    })
    // We turn the response into text as we expect HTML
    .then(res => res.text())

    // Let's turn it into an HTML document
    .then(text => new DOMParser().parseFromString(text, 'text/html'))

    // Now we have a document to work with let's replace the <form>
    .then(doc => {

      // Create result message container and copy HTML from doc
      const result = document.createElement('div');
      result.innerHTML = doc.body.innerHTML;

      // Allow focussing this element with JavaScript
      result.tabIndex = -1;

      // And replace the form with the response children
      form.parentNode.replaceChild(result, form);

      // Move focus to the status message
      result.focus();

    })
    .catch(err => {

      // Some form of connection failure
      form.querySelector('[role=alert]').hidden = false;

    });

  // Make sure connection failure message is hidden
  form.querySelector('[role=alert]').hidden = true;

  // Prevent the default form submit
  e.preventDefault();

});
Enter fullscreen mode Exit fullscreen mode

我们使用 CSS 属性选择器来选择提示段落,[role=alert]无需类名。当然,将来可能也需要类名,但有时通过属性选择就足够了。

我认为我们已经考虑到了各种极端情况,让我们再完善一下吧。

加载时锁定字段

如果表单在发送到服务器时能锁定所有输入字段就好了。这样可以防止用户多次点击提交按钮,也可以防止在等待处理完成期间编辑字段。

我们可以使用该form.elements属性选择所有表单字段,然后禁用每个字段。

如果你<fieldset>的表单中包含一个字段集,你可以禁用该字段集,这将禁用其中的所有字段。

document.addEventListener('submit', e => {

  // Store reference to form to make later code easier to read
  const form = e.target;

  // Post data using the Fetch API
  fetch(form.action, {
      method: form.method,
      body: new FormData(form)
    })
    // We turn the response into text as we expect HTML
    .then(res => res.text())

    // Let's turn it into an HTML document
    .then(text => new DOMParser().parseFromString(text, 'text/html'))

    // Now we have a document to work with let's replace the <form>
    .then(doc => {

      // Create result message container and copy HTML from doc
      const result = document.createElement('div');
      result.innerHTML = doc.body.innerHTML;

      // Allow focussing this element with JavaScript
      result.tabIndex = -1;

      // And replace the form with the response children
      form.parentNode.replaceChild(result, form);

      // Move focus to the status message
      result.focus();

    })
    .catch(err => {

      // Show error message
      form.querySelector('[role=alert]').hidden = false;

    });

  // Disable all form elements to prevent further input
  Array.from(form.elements).forEach(field => field.disabled = true);

  // Make sure connection failure message is hidden
  form.querySelector('[role=alert]').hidden = true;

  // Prevent the default form submit
  e.preventDefault();

});
Enter fullscreen mode Exit fullscreen mode

form.elements需要将其转换为数组,Array.from以便我们能够遍历它forEach为每个字段设置disable属性。true

现在我们陷入了一个棘手的境地,因为如果fetch操作失败,catch所有表单字段都会被禁用,我们将无法再使用表单。让我们通过在catch处理程序中添加相同的语句来解决这个问题,但这次不是禁用字段,而是启用字段。

.catch(err => {

  // Unlock form elements
  Array.from(form.elements).forEach(field => field.disabled = false);

  // Show error message
  form.querySelector('[role=alert]').hidden = false;

});
Enter fullscreen mode Exit fullscreen mode

信不信由你,我们还没脱离险境。因为我们禁用了所有元素,浏览器将焦点转移到了该<body>元素上。如果操作fetch失败,我们会进入catch事件处理程序,启用表单元素,但用户已经失去了在页面上的位置(这对于使用键盘导航的用户,或者依赖屏幕阅读器的用户来说尤其有用)。

我们可以存储当前获得焦点的元素,然后在处理程序中启用所有字段时document.activeElement恢复焦点。在等待响应期间,我们会将焦点移到表单元素本身。element.focus()catch

document.addEventListener('submit', e => {

  // Store reference to form to make later code easier to read
  const form = e.target;

  // Post data using the Fetch API
  fetch(form.action, {
      method: form.method,
      body: new FormData(form)
    })
    // We turn the response into text as we expect HTML
    .then(res => res.text())

    // Let's turn it into an HTML document
    .then(text => new DOMParser().parseFromString(text, 'text/html'))

    // Now we have a document to work with let's replace the <form>
    .then(doc => {

      // Create result message container and copy HTML from doc
      const result = document.createElement('div');
      result.innerHTML = doc.body.innerHTML;

      // Allow focussing this element with JavaScript
      result.tabIndex = -1;

      // And replace the form with the response children
      form.parentNode.replaceChild(result, form);

      // Move focus to the status message
      result.focus();

    })
    .catch(err => {

      // Unlock form elements
      Array.from(form.elements).forEach(field => field.disabled = false);

      // Return focus to active element
      lastActive.focus();

      // Show error message
      form.querySelector('[role=alert]').hidden = false;

    });

  // Before we disable all the fields, remember the last active field
  const lastActive = document.activeElement;

  // Move focus to form while we wait for a response from the server
  form.tabIndex = -1;
  form.focus();

  // Disable all form elements to prevent further input
  Array.from(form.elements).forEach(field => field.disabled = true);

  // Make sure connection failure message is hidden
  form.querySelector('[role=alert]').hidden = true;

  // Prevent the default form submit
  e.preventDefault();

});
Enter fullscreen mode Exit fullscreen mode

我承认它不是几行 JavaScript 代码,但说实话,里面有很多注释。

显示繁忙状态

最后,最好能显示忙碌状态,以便用户知道正在发生某些事情。

请注意,虽然fetch很高级,但它目前不支持设置超时,也不支持进度事件,因此对于可能需要一段时间的繁忙状态,使用 并不丢人XMLHttpRequest,甚至是一个好主意。

话虽如此,现在是时候给我们的提示信息添加一个类别了(该死的过去的自己!)。我们会给它命名status-failure,并在旁边添加我们的忙碌段落。

<form action="subscribe.php" method="POST">

  <div>
    <label for="name">Name</label>
    <input type="text" name="name" id="name"/>
  </div>

  <div>
    <label for="email">Email</label>
    <input type="email" name="email" id="email" required/>
  </div>

  <p role="alert" class="status-failure" hidden>Connection failure, please try again.</p>

  <p role="alert" class="status-busy" hidden>Busy sending data, please wait.</p>

  <button type="submit">Submit</button>

</form>
Enter fullscreen mode Exit fullscreen mode

表单提交后,我们会显示忙碌状态;catch当数据提交成功后,整个表单会被替换,因此在成功流程中无需再次隐藏它。

当表单处于忙碌状态时,我们不会将焦点移到表单上,而是将其移动到忙碌状态。这会触发屏幕阅读器朗读忙碌状态,以便用户知道表单正在忙碌。

我们在事件处理程序的开头存储了对两个状态消息的引用,这使得后面的代码更容易阅读。

document.addEventListener('submit', e => {

  // Store reference to form to make later code easier to read
  const form = e.target;

  // get status message references
  const statusBusy = form.querySelector('.status-busy');
  const statusFailure = form.querySelector('.status-failure');

  // Post data using the Fetch API
  fetch(form.action, {
      method: form.method,
      body: new FormData(form)
    })
    // We turn the response into text as we expect HTML
    .then(res => res.text())

    // Let's turn it into an HTML document
    .then(text => new DOMParser().parseFromString(text, 'text/html'))

    // Now we have a document to work with let's replace the <form>
    .then(doc => {

      // Create result message container and copy HTML from doc
      const result = document.createElement('div');
      result.innerHTML = doc.body.innerHTML;

      // Allow focussing this element with JavaScript
      result.tabIndex = -1;

      // And replace the form with the response children
      form.parentNode.replaceChild(result, form);

      // Move focus to the status message
      result.focus();

    })
    .catch(err => {

      // Unlock form elements
      Array.from(form.elements).forEach(field => field.disabled = false);

      // Return focus to active element
      lastActive.focus();

      // Hide the busy state
      statusBusy.hidden = false;

      // Show error message
      statusFailure.hidden = false;

    });

  // Before we disable all the fields, remember the last active field
  const lastActive = document.activeElement;

  // Show busy state and move focus to it
  statusBusy.hidden = false;
  statusBusy.tabIndex = -1;
  statusBusy.focus();

  // Disable all form elements to prevent further input
  Array.from(form.elements).forEach(field => field.disabled = true);

  // Make sure connection failure message is hidden
  statusFailure.hidden = true;

  // Prevent the default form submit
  e.preventDefault();

});
Enter fullscreen mode Exit fullscreen mode

就是这样!

我们略过了前端开发的 CSS 部分,您可以使用 CSS 框架,也可以应用自定义样式。示例本身应该能为进一步的自定义提供一个很好的起点。

最后一点,不要移除焦点轮廓

结论

我们为表单编写了语义化的 HTML 结构,并在此基础上使用纯 JavaScript 实现了异步上传体验。我们确保表单对使用键盘的用户以及依赖屏幕阅读器等辅助技术的用户都易于访问。此外,由于我们采用了渐进增强策略,即使 JavaScript 代码出现故障,表单仍然可以正常工作。

希望我们已经介绍了一些您可以使用的新 API 和方法,如果您有任何问题,请告诉我!

文章来源:https://dev.to/rikschennink/async-form-posts-with-a- Couple-lines-of-vanilla-javascript-1ia4