Skip to content

HTML 表单进阶:验证与现代特性

上一篇介绍了表单的基础结构和控件,这篇深入探讨表单验证和现代提交方式。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所有必填
minlengthtext, password, textarea最小字符数
maxlengthtext, password, textarea最大字符数
minnumber, range, date, time最小值
maxnumber, range, date, time最大值
stepnumber, range, date, time步长
patterntext, 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() // 返回 boolean
input.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) // 小于 minlength
console.log(validity.tooLong) // 大于 maxlength
console.log(validity.rangeUnderflow) // 小于 min
console.log(validity.rangeOverflow) // 大于 max
console.log(validity.stepMismatch) // 不符合 step
console.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')
// 从表单创建 FormData
const 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 验证怎么选?#

🤔 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),
})

🤔 如何实现实时验证而不是提交时验证?#

使用 inputblur 事件组合:

// blur:离开输入框时验证(首次验证)
input.addEventListener('blur', validate)
// input:输入时验证(已有错误时实时反馈)
input.addEventListener('input', () => {
if (hasError) validate()
})

参考资料#