博客
AJAX 表单提交:无需刷新页面即可发送表单
· 1 分钟阅读
以最普通的方式提交 HTML 表单,浏览器会做它一直在做的事:跳转到新页面,或者刷新当前页面,把片刻之前屏幕上的一切都丢弃。对于独占一整页的表单来说,这没什么问题。但对于位于弹窗、聊天组件、结账流程,或任何单页应用里的表单来说,整页跳转是一次突兀的重置--弹窗关闭了,应用重新挂载了,滚动位置也没了。AJAX 提交--用 JavaScript 的 fetch() 发送表单数据,而不是让浏览器跳转--解决了这个问题,而且只需要几行代码。
为什么要避免整页刷新
整页刷新会丢弃 JavaScript 保存在内存里的一切:打开的弹窗、向导当前所在的步骤、页面其他地方尚未保存的状态。如果表单位于一棵 React、Vue 或 Svelte 组件树中,让浏览器跳转还会拆毁整个客户端应用,使其不得不从头重新加载。
通过 fetch() 提交则会让页面--以及页面上运行的应用--保持完整。接下来发生什么由你决定:内联显示成功消息、关闭弹窗、重置字段,或者按需以编程方式跳转。这就是"浏览器决定"和"你决定"之间的区别。
这是一种增强,而不是必需品
你不需要在两种方式之间二选一。让 <form> 标签上的 action 和 method 属性保持与普通 POST 完全一致,只在 JavaScript 可用时才用它拦截 submit 事件。如果某个脚本加载失败、某个扩展干扰了页面,或者访问者干脆禁用了 JavaScript,表单依然能正常工作--只是会退化为普通的页面跳转,而不是内联的状态消息。这正是渐进增强的核心思想:基础情形--一个直接提交到 https://formhook.app/f/{apiKey} 的 HTML 表单--始终能独立正常工作。JavaScript 只是在它存在时让体验更好。
一个最简示例
不需要任何框架--这就是原生 JavaScript 里的完整模式。它阻止默认的页面跳转,把表单数据以 FormData 的形式发送出去,并根据结果切换显示的状态消息:
const form = document.querySelector("#contact-form");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const res = await fetch(form.action, {
method: "POST",
body: new FormData(form),
headers: { Accept: "application/json" },
});
if (res.ok) {
form.reset();
document.querySelector("#form-status").textContent = "Thanks - we got it.";
} else {
document.querySelector("#form-status").textContent = "Something went wrong. Please try again.";
}
});请求 JSON,而不是重定向
注意上面示例中的 Accept: application/json 请求头。如果没有它,一个同时也要处理普通 HTML 提交的表单后端,可能会尝试把浏览器重定向到一个"感谢"页面--这对普通 POST 请求来说是正确行为,但在你用 fetch() 调用它时毫无用处。发送 Accept: application/json 会告诉 Formhook 的端点这个请求来自 JavaScript,于是它会返回一个 JSON 格式的成功或错误响应,而不是重定向,你的代码就能像示例那样根据 res.ok 来分支处理。具体的请求格式请参阅文档。
别忘了无障碍访问
用 textContent 替换状态消息,对屏幕阅读器来说是不可见的,除非你告诉浏览器要朗读它。给状态元素(也就是示例中 document.querySelector("#form-status") 所指向的那个元素)加上 aria-live="polite",辅助技术就会在它一变化时立刻朗读出更新内容--无论成功还是出错--访问者不需要自己去找到它并重新聚焦。这只是一个属性,却决定了一个表单是对所有人都可用,还是在 JavaScript 接管的那一刻悄悄地把屏幕阅读器用户排除在外。
这一切都不需要任何库或 SDK--Formhook 的端点可以接受来自任何站点的普通表单 POST 请求,上面这个 fetch() 版本只是在此之上可选的渐进增强,而不是前提条件。完整的请求/响应参考请见文档,免费层的限制请见价格页面。
一行代码上线可用的表单
欧盟托管,提交记录永久保存,所有套餐均支持推送通知。
免费开始无需信用卡 · 阅读文档