Constraint Validation API
Constraint Validation API 是 HTML5 提供的表单验证编程接口,它允许开发者通过 JavaScript 精细地检查和控制表单验证状态。通过 validity 对象、checkValidity()、reportValidity()、willValidate 等成员,可以获取每个控件的具体验证状态、手动触发验证,或构建自定义的验证流程。
前置知识
阅读本节前,建议先了解:自定义验证消息
基础概念
什么是 Constraint Validation API
Constraint Validation API 是浏览器内置的表单验证 JavaScript 接口,提供以下核心能力:
- 读取验证状态:获取控件是否有效、具体哪种验证失败
- 手动触发验证:不依赖表单提交即可检查验证
- 自定义消息:设置自定义的验证提示文本
- 完全控制验证流程:取代或增强浏览器默认验证行为
核心 API 成员
| API 成员 | 所属对象 | 说明 |
|---|---|---|
validity | HTMLInputElement 等 | 验证状态对象 |
checkValidity() | 控件 / 表单 | 检查验证,返回布尔值 |
reportValidity() | 控件 / 表单 | 检查验证,并显示提示 |
willValidate | HTMLInputElement 等 | 是否参与验证 |
setCustomValidity() | 控件 | 设置自定义验证消息 |
validationMessage | 控件 | 当前验证消息文本 |
语法
validity 对象
validity 对象包含多个布尔属性,每个属性对应一种验证规则:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>validity 对象</title>
</head>
<body>
<h2>validity 属性查看器</h2>
<form id="check-form">
<div>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" required minlength="5">
</div>
<button type="button" id="check-btn">检查验证状态</button>
</form>
<table id="validity-table" border="1" cellpadding="8" style="margin-top: 16px; border-collapse: collapse;">
<tr><th>属性</th><th>值</th><th>说明</th></tr>
</table>
<script>
document.getElementById('check-btn').addEventListener('click', function () {
const input = document.getElementById('email');
const v = input.validity;
const table = document.getElementById('validity-table');
// 清除旧行(保留表头)
while (table.rows.length > 1) table.deleteRow(1);
const properties = {
valid: '所有验证通过',
valueMissing: 'required 但为空',
typeMismatch: '类型不匹配(如 email 格式错误)',
patternMismatch: '不匹配 pattern 正则',
tooLong: '超过 maxlength',
tooShort: '少于 minlength',
rangeUnderflow: '小于 min',
rangeOverflow: '大于 max',
stepMismatch: '不满足 step 步进',
badInput: '输入无法转为数值(number 类型)',
customError: '有 setCustomValidity 设置的错误'
};
for (const [key, desc] of Object.entries(properties)) {
const row = table.insertRow();
row.insertCell().textContent = key;
row.insertCell().textContent = v[key];
row.insertCell().textContent = desc;
if (v[key]) {
row.style.background = '#ffe0e0';
}
}
});
</script>
</body>
</html>willValidate 属性
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>willValidate</title>
</head>
<body>
<h2>willValidate 检查</h2>
<form>
<input type="text" id="normal" name="normal" required>
<input type="text" id="no-validate" name="no-validate">
<input type="text" id="disabled-field" name="disabled" disabled required>
<input type="hidden" id="hidden-field" name="hidden" required>
</form>
<table border="1" cellpadding="8" style="margin-top: 12px;">
<tr><th>控件</th><th>required</th><th>willValidate</th><th>说明</th></tr>
<tr><td>normal</td><td>yes</td><td>true</td><td>参与验证</td></tr>
<tr><td>no-validate</td><td>no</td><td>true</td><td>参与验证(无规则)</td></tr>
<tr><td>disabled</td><td>yes</td><td>false</td><td>禁用不验证</td></tr>
<tr><td>hidden</td><td>yes</td><td>false</td><td>隐藏不验证</td></tr>
</table>
<script>
['normal', 'no-validate', 'disabled-field', 'hidden-field'].forEach(id => {
const el = document.getElementById(id);
console.log(`${id}: willValidate = ${el.willValidate}`);
});
</script>
</body>
</html>详细说明
checkValidity() vs reportValidity()
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>checkValidity vs reportValidity</title>
</head>
<body>
<h2>验证方法对比</h2>
<form id="demo-form">
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" required>
<br><br>
<button type="button" onclick="useCheck()">checkValidity()</button>
<button type="button" onclick="useReport()">reportValidity()</button>
</form>
<p id="output" style="margin-top: 16px;"></p>
<script>
function useCheck() {
const input = document.getElementById('email');
const valid = input.checkValidity();
document.getElementById('output').innerHTML =
`checkValidity(): <strong>${valid}</strong><br>` +
`<small>返回布尔值,不显示浏览器提示</small>`;
}
function useReport() {
const input = document.getElementById('email');
const valid = input.reportValidity();
document.getElementById('output').innerHTML =
`reportValidity(): <strong>${valid}</strong><br>` +
`<small>返回布尔值,<em>显示</em>浏览器提示</small>`;
}
</script>
</body>
</html>| 方法 | 触发 invalid 事件 | 显示浏览器提示 | 返回值 |
|---|---|---|---|
checkValidity() | ✅ | ❌ | true/false |
reportValidity() | ✅ | ✅ | true/false |
实时验证(input 事件)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>实时验证</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 40px auto; }
.form-group { margin-bottom: 16px; }
label { display: block; font-weight: 600; margin-bottom: 4px; }
input {
width: 100%;
padding: 8px 12px;
border: 2px solid #ddd;
border-radius: 6px;
box-sizing: border-box;
}
input.valid { border-color: #27ae60; }
input.invalid { border-color: #e74c3c; }
.error-msg { color: #e74c3c; font-size: 14px; margin-top: 4px; }
</style>
</head>
<body>
<h2>实时验证</h2>
<form id="form" novalidate>
<div class="form-group">
<label for="username">用户名(3-20 字符):</label>
<input type="text" id="username" name="username" required minlength="3" maxlength="20">
<p class="error-msg" id="username-error"></p>
</div>
<div class="form-group">
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" required>
<p class="error-msg" id="email-error"></p>
</div>
<div class="form-group">
<label for="age">年龄(1-120):</label>
<input type="number" id="age" name="age" min="1" max="120">
<p class="error-msg" id="age-error"></p>
</div>
<button type="submit">提交</button>
</form>
<script>
function validateField(input) {
const errorEl = document.getElementById(input.id + '-error');
const value = input.value.trim();
// 空值时清除错误样式(不验证空字段)
if (value === '') {
input.classList.remove('valid', 'invalid');
errorEl.textContent = '';
return;
}
if (input.checkValidity()) {
input.classList.add('valid');
input.classList.remove('invalid');
errorEl.textContent = '';
} else {
input.classList.add('invalid');
input.classList.remove('valid');
const v = input.validity;
if (v.valueMissing) errorEl.textContent = '此字段不能为空';
else if (v.typeMismatch) errorEl.textContent = '格式不正确';
else if (v.patternMismatch) errorEl.textContent = '格式不符合要求';
else if (v.tooShort) errorEl.textContent = `至少需要 ${input.minLength} 个字符`;
else if (v.tooLong) errorEl.textContent = `不能超过 ${input.maxLength} 个字符`;
else if (v.rangeUnderflow) errorEl.textContent = `不能小于 ${input.min}`;
else if (v.rangeOverflow) errorEl.textContent = `不能大于 ${input.max}`;
else if (v.stepMismatch) errorEl.textContent = '步进值不正确';
else errorEl.textContent = input.validationMessage || '输入无效';
}
}
// 为所有输入框绑定实时验证
document.querySelectorAll('#form input').forEach(input => {
input.addEventListener('input', () => validateField(input));
input.addEventListener('blur', () => validateField(input));
});
// 表单提交验证
document.getElementById('form').addEventListener('submit', function (e) {
e.preventDefault();
let allValid = true;
this.querySelectorAll('input').forEach(input => {
validateField(input);
if (!input.checkValidity()) allValid = false;
});
if (allValid) {
alert('所有字段验证通过!');
}
});
</script>
</body>
</html>表单级 checkValidity
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>表单级验证</title>
</head>
<body>
<h2>表单验证方法</h2>
<form id="form">
<input type="text" name="name" required>
<input type="email" name="email" required>
<button type="button" id="check-form-btn">form.checkValidity()</button>
<button type="button" id="report-form-btn">form.reportValidity()</button>
</form>
<script>
const form = document.getElementById('form');
document.getElementById('check-form-btn').addEventListener('click', () => {
// 检查所有控件,返回 true/false
const valid = form.checkValidity();
console.log('表单验证通过:', valid);
});
document.getElementById('report-form-btn').addEventListener('click', () => {
// 检查所有控件并显示浏览器提示
const valid = form.reportValidity();
console.log('表单验证通过:', valid);
});
// checkValidity 和 reportValidity 都会触发 invalid 事件
form.addEventListener('submit', function (e) {
e.preventDefault();
if (this.reportValidity()) {
console.log('提交数据:', new FormData(this));
}
});
</script>
</body>
</html>实战示例
完整的验证框架
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>验证框架</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 40px auto; }
.form-group { margin-bottom: 16px; }
label { display: block; font-weight: 600; margin-bottom: 4px; }
input, select {
width: 100%;
padding: 8px 12px;
border: 2px solid #ddd;
border-radius: 6px;
box-sizing: border-box;
font-size: 16px;
}
input:focus, select:focus { outline: none; box-shadow: 0 0 0 3px rgba(74,144,217,0.2); }
input.valid { border-color: #27ae60; }
input.invalid { border-color: #e74c3c; }
.error { color: #e74c3c; font-size: 14px; margin-top: 4px; }
.success-msg { padding: 12px; background: #d4edda; color: #155724; border-radius: 6px; margin-top: 16px; display: none; }
button {
padding: 10px 24px;
background: #4a90d9;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
}
</style>
</head>
<body>
<h2>Constraint Validation API 验证框架</h2>
<form id="form" novalidate>
<div class="form-group">
<label for="name">姓名 <span style="color: red;">*</span></label>
<input type="text" id="name" name="name" required minlength="2" maxlength="20">
<p class="error" id="name-error"></p>
</div>
<div class="form-group">
<label for="email">邮箱 <span style="color: red;">*</span></label>
<input type="email" id="email" name="email" required>
<p class="error" id="email-error"></p>
</div>
<div class="form-group">
<label for="phone">手机号 <span style="color: red;">*</span></label>
<input type="tel" id="phone" name="phone" required pattern="1[3-9]\d{9}">
<p class="error" id="phone-error"></p>
</div>
<div class="form-group">
<label for="city">城市 <span style="color: red;">*</span></label>
<select id="city" name="city" required>
<option value="">请选择</option>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
</select>
<p class="error" id="city-error"></p>
</div>
<button type="submit">提交</button>
</form>
<div class="success-msg" id="success">验证通过,提交成功!</div>
<script>
// 验证消息映射
const messages = {
valueMissing: '此字段不能为空',
typeMismatch: '格式不正确',
patternMismatch: '格式不符合要求',
tooShort: '内容过短',
tooLong: '内容过长',
rangeUnderflow: '值过小',
rangeOverflow: '值过大',
stepMismatch: '步进值不正确',
badInput: '输入无效'
};
// 验证单个字段
function validateField(input) {
const errorEl = document.getElementById(input.id + '-error');
if (!errorEl) return true;
const value = input.value.trim();
// 空值且非必填时不验证
if (value === '' && !input.required) {
input.classList.remove('valid', 'invalid');
errorEl.textContent = '';
return true;
}
const isValid = input.checkValidity();
if (isValid) {
input.classList.add('valid');
input.classList.remove('invalid');
errorEl.textContent = '';
} else {
input.classList.add('invalid');
input.classList.remove('valid');
// 从 validity 对象中查找失败原因
let msg = input.validationMessage;
for (const [key, text] of Object.entries(messages)) {
if (input.validity[key]) {
msg = text;
break;
}
}
errorEl.textContent = msg;
}
return isValid;
}
// 绑定实时验证
document.querySelectorAll('#form input, #form select').forEach(input => {
input.addEventListener('input', () => validateField(input));
input.addEventListener('blur', () => validateField(input));
input.addEventListener('change', () => validateField(input));
});
// 表单提交
document.getElementById('form').addEventListener('submit', function (e) {
e.preventDefault();
let allValid = true;
this.querySelectorAll('input, select').forEach(input => {
if (!validateField(input)) allValid = false;
});
if (allValid) {
document.getElementById('success').style.display = 'block';
console.log('表单数据:', Object.fromEntries(new FormData(this)));
} else {
// 聚焦到第一个无效字段
const firstInvalid = this.querySelector('input.invalid, select.invalid');
if (firstInvalid) firstInvalid.focus();
}
});
</script>
</body>
</html>注意事项
validity.valid 的优先级
validity.valid 为 false 时,至少有一个具体的验证属性为 true。检查时应该先查看具体的验证失败原因,再处理。
badInput 的特殊场景
badInput 在 type="number" 中输入非数字字符时为 true,但输入值为空字符串时不算 badInput。
checkValidity 会触发 invalid 事件
调用 checkValidity() 或 reportValidity() 时,如果验证失败,会触发对应控件的 invalid 事件。
novalidate 与 API
即使表单设置了 novalidate,Constraint Validation API 仍然可以正常使用。novalidate 只是阻止表单提交时的自动验证。
最佳实践
- 优先使用 API 而非正则:对于内置支持的验证规则(email、url、number),优先使用原生类型而非
pattern - 实时验证在 blur 后:首次验证建议在失焦时触发,后续用
input事件实时验证 - error 聚焦:验证失败时自动聚焦到第一个错误字段
- 统一的验证框架:封装通用的验证逻辑,减少重复代码
- validity 对象逐条检查:查找具体的失败原因而非只看
valid属性 - 服务端验证并行:客户端验证提升体验,服务端验证保障安全
下一节
继续学习:语义化结构概述(阶段七)