Skip to content

3.5 表单验证

关键词: HTML5表单验证, 内置验证, 自定义验证, 正则表达式, 验证消息, 客户端验证, 服务端验证, 表单安全

学习目标

  • 掌握HTML5内置验证机制
  • 学会自定义验证消息
  • 理解正则表达式验证
  • 掌握JavaScript表单验证
  • 学会表单验证最佳实践

3.5.1 HTML5内置验证

基本概念

HTML5提供了内置的表单验证功能,包括必填字段、数据类型、长度限制等。

常用验证属性

1. 基础验证属性

html
<input type="text" required>                    <!-- 必填 -->
<input type="email" required>                   <!-- 邮箱格式 -->
<input type="url" required>                     <!-- URL格式 -->
<input type="number" min="1" max="100">         <!-- 数字范围 -->
<input type="text" minlength="3" maxlength="20"> <!-- 长度限制 -->
<input type="text" pattern="[0-9]{3}-[0-9]{4}"> <!-- 正则表达式 -->

2. 实际应用示例

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>HTML5内置验证示例</title>
    <style>
        .form-container {
            max-width: 500px;
            margin: 20px auto;
            padding: 20px;
            border: 1px solid #ddd;
            border-radius: 8px;
        }
        
        .form-group {
            margin-bottom: 20px;
        }
        
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        
        input, select, textarea {
            width: 100%;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-size: 14px;
            transition: border-color 0.3s;
        }
        
        input:focus, select:focus, textarea:focus {
            outline: none;
            border-color: #007bff;
            box-shadow: 0 0 0 2px rgba(0,123,255,0.25);
        }
        
        input:valid {
            border-color: #28a745;
        }
        
        input:invalid {
            border-color: #dc3545;
        }
        
        .error-message {
            color: #dc3545;
            font-size: 12px;
            margin-top: 5px;
        }
        
        .success-message {
            color: #28a745;
            font-size: 12px;
            margin-top: 5px;
        }
        
        .submit-btn {
            width: 100%;
            padding: 12px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
        }
        
        .submit-btn:hover {
            background-color: #0056b3;
        }
        
        .validation-summary {
            margin-top: 20px;
            padding: 15px;
            background-color: #f8f9fa;
            border-radius: 4px;
            border-left: 4px solid #007bff;
        }
    </style>
</head>
<body>
    <div class="form-container">
        <h3>HTML5内置验证示例</h3>
        
        <form id="validation-form" novalidate>
            <div class="form-group">
                <label for="username">用户名(必填,3-20字符):</label>
                <input type="text" id="username" name="username" 
                       required minlength="3" maxlength="20"
                       title="用户名必须是3-20个字符">
                <div class="error-message" id="username-error"></div>
            </div>
            
            <div class="form-group">
                <label for="email">邮箱地址(必填):</label>
                <input type="email" id="email" name="email" required
                       title="请输入有效的邮箱地址">
                <div class="error-message" id="email-error"></div>
            </div>
            
            <div class="form-group">
                <label for="website">个人网站:</label>
                <input type="url" id="website" name="website"
                       title="请输入有效的网址">
                <div class="error-message" id="website-error"></div>
            </div>
            
            <div class="form-group">
                <label for="age">年龄(18-100岁):</label>
                <input type="number" id="age" name="age" 
                       min="18" max="100" required
                       title="年龄必须在18-100岁之间">
                <div class="error-message" id="age-error"></div>
            </div>
            
            <div class="form-group">
                <label for="phone">电话号码(格式:XXX-XXXX-XXXX):</label>
                <input type="tel" id="phone" name="phone" 
                       pattern="[0-9]{3}-[0-9]{4}-[0-9]{4}"
                       title="请输入格式为 XXX-XXXX-XXXX 的电话号码">
                <div class="error-message" id="phone-error"></div>
            </div>
            
            <div class="form-group">
                <label for="password">密码(至少8位,包含数字和字母):</label>
                <input type="password" id="password" name="password" 
                       pattern="^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$"
                       required title="密码必须至少8位,包含数字和字母">
                <div class="error-message" id="password-error"></div>
            </div>
            
            <button type="submit" class="submit-btn">提交表单</button>
        </form>
        
        <div class="validation-summary" id="validation-summary" style="display: none;">
            <h4>验证结果:</h4>
            <ul id="validation-results"></ul>
        </div>
    </div>
    
    <script>
        const form = document.getElementById('validation-form');
        const inputs = form.querySelectorAll('input');
        
        // 为每个输入框添加实时验证
        inputs.forEach(input => {
            input.addEventListener('input', function() {
                validateField(this);
            });
            
            input.addEventListener('blur', function() {
                validateField(this);
            });
        });
        
        function validateField(field) {
            const errorDiv = document.getElementById(field.id + '-error');
            
            if (field.validity.valid) {
                errorDiv.textContent = '';
                errorDiv.className = 'success-message';
                errorDiv.textContent = '✓ 输入正确';
            } else {
                errorDiv.className = 'error-message';
                errorDiv.textContent = getValidationMessage(field);
            }
        }
        
        function getValidationMessage(field) {
            if (field.validity.valueMissing) {
                return '此字段为必填项';
            }
            if (field.validity.typeMismatch) {
                return '请输入正确的格式';
            }
            if (field.validity.tooShort) {
                return `至少需要输入 ${field.minLength} 个字符`;
            }
            if (field.validity.tooLong) {
                return `最多只能输入 ${field.maxLength} 个字符`;
            }
            if (field.validity.rangeUnderflow) {
                return `值不能小于 ${field.min}`;
            }
            if (field.validity.rangeOverflow) {
                return `值不能大于 ${field.max}`;
            }
            if (field.validity.patternMismatch) {
                return field.title || '输入格式不正确';
            }
            return '输入无效';
        }
        
        // 表单提交验证
        form.addEventListener('submit', function(e) {
            e.preventDefault();
            
            let isValid = true;
            const results = [];
            
            inputs.forEach(input => {
                if (!input.validity.valid) {
                    isValid = false;
                    results.push(`${input.previousElementSibling.textContent}: ${getValidationMessage(input)}`);
                } else {
                    results.push(`${input.previousElementSibling.textContent}: ✓ 验证通过`);
                }
            });
            
            // 显示验证结果
            const summaryDiv = document.getElementById('validation-summary');
            const resultsList = document.getElementById('validation-results');
            
            resultsList.innerHTML = results.map(result => `<li>${result}</li>`).join('');
            summaryDiv.style.display = 'block';
            
            if (isValid) {
                alert('表单验证成功!');
            }
        });
    </script>
