HTML 表单与浏览器约束验证:标签、校验和提交安全
本文目录
表单是浏览器提供的一套交互协议,不只是输入框加按钮。正确的 form、label、name、input type 和提交按钮能带来键盘操作、自动填充、移动端合适键盘、原生约束和辅助技术支持。JavaScript 用来增强流程,不能成为表单可访问性的唯一来源。
先确定提交契约
action 明确接收目标,method="get" 适合无副作用的查询筛选,method="post" 适合创建或提交数据;GET 参数会出现在 URL、浏览历史和访问日志里,不应携带密码、验证码或私密内容。上传文件需要 enctype="multipart/form-data",服务端必须再做大小、类型和权限检查。
所有输入都要有 name 才能进入原生表单数据;每个控件用 <label for> 与 id 关联。placeholder 只提供示例,不替代字段名称或必填说明。
<form action="/account" method="post">
<label for="display-name">显示名称</label>
<input id="display-name" name="displayName"
autocomplete="nickname" minlength="2" maxlength="40" required>
<label for="email">邮箱</label>
<input id="email" name="email" type="email"
autocomplete="email" required>
<label for="password">密码</label>
<input id="password" name="password" type="password"
autocomplete="new-password" minlength="12" required>
<label for="confirmation">确认密码</label>
<input id="confirmation" name="confirmation" type="password"
autocomplete="new-password" minlength="12" required>
<button type="submit">保存资料</button>
</form>
互斥选项用相同 name 分组;一组控件使用 <fieldset>/<legend> 提供整体说明。必填、长度和范围限制要同时给出可见文案,不能只用红色边框或星号表达。
原生约束优先,复杂规则再增强
选择 email、url、number 等语义类型,并使用 required、min、max、step、minlength、maxlength 和 pattern 表达通用约束。先用浏览器原生校验覆盖基本情况,再用 Constraint Validation API 添加跨字段规则或定制错误。不同字段的校验逻辑应集中,避免散落在多个事件处理器。
const password = document.querySelector("#password");
const confirmation = document.querySelector("#confirmation");
function validateConfirmation() {
const mismatch = confirmation.value !== "" && password.value !== confirmation.value;
confirmation.setCustomValidity(mismatch ? "两次输入的密码不一致" : "");
}
password.addEventListener("input", validateConfirmation);
confirmation.addEventListener("input", validateConfirmation);
提交时用 checkValidity() 判断或用 reportValidity() 触发浏览器反馈。form.submit() 会绕过交互式约束校验;一般使用真实 submit 按钮或处理 submit 事件并明确校验流程。自定义错误应在值变化时清除旧消息,并把错误文字关联到字段。
可访问的错误摘要
多字段表单不要只在页首放一句“提交失败”。摘要要列出每个错误并链接到对应控件,字段本身也要带 aria-invalid;用户修正某一项后,及时清除该项状态。下面示例关闭浏览器默认弹泡,自己构建可聚焦的错误摘要;如果不实现完整反馈,保留原生验证通常更稳妥。
<form id="contact-form" action="/contact" method="post" novalidate>
<section id="error-summary" role="alert" tabindex="-1" hidden>
<h2>请修正以下字段</h2>
<ul></ul>
</section>
<label for="contact-email">邮箱</label>
<input id="contact-email" name="email" type="email" required>
<button type="submit">发送</button>
</form>
const form = document.querySelector("#contact-form");
const summary = document.querySelector("#error-summary");
form.addEventListener("submit", event => {
const invalid = Array.from(form.elements).filter(
control => typeof control.checkValidity === "function" && !control.checkValidity()
);
if (invalid.length === 0) return;
event.preventDefault();
const list = summary.querySelector("ul");
list.replaceChildren(...invalid.map(control => {
const item = document.createElement("li");
const link = document.createElement("a");
link.href = `#${control.id}`;
link.textContent = `${control.labels?.[0]?.textContent?.trim() || "字段"}:${control.validationMessage}`;
item.append(link);
control.setAttribute("aria-invalid", "true");
return item;
}));
summary.hidden = false;
summary.focus();
});
form.addEventListener("input", event => {
const control = event.target;
if (typeof control.checkValidity !== "function" || !control.checkValidity()) return;
control.removeAttribute("aria-invalid");
summary.querySelector(`a[href="#${CSS.escape(control.id)}"]`)?.closest("li")?.remove();
if (!summary.querySelector("li")) summary.hidden = true;
});
把错误摘要聚焦后,键盘用户可以从错误链接跳到字段;读屏用户能收到错误状态和消息。不要用颜色作为唯一提示,也不要在输入过程中不断抢焦点。复杂表单还应测试重复提交、服务端错误回填和校验失败时输入是否保留。
错误反馈和键盘体验
失败后保留用户输入;在表单顶部提供错误摘要并链接到问题字段,字段旁说明如何修正,必要时把焦点移到摘要或第一个错误项。异步提交时禁用重复操作、展示提交中状态,结束后用可感知的状态消息告知成功或失败。不要把焦点悄悄丢到页面顶部。
原生错误泡泡适合简单表单;复杂表单可关闭浏览器交互提示后自绘错误,但要自己实现焦点、错误关联、键盘导航和读屏提示。测试时用 Tab、Shift+Tab、Enter 和 Space 从头完成整条提交流程。
前端校验不是安全校验
客户端校验可以被开发者工具、脚本或直接 HTTP 请求绕过。服务端必须再次检查类型、长度、范围、枚举、业务不变量、授权和频率;文件上传还要检查真实文件特征、保存位置、执行权限和恶意内容。CSRF 防护、认证和速率限制也不能由 HTML 属性替代。
文件选择器的 accept 只能提示用户和筛选文件,不能证明上传内容安全。对于需要上传的表单,前端可以预先限制大小并展示文件名,但服务端仍需独立验证字节数、格式特征、扩展名与 MIME 一致性、用户额度和扫描结果;保存时使用随机对象名并放在不可执行的私有存储中。
<form action="/api/attachments" method="post" enctype="multipart/form-data">
<label for="attachment">选择附件</label>
<input id="attachment" name="attachment" type="file"
accept=".pdf,.png,.jpg,.jpeg" required
aria-describedby="attachment-help">
<p id="attachment-help">仅接受 PDF 或图片,单个文件不超过 10 MB。</p>
<button type="submit">上传</button>
</form>
表单验收清单
- 每个控件有可见 label、唯一 id 和稳定 name;必填/格式要求有文字说明。
- 错误在键盘和读屏流程中可发现,修正后旧错误会消失。
- GET 不泄露私密值;POST 与文件上传按后端契约配置。
- 提交中防止重复提交,失败保留输入,成功有确认反馈。
- 客户端与服务端使用一致的业务规则,但服务端校验是最终边界。
官方参考
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。