pin_drop当前位置:知识文库 ❯ 图文
JS集成:HTML 表单验证 JS 详解 - 详细教程与实战指南
教程简介
JavaScript 表单验证是前端开发中最常见的任务之一,它确保用户输入的数据符合预期格式和业务规则。HTML5 引入了 Constraint Validation API,提供了声明式的表单验证能力,而 JavaScript 则在此基础上实现了更灵活的自定义验证、实时验证和异步验证。完善的表单验证不仅能防止无效数据提交到服务器,还能通过友好的 UI 反馈提升用户体验。
本教程将深入讲解 JavaScript 表单验证、Constraint Validation API、validity 对象、setCustomValidity、checkValidity、reportValidity、实时验证、异步验证、自定义验证规则以及验证 UI 反馈。
核心概念
表单验证层次
Constraint Validation API
Constraint Validation API 是 HTML5 提供的表单验证接口,包含以下核心部分:
ValidityState 对象属性
HTML5 验证属性
语法与用法
checkValidity()
代码示例
const input = document.getElementById('email');
if (input.checkValidity()) {
// 验证通过
} else {
// 验证失败
console.log(input.validationMessage);
}reportValidity()
代码示例
// 检查验证并显示浏览器默认的验证提示
if (!input.reportValidity()) {
// 验证失败,浏览器已显示提示
}setCustomValidity()
代码示例
const input = document.getElementById('password');
input.addEventListener('input', function() {
if (this.value.length < 8) {
this.setCustomValidity('密码至少需要8个字符');
} else if (!/[A-Z]/.test(this.value)) {
this.setCustomValidity('密码需要包含大写字母');
} else {
this.setCustomValidity(''); // 清除自定义错误
}
});validity 对象
代码示例
input.addEventListener('invalid', function() {
if (this.validity.valueMissing) {
console.log('此字段为必填');
} else if (this.validity.typeMismatch) {
console.log('请输入有效的邮箱地址');
} else if (this.validity.patternMismatch) {
console.log('格式不正确');
} else if (this.validity.customError) {
console.log(this.validationMessage);
}
});阻止默认验证
代码示例
<!-- 禁用整个表单的 HTML5 验证 -->
<form novalidate>
<!-- 禁用单个元素的验证 -->
<input formnovalidate>
</form>代码示例
示例1:Constraint Validation API 基础
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Constraint Validation API 基础</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
background: #f5f5f5;
}
.container { max-width: 700px; margin: 0 auto; }
.form-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
padding: 30px;
}
.form-card h3 { margin-top: 0; }
.form-group {
margin-bottom: 18px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: bold;
font-size: 14px;
}
.form-group input {
width: 100%;
padding: 10px 12px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
transition: border-color 0.2s;
}
.form-group input:focus {
outline: none;
border-color: #2196F3;
}
.form-group input:valid:not(:placeholder-shown) {
border-color: #4CAF50;
}
.form-group input:invalid:not(:placeholder-shown):not(:focus) {
border-color: #f44336;
}
.validity-info {
margin-top: 8px;
padding: 10px;
background: #f5f5f5;
border-radius: 6px;
font-size: 12px;
font-family: monospace;
}
.validity-info .property {
display: inline-block;
padding: 2px 6px;
margin: 2px;
border-radius: 3px;
font-size: 11px;
}
.property.true { background: #ffebee; color: #c62828; }
.property.false { background: #e8f5e9; color: #2e7d32; }
.submit-btn {
padding: 12px 30px;
background: #2196F3;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
}
.validation-result {
margin-top: 15px;
padding: 12px;
border-radius: 8px;
font-size: 14px;
display: none;
}
.validation-result.success {
display: block;
background: #e8f5e9;
color: #2e7d32;
}
.validation-result.error {
display: block;
background: #ffebee;
color: #c62828;
}
</style>
</head>
<body>
<div class="container">
<h1>Constraint Validation API 基础</h1>
<div class="form-card">
<h3>注册表单</h3>
<form id="regForm" novalidate>
<div class="form-group">
<label>用户名 (必填, 3-20字符)</label>
<input type="text" id="username" required minlength="3" maxlength="20" placeholder="请输入用户名">
<div class="validity-info" id="usernameValidity"></div>
</div>
<div class="form-group">
<label>邮箱 (必填)</label>
<input type="email" id="email" required placeholder="请输入邮箱">
<div class="validity-info" id="emailValidity"></div>
</div>
<div class="form-group">
<label>年龄 (18-120)</label>
<input type="number" id="age" required min="18" max="120" placeholder="请输入年龄">
<div class="validity-info" id="ageValidity"></div>
</div>
<div class="form-group">
<label>手机号 (11位数字)</label>
<input type="tel" id="phone" required pattern="^1[3-9]\d{9}$" placeholder="请输入手机号">
<div class="validity-info" id="phoneValidity"></div>
</div>
<button type="submit" class="submit-btn">提交</button>
</form>
<div class="validation-result" id="result"></div>
</div>
</div>
<script>
const form = document.getElementById('regForm');
const fields = [
{ id: 'username', validityId: 'usernameValidity' },
{ id: 'email', validityId: 'emailValidity' },
{ id: 'age', validityId: 'ageValidity' },
{ id: 'phone', validityId: 'phoneValidity' },
];
// 实时显示 validity 状态
fields.forEach(({ id, validityId }) => {
const input = document.getElementById(id);
const validityEl = document.getElementById(validityId);
input.addEventListener('input', function() {
updateValidityDisplay(this, validityEl);
});
input.addEventListener('focus', function() {
updateValidityDisplay(this, validityEl);
});
});
function updateValidityDisplay(input, el) {
const v = input.validity;
const properties = [
'valueMissing', 'typeMismatch', 'patternMismatch',
'tooLong', 'tooShort', 'rangeUnderflow', 'rangeOverflow',
'stepMismatch', 'badInput', 'customError', 'valid'
];
let html = properties.map(prop => {
const value = v[prop];
return `<span class="property ${value}">${prop}: ${value}</span>`;
}).join('');
html += `<br><strong>validationMessage:</strong> ${input.validationMessage || '(无)'}`;
el.innerHTML = html;
}
// 表单提交
form.addEventListener('submit', function(e) {
e.preventDefault();
const result = document.getElementById('result');
if (form.checkValidity()) {
result.className = 'validation-result success';
result.textContent = '表单验证通过!所有字段均有效。';
} else {
// 显示第一个无效字段的验证消息
form.reportValidity();
result.className = 'validation-result error';
result.textContent = '表单验证失败,请检查标红的字段。';
}
});
</script>
</body>
</html>示例2:setCustomValidity 与自定义验证
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>setCustomValidity 与自定义验证</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
background: #f5f5f5;
}
.container { max-width: 600px; margin: 0 auto; }
.form-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
padding: 30px;
}
.form-card h3 { margin-top: 0; }
.form-group {
margin-bottom: 18px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: bold;
font-size: 14px;
}
.form-group input {
width: 100%;
padding: 10px 12px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
transition: all 0.2s;
}
.form-group input:focus {
outline: none;
border-color: #2196F3;
}
.form-group input.valid {
border-color: #4CAF50;
background: #f1f8e9;
}
.form-group input.invalid {
border-color: #f44336;
background: #fff8f8;
}
.error-msg {
font-size: 13px;
color: #f44336;
margin-top: 4px;
min-height: 20px;
}
.strength-bar {
display: flex;
gap: 4px;
margin-top: 8px;
}
.strength-segment {
flex: 1;
height: 6px;
border-radius: 3px;
background: #e0e0e0;
transition: background 0.3s;
}
.strength-weak .strength-segment:nth-child(1) { background: #f44336; }
.strength-medium .strength-segment:nth-child(1),
.strength-medium .strength-segment:nth-child(2) { background: #FF9800; }
.strength-strong .strength-segment:nth-child(1),
.strength-strong .strength-segment:nth-child(2),
.strength-strong .strength-segment:nth-child(3) { background: #4CAF50; }
.strength-very-strong .strength-segment { background: #2196F3; }
.strength-text {
font-size: 12px;
margin-top: 4px;
color: #888;
}
.submit-btn {
padding: 12px 30px;
background: #2196F3;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
width: 100%;
}
.submit-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="container">
<h1>setCustomValidity 与自定义验证</h1>
<div class="form-card">
<h3>密码设置</h3>
<form id="passwordForm" novalidate>
<div class="form-group">
<label>密码</label>
<input type="password" id="password" required placeholder="请输入密码">
<div class="strength-bar" id="strengthBar">
<div class="strength-segment"></div>
<div class="strength-segment"></div>
<div class="strength-segment"></div>
<div class="strength-segment"></div>
</div>
<div class="strength-text" id="strengthText"></div>
<div class="error-msg" id="passwordError"></div>
</div>
<div class="form-group">
<label>确认密码</label>
<input type="password" id="confirmPassword" required placeholder="请再次输入密码">
<div class="error-msg" id="confirmError"></div>
</div>
<button type="submit" class="submit-btn" id="submitBtn" disabled>设置密码</button>
</form>
</div>
</div>
<script>
const password = document.getElementById('password');
const confirmPassword = document.getElementById('confirmPassword');
const strengthBar = document.getElementById('strengthBar');
const strengthText = document.getElementById('strengthText');
const passwordError = document.getElementById('passwordError');
const confirmError = document.getElementById('confirmError');
const submitBtn = document.getElementById('submitBtn');
// 密码强度验证
password.addEventListener('input', function() {
const value = this.value;
const strength = getPasswordStrength(value);
// 更新强度指示器
strengthBar.className = 'strength-bar ' + strength.className;
strengthText.textContent = value ? strength.label : '';
// 自定义验证规则
let errorMsg = '';
if (value.length > 0 && value.length < 8) {
errorMsg = '密码至少需要8个字符';
} else if (value.length >= 8 && !/[A-Z]/.test(value)) {
errorMsg = '密码需要包含至少一个大写字母';
} else if (value.length >= 8 && !/[a-z]/.test(value)) {
errorMsg = '密码需要包含至少一个小写字母';
} else if (value.length >= 8 && !/[0-9]/.test(value)) {
errorMsg = '密码需要包含至少一个数字';
} else if (value.length >= 8 && !/[^A-Za-z0-9]/.test(value)) {
errorMsg = '密码需要包含至少一个特殊字符';
}
// 使用 setCustomValidity 设置自定义错误
this.setCustomValidity(errorMsg);
passwordError.textContent = errorMsg;
// 更新样式
this.classList.toggle('valid', value.length > 0 && !errorMsg);
this.classList.toggle('invalid', value.length > 0 && !!errorMsg);
// 验证确认密码
validateConfirm();
updateSubmitState();
});
// 确认密码验证
confirmPassword.addEventListener('input', validateConfirm);
function validateConfirm() {
const value = confirmPassword.value;
let errorMsg = '';
if (value && value !== password.value) {
errorMsg = '两次输入的密码不一致';
}
confirmPassword.setCustomValidity(errorMsg);
confirmError.textContent = errorMsg;
confirmPassword.classList.toggle('valid', value && !errorMsg);
confirmPassword.classList.toggle('invalid', value && !!errorMsg);
updateSubmitState();
}
function updateSubmitState() {
submitBtn.disabled = !password.value || !confirmPassword.value ||
!!passwordError.textContent || !!confirmError.textContent;
}
function getPasswordStrength(pwd) {
if (!pwd) return { className: '', label: '' };
let score = 0;
if (pwd.length >= 8) score++;
if (/[A-Z]/.test(pwd) && /[a-z]/.test(pwd)) score++;
if (/[0-9]/.test(pwd)) score++;
if (/[^A-Za-z0-9]/.test(pwd)) score++;
const levels = [
{ className: 'strength-weak', label: '弱' },
{ className: 'strength-medium', label: '中等' },
{ className: 'strength-strong', label: '强' },
{ className: 'strength-very-strong', label: '非常强' },
];
return levels[Math.min(score, 4) - 1] || levels[0];
}
// 表单提交
document.getElementById('passwordForm').addEventListener('submit', function(e) {
e.preventDefault();
if (this.checkValidity()) {
alert('密码设置成功!');
}
});
</script>
</body>
</html>示例3:实时验证
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>实时验证</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
background: #f5f5f5;
}
.container { max-width: 600px; margin: 0 auto; }
.form-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
padding: 30px;
}
.form-card h3 { margin-top: 0; }
.form-group {
margin-bottom: 18px;
position: relative;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: bold;
font-size: 14px;
}
.input-wrapper {
position: relative;
}
.input-wrapper input {
width: 100%;
padding: 10px 40px 10px 12px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
transition: border-color 0.2s;
}
.input-wrapper input:focus {
outline: none;
border-color: #2196F3;
}
.input-wrapper .status-icon {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
font-size: 18px;
}
.form-group.valid .input-wrapper input { border-color: #4CAF50; }
.form-group.valid .status-icon { color: #4CAF50; }
.form-group.invalid .input-wrapper input { border-color: #f44336; }
.form-group.invalid .status-icon { color: #f44336; }
.form-group.validating .input-wrapper input { border-color: #FF9800; }
.error-msg {
font-size: 13px;
color: #f44336;
margin-top: 4px;
min-height: 20px;
}
.success-msg {
font-size: 13px;
color: #4CAF50;
margin-top: 4px;
}
.char-count {
font-size: 12px;
color: #888;
text-align: right;
margin-top: 2px;
}
.submit-btn {
padding: 12px 30px;
background: #2196F3;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
width: 100%;
}
</style>
</head>
<body>
<div class="container">
<h1>实时验证</h1>
<div class="form-card">
<h3>用户注册</h3>
<form id="regForm" novalidate>
<div class="form-group" id="group-username">
<label>用户名</label>
<div class="input-wrapper">
<input type="text" id="username" required minlength="3" maxlength="20" placeholder="3-20个字符">
<span class="status-icon" id="username-icon"></span>
</div>
<div class="error-msg" id="username-error"></div>
<div class="char-count"><span id="username-count">0</span>/20</div>
</div>
<div class="form-group" id="group-email">
<label>邮箱</label>
<div class="input-wrapper">
<input type="email" id="email" required placeholder="请输入邮箱地址">
<span class="status-icon" id="email-icon"></span>
</div>
<div class="error-msg" id="email-error"></div>
</div>
<div class="form-group" id="group-nickname">
<label>昵称</label>
<div class="input-wrapper">
<input type="text" id="nickname" required placeholder="请输入昵称">
<span class="status-icon" id="nickname-icon"></span>
</div>
<div class="error-msg" id="nickname-error"></div>
</div>
<button type="submit" class="submit-btn">注册</button>
</form>
</div>
</div>
<script>
const form = document.getElementById('regForm');
// 用户名实时验证
const username = document.getElementById('username');
username.addEventListener('input', function() {
const value = this.value;
document.getElementById('username-count').textContent = value.length;
if (!value) {
setFieldState('username', 'invalid', '用户名为必填项');
} else if (value.length < 3) {
setFieldState('username', 'invalid', '用户名至少3个字符');
} else if (value.length > 20) {
setFieldState('username', 'invalid', '用户名最多20个字符');
} else if (!/^[a-zA-Z0-9_]+$/.test(value)) {
setFieldState('username', 'invalid', '用户名只能包含字母、数字和下划线');
} else {
setFieldState('username', 'valid', '');
}
});
// 邮箱实时验证
const email = document.getElementById('email');
email.addEventListener('input', function() {
const value = this.value;
if (!value) {
setFieldState('email', 'invalid', '邮箱为必填项');
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
setFieldState('email', 'invalid', '请输入有效的邮箱地址');
} else {
setFieldState('email', 'valid', '');
}
});
// 昵称异步验证(模拟检查是否已存在)
const nickname = document.getElementById('nickname');
let nicknameTimer = null;
nickname.addEventListener('input', function() {
const value = this.value;
clearTimeout(nicknameTimer);
if (!value) {
setFieldState('nickname', 'invalid', '昵称为必填项');
return;
}
// 显示验证中状态
setFieldState('nickname', 'validating', '');
// 模拟异步验证
nicknameTimer = setTimeout(() => {
const existingNames = ['admin', 'test', 'user', 'demo'];
if (existingNames.includes(value.toLowerCase())) {
setFieldState('nickname', 'invalid', `昵称 "${value}" 已被使用`);
} else {
setFieldState('nickname', 'valid', '昵称可用');
}
}, 500);
});
function setFieldState(fieldName, state, message) {
const group = document.getElementById('group-' + fieldName);
const icon = document.getElementById(fieldName + '-icon');
const errorEl = document.getElementById(fieldName + '-error');
group.className = 'form-group ' + state;
if (state === 'valid') {
icon.innerHTML = '✓';
errorEl.className = message ? 'success-msg' : 'error-msg';
errorEl.textContent = message;
} else if (state === 'invalid') {
icon.innerHTML = '✗';
errorEl.className = 'error-msg';
errorEl.textContent = message;
} else if (state === 'validating') {
icon.innerHTML = '⌛';
errorEl.className = 'error-msg';
errorEl.textContent = '验证中...';
}
}
// 表单提交
form.addEventListener('submit', function(e) {
e.preventDefault();
// 触发所有字段的验证
username.dispatchEvent(new Event('input'));
email.dispatchEvent(new Event('input'));
nickname.dispatchEvent(new Event('input'));
// 检查是否所有字段都有效
const allValid = document.querySelectorAll('.form-group.valid').length === 3;
if (allValid) {
alert('注册成功!');
}
});
</script>
</body>
</html>示例4:异步验证
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>异步验证</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
background: #f5f5f5;
}
.container { max-width: 600px; margin: 0 auto; }
.form-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
padding: 30px;
}
.form-card h3 { margin-top: 0; }
.form-group {
margin-bottom: 18px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: bold;
font-size: 14px;
}
.input-with-btn {
display: flex;
gap: 8px;
}
.input-with-btn input {
flex: 1;
padding: 10px 12px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
}
.input-with-btn input:focus { outline: none; border-color: #2196F3; }
.input-with-btn input.valid { border-color: #4CAF50; }
.input-with-btn input.invalid { border-color: #f44336; }
.input-with-btn input.checking { border-color: #FF9800; }
.check-btn {
padding: 10px 16px;
background: #2196F3;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
white-space: nowrap;
}
.check-btn:disabled { background: #ccc; cursor: not-allowed; }
.field-message {
font-size: 13px;
margin-top: 4px;
min-height: 20px;
}
.field-message.error { color: #f44336; }
.field-message.success { color: #4CAF50; }
.field-message.checking { color: #FF9800; }
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid #FF9800;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
vertical-align: middle;
margin-right: 4px;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="container">
<h1>异步验证</h1>
<div class="form-card">
<h3>异步字段验证</h3>
<p>模拟服务器端验证,如检查用户名是否已存在、邮箱是否已注册。</p>
<div class="form-group">
<label>用户名</label>
<div class="input-with-btn">
<input type="text" id="asyncUsername" placeholder="输入用户名检查可用性">
<button class="check-btn" id="checkUsernameBtn">检查</button>
</div>
<div class="field-message" id="usernameMsg"></div>
</div>
<div class="form-group">
<label>邮箱</label>
<div class="input-with-btn">
<input type="email" id="asyncEmail" placeholder="输入邮箱检查是否已注册">
<button class="check-btn" id="checkEmailBtn">检查</button>
</div>
<div class="field-message" id="emailMsg"></div>
</div>
<div class="form-group">
<label>邀请码</label>
<div class="input-with-btn">
<input type="text" id="inviteCode" placeholder="输入邀请码验证">
<button class="check-btn" id="checkCodeBtn">验证</button>
</div>
<div class="field-message" id="codeMsg"></div>
</div>
</div>
</div>
<script>
// 模拟服务器验证
const mockServer = {
existingUsers: ['admin', 'test', 'user', 'demo', 'root'],
existingEmails: ['admin@example.com', 'test@example.com'],
validCodes: ['ABC123', 'XYZ789', 'WELCOME'],
checkUsername(username) {
return new Promise(resolve => {
setTimeout(() => {
const exists = this.existingUsers.includes(username.toLowerCase());
resolve({ available: !exists, message: exists ? '用户名已被占用' : '用户名可用' });
}, 800 + Math.random() * 500);
});
},
checkEmail(email) {
return new Promise(resolve => {
setTimeout(() => {
const exists = this.existingEmails.includes(email.toLowerCase());
resolve({ available: !exists, message: exists ? '邮箱已注册' : '邮箱可用' });
}, 600 + Math.random() * 400);
});
},
checkCode(code) {
return new Promise(resolve => {
setTimeout(() => {
const valid = this.validCodes.includes(code.toUpperCase());
resolve({ valid, message: valid ? '邀请码有效' : '邀请码无效' });
}, 1000 + Math.random() * 500);
});
}
};
// 用户名异步验证
const asyncUsername = document.getElementById('asyncUsername');
const checkUsernameBtn = document.getElementById('checkUsernameBtn');
const usernameMsg = document.getElementById('usernameMsg');
let usernameTimer = null;
asyncUsername.addEventListener('input', function() {
clearTimeout(usernameTimer);
this.classList.remove('valid', 'invalid');
usernameMsg.textContent = '';
usernameMsg.className = 'field-message';
if (this.value.length >= 3) {
usernameTimer = setTimeout(() => checkUsername(), 500);
}
});
checkUsernameBtn.addEventListener('click', checkUsername);
async function checkUsername() {
const value = asyncUsername.value.trim();
if (!value) return;
asyncUsername.classList.add('checking');
checkUsernameBtn.disabled = true;
usernameMsg.className = 'field-message checking';
usernameMsg.innerHTML = '<span class="spinner"></span>检查中...';
try {
const result = await mockServer.checkUsername(value);
asyncUsername.classList.remove('checking');
asyncUsername.classList.add(result.available ? 'valid' : 'invalid');
usernameMsg.className = 'field-message ' + (result.available ? 'success' : 'error');
usernameMsg.textContent = result.message;
// 使用 setCustomValidity
asyncUsername.setCustomValidity(result.available ? '' : result.message);
} catch (err) {
usernameMsg.className = 'field-message error';
usernameMsg.textContent = '验证失败,请重试';
} finally {
checkUsernameBtn.disabled = false;
}
}
// 邮箱异步验证
const asyncEmail = document.getElementById('asyncEmail');
const checkEmailBtn = document.getElementById('checkEmailBtn');
const emailMsg = document.getElementById('emailMsg');
let emailTimer = null;
asyncEmail.addEventListener('input', function() {
clearTimeout(emailTimer);
this.classList.remove('valid', 'invalid');
emailMsg.textContent = '';
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.value)) {
emailTimer = setTimeout(() => checkEmail(), 500);
}
});
checkEmailBtn.addEventListener('click', checkEmail);
async function checkEmail() {
const value = asyncEmail.value.trim();
if (!value) return;
asyncEmail.classList.add('checking');
checkEmailBtn.disabled = true;
emailMsg.className = 'field-message checking';
emailMsg.innerHTML = '<span class="spinner"></span>检查中...';
try {
const result = await mockServer.checkEmail(value);
asyncEmail.classList.remove('checking');
asyncEmail.classList.add(result.available ? 'valid' : 'invalid');
emailMsg.className = 'field-message ' + (result.available ? 'success' : 'error');
emailMsg.textContent = result.message;
asyncEmail.setCustomValidity(result.available ? '' : result.message);
} finally {
checkEmailBtn.disabled = false;
}
}
// 邀请码验证
const inviteCode = document.getElementById('inviteCode');
const checkCodeBtn = document.getElementById('checkCodeBtn');
const codeMsg = document.getElementById('codeMsg');
checkCodeBtn.addEventListener('click', async function() {
const value = inviteCode.value.trim();
if (!value) {
codeMsg.className = 'field-message error';
codeMsg.textContent = '请输入邀请码';
return;
}
inviteCode.classList.add('checking');
checkCodeBtn.disabled = true;
codeMsg.className = 'field-message checking';
codeMsg.innerHTML = '<span class="spinner"></span>验证中...';
try {
const result = await mockServer.checkCode(value);
inviteCode.classList.remove('checking');
inviteCode.classList.add(result.valid ? 'valid' : 'invalid');
codeMsg.className = 'field-message ' + (result.valid ? 'success' : 'error');
codeMsg.textContent = result.message;
inviteCode.setCustomValidity(result.valid ? '' : result.message);
} finally {
checkCodeBtn.disabled = false;
}
});
</script>
</body>
</html>示例5:完整表单验证系统
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>完整表单验证系统</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
background: #0f0f23;
color: white;
}
.app { max-width: 500px; margin: 0 auto; }
.form-container {
background: #16213e;
border-radius: 16px;
padding: 30px;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
.form-container h2 {
text-align: center;
margin-bottom: 25px;
}
.form-step {
display: none;
}
.form-step.active { display: block; }
.field {
margin-bottom: 18px;
}
.field label {
display: block;
margin-bottom: 6px;
font-size: 14px;
color: #aaa;
}
.field input, .field select {
width: 100%;
padding: 12px 14px;
background: #1e2a4a;
border: 2px solid #2a2a4a;
border-radius: 8px;
color: white;
font-size: 14px;
transition: border-color 0.2s;
}
.field input:focus, .field select:focus {
outline: none;
border-color: #667eea;
}
.field input.valid { border-color: #4CAF50; }
.field input.invalid { border-color: #f44336; }
.field-error {
font-size: 12px;
color: #f44336;
margin-top: 4px;
min-height: 18px;
}
.step-indicator {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 25px;
}
.step-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #2a2a4a;
transition: all 0.3s;
}
.step-dot.active { background: #667eea; transform: scale(1.3); }
.step-dot.completed { background: #4CAF50; }
.btn-row {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn {
flex: 1;
padding: 12px;
border: none;
border-radius: 8px;
font-size: 15px;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary { background: #667eea; color: white; }
.btn-primary:hover { background: #5a6fd6; }
.btn-secondary { background: #2a2a4a; color: #aaa; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.success-screen {
text-align: center;
padding: 40px 0;
}
.success-screen .icon { font-size: 60px; margin-bottom: 15px; }
.success-screen h3 { margin-bottom: 10px; }
.success-screen p { color: #888; }
</style>
</head>
<body>
<div class="app">
<div class="form-container">
<h2>用户注册</h2>
<div class="step-indicator">
<div class="step-dot active" id="dot1"></div>
<div class="step-dot" id="dot2"></div>
<div class="step-dot" id="dot3"></div>
</div>
<form id="multiForm" novalidate>
<!-- 步骤1:基本信息 -->
<div class="form-step active" id="step1">
<div class="field">
<label>用户名 *</label>
<input type="text" name="username" required minlength="3" maxlength="20"
data-rules="required|minLength:3|alphanumeric" placeholder="3-20个字符,字母数字下划线">
<div class="field-error" data-error="username"></div>
</div>
<div class="field">
<label>邮箱 *</label>
<input type="email" name="email" required
data-rules="required|email" placeholder="请输入邮箱">
<div class="field-error" data-error="email"></div>
</div>
<div class="field">
<label>密码 *</label>
<input type="password" name="password" required
data-rules="required|minLength:8|hasUpper|hasLower|hasDigit|hasSpecial" placeholder="至少8位,含大小写字母、数字和特殊字符">
<div class="field-error" data-error="password"></div>
</div>
<div class="btn-row">
<button type="button" class="btn btn-primary" onclick="nextStep()">下一步</button>
</div>
</div>
<!-- 步骤2:个人信息 -->
<div class="form-step" id="step2">
<div class="field">
<label>姓名 *</label>
<input type="text" name="realname" required
data-rules="required|chineseName" placeholder="请输入真实姓名">
<div class="field-error" data-error="realname"></div>
</div>
<div class="field">
<label>手机号 *</label>
<input type="tel" name="phone" required
data-rules="required|phone" placeholder="请输入11位手机号">
<div class="field-error" data-error="phone"></div>
</div>
<div class="field">
<label>年龄</label>
<input type="number" name="age" min="1" max="150"
data-rules="min:1|max:150" placeholder="请输入年龄">
<div class="field-error" data-error="age"></div>
</div>
<div class="btn-row">
<button type="button" class="btn btn-secondary" onclick="prevStep()">上一步</button>
<button type="button" class="btn btn-primary" onclick="nextStep()">下一步</button>
</div>
</div>
<!-- 步骤3:确认 -->
<div class="form-step" id="step3">
<div class="field">
<label>确认密码 *</label>
<input type="password" name="confirmPassword" required
data-rules="required|match:password" placeholder="请再次输入密码">
<div class="field-error" data-error="confirmPassword"></div>
</div>
<div class="field">
<label>邀请码</label>
<input type="text" name="inviteCode"
data-rules="inviteCode" placeholder="选填">
<div class="field-error" data-error="inviteCode"></div>
</div>
<div class="btn-row">
<button type="button" class="btn btn-secondary" onclick="prevStep()">上一步</button>
<button type="submit" class="btn btn-primary">提交注册</button>
</div>
</div>
</form>
<div class="success-screen" id="successScreen" style="display: none;">
<div class="icon">✓</div>
<h3>注册成功!</h3>
<p>您的账户已创建完成。</p>
</div>
</div>
</div>
<script>
let currentStep = 1;
const totalSteps = 3;
// 自定义验证规则
const customValidators = {
alphanumeric: (value) => /^[a-zA-Z0-9_]+$/.test(value) || '只能包含字母、数字和下划线',
chineseName: (value) => /^[\u4e00-\u9fa5]{2,6}$/.test(value) || '请输入2-6个中文字符',
phone: (value) => /^1[3-9]\d{9}$/.test(value) || '请输入有效的11位手机号',
hasUpper: (value) => /[A-Z]/.test(value) || '需要包含大写字母',
hasLower: (value) => /[a-z]/.test(value) || '需要包含小写字母',
hasDigit: (value) => /[0-9]/.test(value) || '需要包含数字',
hasSpecial: (value) => /[^A-Za-z0-9]/.test(value) || '需要包含特殊字符',
match: (value, field) => value === document.querySelector(`[name="${field}"]`).value || '两次输入不一致',
inviteCode: (value) => !value || /^[A-Z0-9]{6}$/.test(value.toUpperCase()) || '邀请码格式为6位字母数字',
};
// 通用验证规则
const baseValidators = {
required: (value) => value.trim() !== '' || '此字段为必填',
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) || '请输入有效的邮箱',
minLength: (value, len) => value.length >= parseInt(len) || `最少需要 ${len} 个字符`,
maxLength: (value, len) => value.length <= parseInt(len) || `最多允许 ${len} 个字符`,
min: (value, num) => !value || parseFloat(value) >= parseInt(num) || `最小值为 ${num}`,
max: (value, num) => !value || parseFloat(value) <= parseInt(num) || `最大值为 ${num}`,
};
// 验证单个字段
function validateField(input) {
const rules = input.dataset.rules;
if (!rules) return true;
const value = input.value;
const ruleList = rules.split('|');
const errorEl = document.querySelector(`[data-error="${input.name}"]`);
for (const rule of ruleList) {
const [ruleName, param] = rule.split(':');
const validator = baseValidators[ruleName] || customValidators[ruleName];
if (validator) {
const result = validator(value, param);
if (result !== true) {
input.classList.add('invalid');
input.classList.remove('valid');
input.setCustomValidity(result);
if (errorEl) errorEl.textContent = result;
return false;
}
}
}
input.classList.remove('invalid');
input.classList.add('valid');
input.setCustomValidity('');
if (errorEl) errorEl.textContent = '';
return true;
}
// 实时验证
document.querySelectorAll('[data-rules]').forEach(input => {
input.addEventListener('blur', function() {
if (this.value) validateField(this);
});
input.addEventListener('input', function() {
if (this.classList.contains('invalid')) {
validateField(this);
}
});
});
// 步骤导航
function nextStep() {
const currentStepEl = document.getElementById('step' + currentStep);
const inputs = currentStepEl.querySelectorAll('[data-rules]');
let allValid = true;
inputs.forEach(input => {
if (!validateField(input)) {
allValid = false;
}
});
if (!allValid) return;
if (currentStep < totalSteps) {
document.getElementById('step' + currentStep).classList.remove('active');
document.getElementById('dot' + currentStep).classList.remove('active');
document.getElementById('dot' + currentStep).classList.add('completed');
currentStep++;
document.getElementById('step' + currentStep).classList.add('active');
document.getElementById('dot' + currentStep).classList.add('active');
}
}
function prevStep() {
if (currentStep > 1) {
document.getElementById('step' + currentStep).classList.remove('active');
document.getElementById('dot' + currentStep).classList.remove('active');
currentStep--;
document.getElementById('step' + currentStep).classList.add('active');
document.getElementById('dot' + currentStep).classList.add('active');
document.getElementById('dot' + currentStep).classList.remove('completed');
}
}
// 表单提交
document.getElementById('multiForm').addEventListener('submit', function(e) {
e.preventDefault();
const inputs = this.querySelectorAll('[data-rules]');
let allValid = true;
inputs.forEach(input => {
if (!validateField(input)) {
allValid = false;
}
});
if (allValid) {
this.style.display = 'none';
document.querySelector('.step-indicator').style.display = 'none';
document.getElementById('successScreen').style.display = 'block';
}
});
</script>
</body>
</html>浏览器兼容性
注意事项与最佳实践
1. 始终在服务器端验证
前端验证只是用户体验优化,不能替代服务器端验证。恶意用户可以绕过前端验证。
2. 使用 novalidate 禁用默认验证
自定义验证 UI 时,使用 novalidate 禁用浏览器默认的验证气泡,然后使用 JavaScript 实现自定义验证反馈。
代码示例
<form novalidate>
<!-- 自定义验证逻辑 -->
</form>3. setCustomValidity 清空错误
当验证通过时,必须调用 setCustomValidity('') 清空自定义错误,否则 checkValidity() 始终返回 false。
代码示例
input.addEventListener('input', function() {
if (valid) {
this.setCustomValidity(''); // 必须清空!
} else {
this.setCustomValidity('错误消息');
}
});4. 实时验证时机
input事件:用户输入时实时验证(适合即时反馈)blur事件:用户离开字段时验证(避免过早提示错误)submit事件:提交时验证所有字段
推荐策略:首次 blur 后开始 input 实时验证。
5. 验证消息要具体友好
代码示例
// 不好
'输入无效'
// 好
'密码至少需要8个字符,包含大写字母、小写字母和数字'6. 异步验证需要防抖
异步验证(如检查用户名是否存在)应使用防抖,避免频繁请求服务器。
代码示例
let timer;
input.addEventListener('input', function() {
clearTimeout(timer);
timer = setTimeout(() => checkAvailability(this.value), 500);
});代码规范示例
规范的表单验证器
代码示例
class FormValidator {
constructor(form, options = {}) {
this.form = form;
this.options = {
liveValidation: options.liveValidation !== false,
validateOnBlur: options.validateOnBlur !== false,
debounceDelay: options.debounceDelay || 300,
onFieldValid: options.onFieldValid || null,
onFieldInvalid: options.onFieldInvalid || null,
};
this.validators = { ...FormValidator.defaultValidators, ...(options.validators || {}) };
this._init();
}
static defaultValidators = {
required: (v) => v.trim() !== '' || '此字段为必填',
email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || '请输入有效的邮箱',
minLength: (v, len) => v.length >= parseInt(len) || `最少需要 ${len} 个字符`,
maxLength: (v, len) => v.length <= parseInt(len) || `最多允许 ${len} 个字符`,
min: (v, num) => !v || parseFloat(v) >= parseInt(num) || `最小值为 ${num}`,
max: (v, num) => !v || parseFloat(v) <= parseInt(num) || `最大值为 ${num}`,
pattern: (v, regex) => new RegExp(regex).test(v) || '格式不正确',
};
_init() {
if (this.options.validateOnBlur) {
this.form.addEventListener('focusout', (e) => {
if (e.target.matches('[data-rules]')) {
this.validateField(e.target);
}
});
}
if (this.options.liveValidation) {
this.form.addEventListener('input', (e) => {
if (e.target.matches('[data-rules]') && e.target.dataset.validated) {
this.validateField(e.target);
}
});
}
this.form.addEventListener('submit', (e) => {
e.preventDefault();
if (this.validateAll()) {
this.form.submit();
}
});
}
validateField(input) {
const rules = input.dataset.rules;
if (!rules) return true;
input.dataset.validated = 'true';
const value = input.value;
for (const rule of rules.split('|')) {
const [name, param] = rule.split(':');
const validator = this.validators[name];
if (validator) {
const result = validator(value, param);
if (result !== true) {
input.setCustomValidity(result);
this._showError(input, result);
return false;
}
}
}
input.setCustomValidity('');
this._showSuccess(input);
return true;
}
validateAll() {
let allValid = true;
this.form.querySelectorAll('[data-rules]').forEach(input => {
if (!this.validateField(input)) allValid = false;
});
return allValid;
}
_showError(input, message) {
input.classList.add('invalid');
input.classList.remove('valid');
const errorEl = this.form.querySelector(`[data-error="${input.name}"]`);
if (errorEl) errorEl.textContent = message;
if (this.options.onFieldInvalid) this.options.onFieldInvalid(input, message);
}
_showSuccess(input) {
input.classList.remove('invalid');
input.classList.add('valid');
const errorEl = this.form.querySelector(`[data-error="${input.name}"]`);
if (errorEl) errorEl.textContent = '';
if (this.options.onFieldValid) this.options.onFieldValid(input);
}
}常见问题与解决方案
问题1:setCustomValidity 后 checkValidity 始终返回 false
原因:设置自定义错误后未清空。
解决方案:验证通过时调用 setCustomValidity('')。
问题2:reportValidity 在 IE 中不支持
解决方案:使用 polyfill 或手动触发验证并显示错误。
代码示例
if (!HTMLInputElement.prototype.reportValidity) {
HTMLInputElement.prototype.reportValidity = function() {
if (!this.checkValidity()) {
alert(this.validationMessage);
return false;
}
return true;
};
}问题3:number 类型输入框的验证问题
原因:type="number" 的输入框在用户输入非数字时,value 为空字符串,但 validity.badInput 为 true。
解决方案:检查 validity.badInput。
代码示例
if (input.validity.badInput) {
// 用户输入了非数字字符
}问题4:实时验证过早显示错误
原因:用户还没输入完就显示错误,体验不好。
解决方案:首次 blur 后才开始 input 实时验证。
问题5:异步验证竞态问题
原因:多个异步验证请求可能乱序返回。
解决方案:使用请求计数器或 AbortController。
代码示例
let requestId = 0;
async function validateAsync(value) {
const id = ++requestId;
const result = await fetch('/api/check?value=' + value);
if (id === requestId) {
// 只有最新的请求才更新 UI
updateUI(result);
}
}总结
JavaScript 表单验证是前端开发的核心技能,Constraint Validation API 提供了强大的声明式验证能力,而 JavaScript 则在此基础上实现了更灵活的自定义验证。以下是核心要点:
Constraint Validation API:使用
validity对象检查各种验证状态,checkValidity()和reportValidity()触发验证。setCustomValidity:设置自定义验证消息,验证通过时必须清空(
setCustomValidity(''))。实时验证:结合
input和blur事件,在合适的时机显示验证反馈。异步验证:使用防抖和竞态处理,确保异步验证的可靠性和用户体验。
自定义验证规则:通过
data-rules属性声明验证规则,实现声明式验证。验证 UI 反馈:使用 CSS 伪类(
:valid/:invalid)和自定义样式提供视觉反馈。novalidate:自定义验证 UI 时使用
novalidate禁用浏览器默认验证。服务器验证:前端验证只是用户体验优化,必须配合服务器端验证。
掌握表单验证的各种技术和最佳实践,能够构建用户友好、数据安全的表单交互体验。
常见问题
问题1:setCustomValidity 后 checkValidity 始终返回 false?
设置自定义错误后未清空。 验证通过时调用 setCustomValidity('')。
问题2:reportValidity 在 IE 中不支持?
使用 polyfill 或手动触发验证并显示错误。
问题3:number 类型输入框的验证问题?
type="number" 的输入框在用户输入非数字时,value 为空字符串,但 validity.badInput 为 true。 检查 validity.badInput。
问题4:实时验证过早显示错误?
用户还没输入完就显示错误,体验不好。 首次 blur 后才开始 input 实时验证。
问题5:异步验证竞态问题?
多个异步验证请求可能乱序返回。 使用请求计数器或 AbortController。
本文由小确幸生活整理发布,转载请注明出处
本文涉及AI创作
内容由AI创作,请仔细甄别