上一篇介绍了表单的基础结构和控件,这篇深入探讨表单验证和现代提交方式。HTML5 提供了强大的原生验证能力,配合 JavaScript 可以实现优雅的用户体验。
🎯 HTML5 原生验证#
HTML5 引入了多种验证属性,浏览器会自动检查并提示错误。
必填验证 required#
<input type="text" name="username" required />用户提交时如果该字段为空,浏览器会阻止提交并显示提示。
长度限制#
<!-- 最小长度 --><input type="text" name="username" minlength="3" />
<!-- 最大长度 --><input type="text" name="username" maxlength="20" />
<!-- 组合使用 --><input type="password" name="password" minlength="8" maxlength="32" required />数值范围#
<!-- 最小值 --><input type="number" name="age" min="0" />
<!-- 最大值 --><input type="number" name="age" max="120" />
<!-- 步长 --><input type="number" name="quantity" min="0" max="100" step="5" /><!-- 只能输入 0, 5, 10, 15... -->
<!-- 日期范围 --><input type="date" name="birthday" min="1900-01-01" max="2025-12-31" />正则验证 pattern#
<!-- 6位数字验证码 --><input type="text" name="code" pattern="[0-9]{6}" title="请输入6位数字验证码" />
<!-- 中国手机号 --><input type="tel" name="phone" pattern="1[3-9]\d{9}" title="请输入有效的手机号"/>
<!-- 用户名:字母开头,3-16位字母数字下划线 --><input type="text" name="username" pattern="[a-zA-Z][a-zA-Z0-9_]{2,15}" title="字母开头,3-16位字母数字下划线"/>🔶 注意:pattern 使用 JavaScript 正则语法,会自动添加 ^ 和 $ 锚点,无需手动添加。
类型验证#
某些 input 类型自带验证:
<!-- 邮箱格式验证 --><input type="email" name="email" />
<!-- URL 格式验证 --><input type="url" name="website" />验证属性汇总#
| 属性 | 适用类型 | 说明 |
|---|---|---|
required | 所有 | 必填 |
minlength | text, password, textarea | 最小字符数 |
maxlength | text, password, textarea | 最大字符数 |
min | number, range, date, time | 最小值 |
max | number, range, date, time | 最大值 |
step | number, range, date, time | 步长 |
pattern | text, password, tel, search | 正则表达式 |
验证状态伪类#
CSS 可以根据验证状态设置样式:
/* 有效输入 */input:valid { border-color: green;}
/* 无效输入 */input:invalid { border-color: red;}
/* 必填字段 */input:required { border-left: 3px solid orange;}
/* 可选字段 */input:optional { border-left: 3px solid gray;}
/* 在范围内 */input:in-range { background-color: #e8f5e9;}
/* 超出范围 */input:out-of-range { background-color: #ffebee;}
/* 用户交互后才显示验证状态 */input:user-invalid { border-color: red; background-color: #fff0f0;}🔶 推荐:使用 :user-invalid(较新)代替 :invalid,避免页面加载时就显示红色错误状态。
/* 只在用户输入后显示错误 */input:user-invalid { border-color: red;}
/* 显示验证图标 */input:valid::after { content: '✓';}Constraint Validation API#
JavaScript 提供了完整的验证 API,实现更灵活的验证逻辑。
检查有效性#
const input = document.querySelector('#email')const form = document.querySelector('form')
// 检查单个元素input.checkValidity() // 返回 booleaninput.reportValidity() // 返回 boolean 并显示提示
// 检查整个表单form.checkValidity()form.reportValidity()ValidityState 对象#
每个表单元素都有 validity 属性,包含详细的验证状态:
const input = document.querySelector('#username')const validity = input.validity
console.log(validity.valid) // 是否全部通过console.log(validity.valueMissing) // required 未填console.log(validity.typeMismatch) // 类型不匹配(email、url)console.log(validity.patternMismatch) // 正则不匹配console.log(validity.tooShort) // 小于 minlengthconsole.log(validity.tooLong) // 大于 maxlengthconsole.log(validity.rangeUnderflow) // 小于 minconsole.log(validity.rangeOverflow) // 大于 maxconsole.log(validity.stepMismatch) // 不符合 stepconsole.log(validity.badInput) // 无法解析(如 number 输入了字母)console.log(validity.customError) // 自定义错误自定义错误消息#
const password = document.querySelector('#password')const confirmPassword = document.querySelector('#confirm-password')
confirmPassword.addEventListener('input', () => { if (confirmPassword.value !== password.value) { confirmPassword.setCustomValidity('两次密码输入不一致') } else { confirmPassword.setCustomValidity('') // 清除错误 }})完整验证示例#
<form id="registerForm"> <div> <label for="email">邮箱</label> <input type="email" id="email" name="email" required /> <span class="error"></span> </div>
<div> <label for="password">密码</label> <input type="password" id="password" name="password" required minlength="8" pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$" /> <span class="error"></span> </div>
<button type="submit">注册</button></form>
<script> const form = document.querySelector('#registerForm')
// 自定义错误消息 const errorMessages = { email: { valueMissing: '请输入邮箱地址', typeMismatch: '请输入有效的邮箱格式', }, password: { valueMissing: '请输入密码', tooShort: '密码至少需要8个字符', patternMismatch: '密码必须包含大小写字母和数字', }, }
// 显示错误 function showError(input) { const errorSpan = input.parentElement.querySelector('.error') const messages = errorMessages[input.name]
for (const [key, message] of Object.entries(messages)) { if (input.validity[key]) { errorSpan.textContent = message return } } errorSpan.textContent = '' }
// 实时验证 form.querySelectorAll('input').forEach((input) => { input.addEventListener('blur', () => showError(input)) input.addEventListener('input', () => { if (!input.validity.valid) { showError(input) } else { input.parentElement.querySelector('.error').textContent = '' } }) })
// 提交验证 form.addEventListener('submit', (e) => { let isValid = true
form.querySelectorAll('input').forEach((input) => { if (!input.validity.valid) { showError(input) isValid = false } })
if (!isValid) { e.preventDefault() } })</script>禁用原生验证#
有时需要完全自定义验证逻辑:
<!-- 禁用整个表单的原生验证 --><form novalidate>...</form>
<!-- 或通过 JavaScript --><script> form.noValidate = true</script>HTML5 新增 Input 类型#
日期时间选择器#
<!-- 日期 --><input type="date" name="date" value="2025-01-15" />
<!-- 时间 --><input type="time" name="time" value="14:30" />
<!-- 日期+时间 --><input type="datetime-local" name="datetime" value="2025-01-15T14:30" />
<!-- 月份 --><input type="month" name="month" value="2025-01" />
<!-- 周 --><input type="week" name="week" value="2025-W03" />获取值:
const dateInput = document.querySelector('input[type="date"]')console.log(dateInput.value) // "2025-01-15"console.log(dateInput.valueAsDate) // Date 对象console.log(dateInput.valueAsNumber) // 时间戳颜色选择器#
<input type="color" name="color" value="#3b82f6" />const colorInput = document.querySelector('input[type="color"]')colorInput.addEventListener('input', (e) => { document.body.style.backgroundColor = e.target.value})范围滑块#
<input type="range" name="volume" min="0" max="100" value="50" step="10" /><output id="volumeValue">50</output>
<script> const range = document.querySelector('input[type="range"]') const output = document.querySelector('#volumeValue') range.addEventListener('input', () => { output.textContent = range.value })</script>搜索框#
<input type="search" name="q" placeholder="搜索..." />搜索框在某些浏览器会显示清除按钮(×),按 Escape 键可清空。
FormData API#
FormData 是处理表单数据的现代方式,特别适合异步提交。
基础用法#
const form = document.querySelector('form')
// 从表单创建 FormDataconst formData = new FormData(form)
// 获取值formData.get('username') // 单个值formData.getAll('interests') // 多个同名值(如复选框)
// 检查是否存在formData.has('email')
// 添加/修改值formData.set('username', 'newValue') // 覆盖formData.append('tag', 'value') // 追加(允许同名多值)
// 删除formData.delete('password')
// 遍历for (const [key, value] of formData) { console.log(key, value)}异步提交表单#
const form = document.querySelector('#myForm')
form.addEventListener('submit', async (e) => { e.preventDefault()
const formData = new FormData(form)
try { const response = await fetch('/api/submit', { method: 'POST', body: formData, // 自动设置正确的 Content-Type })
if (response.ok) { const result = await response.json() console.log('提交成功:', result) } else { console.error('提交失败') } } catch (error) { console.error('网络错误:', error) }})发送 JSON 数据#
const form = document.querySelector('#myForm')
form.addEventListener('submit', async (e) => { e.preventDefault()
const formData = new FormData(form)
// FormData 转换为普通对象 const data = Object.fromEntries(formData)
// 处理复选框等多值字段 const interests = formData.getAll('interests') data.interests = interests
const response = await fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), })})文件上传#
<form id="uploadForm"> <input type="file" name="avatar" accept="image/*" /> <input type="file" name="documents" multiple /> <button type="submit">上传</button></form>
<script> const form = document.querySelector('#uploadForm')
form.addEventListener('submit', async (e) => { e.preventDefault()
const formData = new FormData(form)
// 可以添加额外数据 formData.append('userId', '12345')
const response = await fetch('/api/upload', { method: 'POST', body: formData, // 不要手动设置 Content-Type,浏览器会自动添加 boundary }) })</script>上传进度#
function uploadWithProgress(formData, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest()
xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percent = (e.loaded / e.total) * 100 onProgress(percent) } })
xhr.addEventListener('load', () => { if (xhr.status === 200) { resolve(JSON.parse(xhr.responseText)) } else { reject(new Error('Upload failed')) } })
xhr.addEventListener('error', () => reject(new Error('Network error')))
xhr.open('POST', '/api/upload') xhr.send(formData) })}
// 使用const formData = new FormData(form)uploadWithProgress(formData, (percent) => { progressBar.style.width = `${percent}%` progressText.textContent = `${Math.round(percent)}%`})🎯 实战:带验证的联系表单#
综合运用本文知识,构建一个完整的联系表单:
<!doctype html><html lang="zh-Hans"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>联系我们</title> <style> * { box-sizing: border-box; } body { font-family: system-ui, sans-serif; padding: 2rem; } form { max-width: 500px; margin: 0 auto; } .form-group { margin-bottom: 1.5rem; } label { display: block; margin-bottom: 0.5rem; font-weight: 600; } input, textarea, select { width: 100%; padding: 0.75rem; border: 2px solid #e2e8f0; border-radius: 0.5rem; font-size: 1rem; transition: border-color 0.2s; } input:focus, textarea:focus, select:focus { outline: none; border-color: #3b82f6; } input:user-invalid, textarea:user-invalid { border-color: #ef4444; } input:user-valid, textarea:user-valid { border-color: #22c55e; } .error { color: #ef4444; font-size: 0.875rem; margin-top: 0.25rem; min-height: 1.25rem; } button { width: 100%; padding: 1rem; background: #3b82f6; color: white; border: none; border-radius: 0.5rem; font-size: 1rem; cursor: pointer; transition: background 0.2s; } button:hover { background: #2563eb; } button:disabled { background: #94a3b8; cursor: not-allowed; } .success { background: #dcfce7; color: #166534; padding: 1rem; border-radius: 0.5rem; text-align: center; } </style> </head> <body> <form id="contactForm" novalidate> <h1>联系我们</h1>
<div class="form-group"> <label for="name">姓名 *</label> <input type="text" id="name" name="name" required minlength="2" maxlength="50" placeholder="请输入您的姓名" /> <div class="error" data-for="name"></div> </div>
<div class="form-group"> <label for="email">邮箱 *</label> <input type="email" id="email" name="email" required placeholder="example@mail.com" /> <div class="error" data-for="email"></div> </div>
<div class="form-group"> <label for="phone">手机号</label> <input type="tel" id="phone" name="phone" pattern="1[3-9]\d{9}" placeholder="可选,如 13800138000" /> <div class="error" data-for="phone"></div> </div>
<div class="form-group"> <label for="subject">主题 *</label> <select id="subject" name="subject" required> <option value="">请选择主题</option> <option value="general">一般咨询</option> <option value="support">技术支持</option> <option value="feedback">意见反馈</option> <option value="business">商务合作</option> </select> <div class="error" data-for="subject"></div> </div>
<div class="form-group"> <label for="message">留言内容 *</label> <textarea id="message" name="message" rows="5" required minlength="10" maxlength="1000" placeholder="请详细描述您的问题或建议..." ></textarea> <div class="error" data-for="message"></div> </div>
<button type="submit">发送消息</button> </form>
<script> const form = document.querySelector('#contactForm')
// 错误消息配置 const messages = { name: { valueMissing: '请输入姓名', tooShort: '姓名至少需要2个字符', tooLong: '姓名不能超过50个字符', }, email: { valueMissing: '请输入邮箱地址', typeMismatch: '请输入有效的邮箱格式', }, phone: { patternMismatch: '请输入有效的手机号(如13800138000)', }, subject: { valueMissing: '请选择一个主题', }, message: { valueMissing: '请输入留言内容', tooShort: '留言内容至少需要10个字符', tooLong: '留言内容不能超过1000个字符', }, }
// 获取错误消息 function getErrorMessage(input) { const fieldMessages = messages[input.name] if (!fieldMessages) return input.validationMessage
for (const [key, message] of Object.entries(fieldMessages)) { if (input.validity[key]) return message } return '' }
// 显示/隐藏错误 function updateError(input) { const errorEl = document.querySelector(`[data-for="${input.name}"]`) if (!errorEl) return
if (input.validity.valid) { errorEl.textContent = '' } else { errorEl.textContent = getErrorMessage(input) } }
// 绑定事件 form.querySelectorAll('input, textarea, select').forEach((input) => { input.addEventListener('blur', () => updateError(input)) input.addEventListener('input', () => updateError(input)) })
// 提交处理 form.addEventListener('submit', async (e) => { e.preventDefault()
// 验证所有字段 let isValid = true form.querySelectorAll('input, textarea, select').forEach((input) => { updateError(input) if (!input.validity.valid) isValid = false })
if (!isValid) return
// 提交数据 const button = form.querySelector('button') button.disabled = true button.textContent = '发送中...'
try { const formData = new FormData(form) const data = Object.fromEntries(formData)
// 模拟 API 请求 await new Promise((resolve) => setTimeout(resolve, 1500)) console.log('提交数据:', data)
// 成功提示 form.innerHTML = ` <div class="success"> <h2>✓ 发送成功</h2> <p>感谢您的留言,我们会尽快回复!</p> </div> ` } catch (error) { button.disabled = false button.textContent = '发送消息' alert('发送失败,请稍后重试') } }) </script> </body></html>常见问题#
🤔 原生验证和 JavaScript 验证怎么选?#
- 简单场景:优先使用原生验证,零 JS 代码即可工作
- 复杂场景:用
novalidate禁用原生验证,完全用 JS 控制 - 推荐方案:结合使用,原生验证作为第一道防线,JS 提供更好的 UX
🤔 FormData 可以处理嵌套对象吗?#
FormData 本身不支持嵌套结构。如需发送嵌套 JSON:
const formData = new FormData(form)const data = { user: { name: formData.get('name'), email: formData.get('email'), }, message: formData.get('message'),}
fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data),})🤔 如何实现实时验证而不是提交时验证?#
使用 input 和 blur 事件组合:
// blur:离开输入框时验证(首次验证)input.addEventListener('blur', validate)
// input:输入时验证(已有错误时实时反馈)input.addEventListener('input', () => { if (hasError) validate()})