pin_drop当前位置:知识文库 ❯ 图文

JS集成:HTML 表单验证 JS 详解 - 详细教程与实战指南

教程简介

JavaScript 表单验证是前端开发中最常见的任务之一,它确保用户输入的数据符合预期格式和业务规则。HTML5 引入了 Constraint Validation API,提供了声明式的表单验证能力,而 JavaScript 则在此基础上实现了更灵活的自定义验证、实时验证和异步验证。完善的表单验证不仅能防止无效数据提交到服务器,还能通过友好的 UI 反馈提升用户体验。

本教程将深入讲解 JavaScript 表单验证、Constraint Validation API、validity 对象、setCustomValidity、checkValidity、reportValidity、实时验证、异步验证、自定义验证规则以及验证 UI 反馈。


核心概念

表单验证层次

层次 技术 说明
HTML 验证 required, pattern, min, max 等 声明式,浏览器原生支持
Constraint Validation API validity, checkValidity 等 JavaScript 调用 HTML5 验证
自定义 JS 验证 setCustomValidity, 自定义逻辑 灵活的业务规则验证
服务器验证 后端逻辑 最终安全保障

Constraint Validation API

Constraint Validation API 是 HTML5 提供的表单验证接口,包含以下核心部分:

API 说明
element.validity ValidityState 对象,包含各种验证状态
element.checkValidity() 检查有效性,返回 boolean
element.reportValidity() 检查并显示验证消息,返回 boolean
element.setCustomValidity(msg) 设置自定义验证消息
element.validationMessage 当前验证消息
element.willValidate 元素是否参与验证
form.checkValidity() 检查整个表单
form.reportValidity() 检查并显示整个表单的验证消息

ValidityState 对象属性

属性 说明 对应的 HTML 属性/规则
valueMissing 必填字段为空 required
typeMismatch 值与类型不匹配 type="email", type="url"
patternMismatch 值与正则不匹配 pattern
tooLong 值超过最大长度 maxlength
tooShort 值不足最小长度 minlength
rangeUnderflow 值小于最小值 min
rangeOverflow 值大于最大值 max
stepMismatch 值不符合步长 step
badInput 浏览器无法转换输入 如字母输入到 number
customError 有自定义错误 setCustomValidity()
valid 所有验证都通过 -

HTML5 验证属性

属性 说明 示例
required 必填 <input required>
pattern 正则验证 <input pattern="[0-9]{3}">
minlength 最小长度 <input minlength="3">
maxlength 最大长度 <input maxlength="20">
min 最小值 <input type="number" min="0">
max 最大值 <input type="number" max="100">
step 步长 <input type="number" step="0.1">
type 输入类型 <input type="email">

语法与用法

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 = '&#10003;';
                errorEl.className = message ? 'success-msg' : 'error-msg';
                errorEl.textContent = message;
            } else if (state === 'invalid') {
                icon.innerHTML = '&#10007;';
                errorEl.className = 'error-msg';
                errorEl.textContent = message;
            } else if (state === 'validating') {
                icon.innerHTML = '&#8987;';
                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">&#10003;</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>

浏览器兼容性

特性 Chrome Firefox Safari Edge IE
Constraint Validation API 10+ 4+ 10+ 12+ 10+
validity 对象 10+ 4+ 10+ 12+ 10+
checkValidity() 10+ 4+ 10+ 12+ 10+
reportValidity() 40+ 49+ 10.1+ 17+ 不支持
setCustomValidity() 10+ 4+ 10+ 12+ 10+
:valid / :invalid 伪类 10+ 4+ 5+ 12+ 10+
:placeholder-shown 47+ 51+ 9+ 79+ 不支持
novalidate 属性 10+ 4+ 10+ 12+ 10+
input type="email" 10+ 4+ 10+ 12+ 10+
pattern 属性 10+ 4+ 10+ 12+ 10+

注意事项与最佳实践

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 则在此基础上实现了更灵活的自定义验证。以下是核心要点:

  1. Constraint Validation API:使用 validity 对象检查各种验证状态,checkValidity()reportValidity() 触发验证。

  2. setCustomValidity:设置自定义验证消息,验证通过时必须清空(setCustomValidity(''))。

  3. 实时验证:结合 inputblur 事件,在合适的时机显示验证反馈。

  4. 异步验证:使用防抖和竞态处理,确保异步验证的可靠性和用户体验。

  5. 自定义验证规则:通过 data-rules 属性声明验证规则,实现声明式验证。

  6. 验证 UI 反馈:使用 CSS 伪类(:valid/:invalid)和自定义样式提供视觉反馈。

  7. novalidate:自定义验证 UI 时使用 novalidate 禁用浏览器默认验证。

  8. 服务器验证:前端验证只是用户体验优化,必须配合服务器端验证。

掌握表单验证的各种技术和最佳实践,能够构建用户友好、数据安全的表单交互体验。

常见问题

问题1:setCustomValidity 后 checkValidity 始终返回 false?

设置自定义错误后未清空。 验证通过时调用 setCustomValidity('')

问题2:reportValidity 在 IE 中不支持?

使用 polyfill 或手动触发验证并显示错误。

问题3:number 类型输入框的验证问题?

type="number" 的输入框在用户输入非数字时,value 为空字符串,但 validity.badInput 为 true。 检查 validity.badInput

问题4:实时验证过早显示错误?

用户还没输入完就显示错误,体验不好。 首次 blur 后才开始 input 实时验证。

问题5:异步验证竞态问题?

多个异步验证请求可能乱序返回。 使用请求计数器或 AbortController。

标签: Script标签 JavaScript JS HTML 表单验证 状态管理

本文由小确幸生活整理发布,转载请注明出处

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: JS集成:HTML 事件委托详解 - 详细教程与实战指南 下一篇: 高级技巧:HTML无障碍访问基础 - 详细教程与实战指南

poll相关推荐