</body>
</html>

易错点提醒

  1. novalidate属性:阻止浏览器默认验证,用于自定义验证
  2. title属性:为pattern等验证提供错误提示
  3. 浏览器兼容性:旧版浏览器可能不支持所有验证属性

3.5.2 自定义验证消息

使用setCustomValidity()方法

javascript
input.setCustomValidity('自定义错误消息');

实际应用示例

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>自定义验证消息示例</title>
    <style>
        .custom-form {
            max-width: 500px;
            margin: 20px auto;
            padding: 20px;
            border: 1px solid #ddd;
            border-radius: 8px;
        }
        
        .form-group {
            margin-bottom: 20px;
        }
        
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        
        input {
            width: 100%;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-size: 14px;
        }
        
        .validation-message {
            margin-top: 5px;
            font-size: 12px;
            padding: 5px;
            border-radius: 3px;
        }
        
        .error {
            color: #dc3545;
            background-color: #f8d7da;
            border: 1px solid #f5c6cb;
        }
        
        .success {
            color: #155724;
            background-color: #d4edda;
            border: 1px solid #c3e6cb;
        }
        
        .password-strength {
            height: 5px;
            background-color: #e9ecef;
            border-radius: 3px;
            margin-top: 5px;
            overflow: hidden;
        }
        
        .strength-bar {
            height: 100%;
            transition: width 0.3s, background-color 0.3s;
        }
        
        .strength-weak { background-color: #dc3545; }
        .strength-medium { background-color: #ffc107; }
        .strength-strong { background-color: #28a745; }
    </style>
</head>
<body>
    <div class="custom-form">
        <h3>自定义验证消息示例</h3>
        
        <form id="custom-form">
            <div class="form-group">
                <label for="confirm-password">确认密码:</label>
                <input type="password" id="confirm-password" name="confirm-password" required>
                <div class="validation-message" id="confirm-password-msg"></div>
            </div>
            
            <div class="form-group">
                <label for="strong-password">强密码(自定义规则):</label>
                <input type="password" id="strong-password" name="strong-password" required>
                <div class="password-strength">
                    <div class="strength-bar" id="strength-bar"></div>
                </div>
                <div class="validation-message" id="strong-password-msg"></div>
            </div>
            
            <div class="form-group">
                <label for="unique-username">唯一用户名:</label>
                <input type="text" id="unique-username" name="unique-username" required>
                <div class="validation-message" id="unique-username-msg"></div>
            </div>
            
            <div class="form-group">
                <label for="adult-age">成人年龄验证:</label>
                <input type="number" id="adult-age" name="adult-age" required>
                <div class="validation-message" id="adult-age-msg"></div>
            </div>
            
            <button type="submit">提交</button>
        </form>
    </div>
    
    <script>
        const form = document.getElementById('custom-form');
        const passwordInput = document.getElementById('strong-password');
        const confirmPasswordInput = document.getElementById('confirm-password');
        const usernameInput = document.getElementById('unique-username');
        const ageInput = document.getElementById('adult-age');
        
        // 已存在的用户名(模拟)
        const existingUsernames = ['admin', 'user', 'test', 'demo'];
        
        // 密码强度验证
        passwordInput.addEventListener('input', function() {
            const password = this.value;
            const strength = calculatePasswordStrength(password);
            updatePasswordStrength(strength);
            validatePassword(password);
        });
        
        function calculatePasswordStrength(password) {
            let score = 0;
            
            if (password.length >= 8) score += 25;
            if (password.length >= 12) score += 25;
            if (/[a-z]/.test(password)) score += 15;
            if (/[A-Z]/.test(password)) score += 15;
            if (/[0-9]/.test(password)) score += 10;
            if (/[^A-Za-z0-9]/.test(password)) score += 10;
            
            return Math.min(score, 100);
        }
        
        function updatePasswordStrength(strength) {
            const strengthBar = document.getElementById('strength-bar');
            strengthBar.style.width = strength + '%';
            
            if (strength < 40) {
                strengthBar.className = 'strength-bar strength-weak';
            } else if (strength < 70) {
                strengthBar.className = 'strength-bar strength-medium';
            } else {
                strengthBar.className = 'strength-bar strength-strong';
            }
        }
        
        function validatePassword(password) {
            const messageDiv = document.getElementById('strong-password-msg');
            
            if (password.length < 8) {
                passwordInput.setCustomValidity('密码长度至少8位');
                showMessage(messageDiv, '密码长度至少8位', 'error');
            } else if (!/[A-Za-z]/.test(password)) {
                passwordInput.setCustomValidity('密码必须包含字母');
                showMessage(messageDiv, '密码必须包含字母', 'error');
            } else if (!/[0-9]/.test(password)) {
                passwordInput.setCustomValidity('密码必须包含数字');
                showMessage(messageDiv, '密码必须包含数字', 'error');
            } else if (!/[^A-Za-z0-9]/.test(password)) {
                passwordInput.setCustomValidity('密码必须包含特殊字符');
                showMessage(messageDiv, '建议包含特殊字符以提高安全性', 'error');
            } else {
                passwordInput.setCustomValidity('');
                showMessage(messageDiv, '密码强度良好', 'success');
            }
        }
        
        // 确认密码验证
        confirmPasswordInput.addEventListener('input', function() {
            const confirmPassword = this.value;
            const originalPassword = passwordInput.value;
            const messageDiv = document.getElementById('confirm-password-msg');
            
            if (confirmPassword !== originalPassword) {
                this.setCustomValidity('两次输入的密码不一致');
                showMessage(messageDiv, '两次输入的密码不一致', 'error');
            } else {
                this.setCustomValidity('');
                showMessage(messageDiv, '密码匹配成功', 'success');
            }
        });
        
        // 用户名唯一性验证
        usernameInput.addEventListener('input', function() {
            const username = this.value.toLowerCase();
            const messageDiv = document.getElementById('unique-username-msg');
            
            if (existingUsernames.includes(username)) {
                this.setCustomValidity('用户名已存在');
                showMessage(messageDiv, '用户名已存在,请选择其他用户名', 'error');
            } else if (username.length < 3) {
                this.setCustomValidity('用户名至少3个字符');
                showMessage(messageDiv, '用户名至少3个字符', 'error');
            } else {
                this.setCustomValidity('');
                showMessage(messageDiv, '用户名可用', 'success');
            }
        });
        
        // 年龄验证
        ageInput.addEventListener('input', function() {
            const age = parseInt(this.value);
            const messageDiv = document.getElementById('adult-age-msg');
            
            if (age < 18) {
                this.setCustomValidity('必须年满18岁');
                showMessage(messageDiv, '必须年满18岁才能注册', 'error');
            } else if (age > 120) {
                this.setCustomValidity('年龄不能超过120岁');
                showMessage(messageDiv, '请输入真实年龄', 'error');
            } else {
                this.setCustomValidity('');
                showMessage(messageDiv, '年龄验证通过', 'success');
            }
        });
        
        function showMessage(element, message, type) {
            element.textContent = message;
            element.className = `validation-message ${type}`;
        }
        
        // 表单提交
        form.addEventListener('submit', function(e) {
            e.preventDefault();
            
            const isValid = form.checkValidity();
            if (isValid) {
                alert('表单验证成功!');
            } else {
                alert('请检查表单中的错误');
            }
        });
    </script>
</body>
</html>

3.5.3 正则表达式验证

常用正则表达式

javascript
// 手机号码(中国大陆)
const phoneRegex = /^1[3-9]\d{9}$/;

// 身份证号码(中国大陆)
const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}(\d|X)$/;

// 邮政编码
const postalCodeRegex = /^\d{6}$/;

// 银行卡号
const bankCardRegex = /^\d{16,19}$/;

实际应用示例

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>正则表达式验证示例</title>
    <style>
        .regex-form {
            max-width: 600px;
            margin: 20px auto;
            padding: 20px;
            border: 1px solid #ddd;
            border-radius: 8px;
        }
        
        .form-group {
            margin-bottom: 20px;
        }
        
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        
        input {
            width: 100%;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-size: 14px;
        }
        
        .validation-result {
            margin-top: 8px;
            padding: 8px;
            border-radius: 4px;
            font-size: 12px;
        }
        
        .valid {
            background-color: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        
        .invalid {
            background-color: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
        
        .pattern-info {
            font-size: 12px;
            color: #666;
            margin-top: 5px;
        }
    </style>
</head>
<body>
    <div class="regex-form">
        <h3>正则表达式验证示例</h3>
        
        <form>
            <div class="form-group">
                <label for="phone">手机号码:</label>
                <input type="text" id="phone" placeholder="请输入11位手机号">
                <div class="pattern-info">格式:1开头的11位数字</div>
                <div class="validation-result" id="phone-result"></div>
            </div>
            
            <div class="form-group">
                <label for="id-card">身份证号码:</label>
                <input type="text" id="id-card" placeholder="请输入18位身份证号">
                <div class="pattern-info">格式:18位数字,最后一位可以是X</div>
                <div class="validation-result" id="id-card-result"></div>
            </div>
            
            <div class="form-group">
                <label for="postal-code">邮政编码:</label>
                <input type="text" id="postal-code" placeholder="请输入6位邮政编码">
                <div class="pattern-info">格式:6位数字</div>
                <div class="validation-result" id="postal-code-result"></div>
            </div>
            
            <div class="form-group">
                <label for="bank-card">银行卡号:</label>
                <input type="text" id="bank-card" placeholder="请输入银行卡号">
                <div class="pattern-info">格式:16-19位数字</div>
                <div class="validation-result" id="bank-card-result"></div>
            </div>
            
            <div class="form-group">
                <label for="qq-number">QQ号码:</label>
                <input type="text" id="qq-number" placeholder="请输入QQ号">
                <div class="pattern-info">格式:5-11位数字,不能以0开头</div>
                <div class="validation-result" id="qq-number-result"></div>
            </div>
            
            <div class="form-group">
                <label for="license-plate">车牌号码:</label>
                <input type="text" id="license-plate" placeholder="请输入车牌号">
                <div class="pattern-info">格式:省份简称+字母+5位数字或字母</div>
                <div class="validation-result" id="license-plate-result"></div>
            </div>
        </form>
    </div>
    
    <script>
        // 正则表达式定义
        const patterns = {
            phone: {
                regex: /^1[3-9]\d{9}$/,
                message: '手机号码格式正确',
                errorMessage: '请输入有效的手机号码'
            },
            idCard: {
                regex: /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}(\d|X)$/i,
                message: '身份证号码格式正确',
                errorMessage: '请输入有效的身份证号码'
            },
            postalCode: {
                regex: /^\d{6}$/,
                message: '邮政编码格式正确',
                errorMessage: '请输入6位数字的邮政编码'
            },
            bankCard: {
                regex: /^\d{16,19}$/,
                message: '银行卡号格式正确',
                errorMessage: '请输入16-19位数字的银行卡号'
            },
            qqNumber: {
                regex: /^[1-9]\d{4,10}$/,
                message: 'QQ号码格式正确',
                errorMessage: '请输入5-11位数字的QQ号码'
            },
            licensePlate: {
                regex: /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/,
                message: '车牌号码格式正确',
                errorMessage: '请输入有效的车牌号码'
            }
        };
        
        // 为每个输入框添加验证
        Object.keys(patterns).forEach(key => {
            const input = document.getElementById(key.replace(/([A-Z])/g, '-$1').toLowerCase());
            const result = document.getElementById(key.replace(/([A-Z])/g, '-$1').toLowerCase() + '-result');
            
            input.addEventListener('input', function() {
                validateWithRegex(this, result, patterns[key]);
            });
        });
        
        function validateWithRegex(input, resultDiv, pattern) {
            const value = input.value.trim();
            
            if (value === '') {
                resultDiv.textContent = '';
                resultDiv.className = 'validation-result';
                return;
            }
            
            if (pattern.regex.test(value)) {
                resultDiv.textContent = '✓ ' + pattern.message;
                resultDiv.className = 'validation-result valid';
            } else {
                resultDiv.textContent = '✗ ' + pattern.errorMessage;
                resultDiv.className = 'validation-result invalid';
            }
        }
        
        // 银行卡号格式化显示
        document.getElementById('bank-card').addEventListener('input', function() {
            let value = this.value.replace(/\s/g, '');
            let formatted = value.replace(/(\d{4})(?=\d)/g, '$1 ');
            this.value = formatted;
        });
        
        // 身份证号码校验码验证
        function validateIdCard(idCard) {
            const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
            const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
            
            let sum = 0;
            for (let i = 0; i < 17; i++) {
                sum += parseInt(idCard.charAt(i)) * weights[i];
            }
            
            const checkCode = checkCodes[sum % 11];
            return checkCode === idCard.charAt(17).toUpperCase();
        }
        
        // 增强身份证验证
        document.getElementById('id-card').addEventListener('input', function() {
            const value = this.value.trim();
            const result = document.getElementById('id-card-result');
            
            if (value === '') {
                result.textContent = '';
                result.className = 'validation-result';
                return;
            }
            
            if (patterns.idCard.regex.test(value)) {
                if (validateIdCard(value)) {
                    result.textContent = '✓ 身份证号码格式正确,校验码验证通过';
                    result.className = 'validation-result valid';
                } else {
                    result.textContent = '✗ 身份证号码校验码错误';
                    result.className = 'validation-result invalid';
                }
            } else {
                result.textContent = '✗ 请输入有效的身份证号码';
                result.className = 'validation-result invalid';
            }
        });
    </script>
</body>
</html>

3.5.4 表单验证最佳实践

1. 实时验证与提交验证结合

javascript
// 实时验证
input.addEventListener('input', function() {
    validateField(this);
});

// 提交验证
form.addEventListener('submit', function(e) {
    if (!validateForm()) {
        e.preventDefault();
    }
});

2. 用户友好的错误提示

javascript
const errorMessages = {
    required: '此字段为必填项',
    email: '请输入有效的邮箱地址',
    minLength: '输入内容过短',
    maxLength: '输入内容过长',
    pattern: '输入格式不正确'
};

3. 视觉反馈设计

css
.input-valid {
    border-color: #28a745;
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><path fill="%2328a745" d="M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z"/></svg>');
    background-repeat: no-repeat;
    background-position: right 10px center;
    background-size: 16px 16px;
}

.input-invalid {
    border-color: #dc3545;
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><path fill="%23dc3545" d="M7.5 6.5L5 4 7.5 1.5 6.5 0.5 4 3 1.5 0.5 0.5 1.5 3 4 0.5 6.5 1.5 7.5 4 5 6.5 7.5z"/></svg>');
    background-repeat: no-repeat;
    background-position: right 10px center;
    background-size: 16px 16px;
}

易错点提醒

  1. 过度验证:不要在用户输入时就立即显示错误
  2. 错误信息:要清楚地说明如何修正错误
  3. 可访问性:确保屏幕阅读器可以读取错误信息
  4. 性能考虑:避免在每次输入时都进行复杂的验证

本节要点回顾

  • HTML5内置验证:利用浏览器原生验证功能,简单高效
  • 自定义验证消息:使用JavaScript提供更友好的错误提示
  • 正则表达式验证:处理复杂的格式验证需求
  • 最佳实践:创建用户友好的验证体验,注重可访问性
  • 安全性考虑:客户端验证只是辅助,服务器端验证是必需的
  • 用户体验优化:适时提供反馈,避免干扰用户输入过程

相关学习资源

常见问题FAQ

Q: HTML5验证与JavaScript验证有什么区别?

A: HTML5验证是浏览器原生功能,JavaScript验证更灵活,可以提供更丰富的用户体验。

Q: 如何处理不支持HTML5验证的旧浏览器?

A: 使用渐进增强策略,提供JavaScript回退方案,确保基本验证功能正常工作。

Q: 客户端验证是否足够安全?

A: 不够,客户端验证主要用于提升用户体验,服务器端验证是安全的关键。

Q: 如何平衡验证的及时性和用户体验?

A: 建议在用户完成输入后(失焦时)进行验证,避免在输入过程中频繁提示错误。


下一节预览第4章第1节 - 音频元素 - 学习HTML5音频元素的使用