Skip to content

自定义验证消息

HTML5 表单验证提供了默认的浏览器提示消息,但这些消息通常以英文显示且样式不可控。通过 JavaScript 的 setCustomValidity()invalid 事件和 reportValidity() 方法,可以实现自定义的中文验证消息和更友好的提示体验。

前置知识

阅读本节前,建议先了解:autofocus / tabindex

基础概念

Constraint Validation API

setCustomValidity() 是浏览器提供的自定义验证消息 API,它允许为控件设置自定义的错误提示文本。主要方法:

方法/属性说明
setCustomValidity(message)设置自定义验证消息。空字符串表示验证通过
validity.customError是否存在自定义验证错误
validationMessage当前的验证消息文本
reportValidity()触发浏览器显示验证消息
checkValidity()检查验证状态但不显示消息

invalid 事件

当控件验证失败时触发 invalid 事件,可以在该事件中自定义消息:

javascript
input.addEventListener('invalid', function () {
    if (this.validity.valueMissing) {
        this.setCustomValidity('此字段不能为空');
    }
});

语法

setCustomValidity 基本用法

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>自定义验证消息</title>
</head>
<body>
    <h2>自定义验证</h2>
    <form id="custom-form">
        <div>
            <label for="username">用户名:</label>
            <input type="text" id="username" name="username" required
                   minlength="3" maxlength="20">
        </div>
        <div>
            <label for="age">年龄:</label>
            <input type="number" id="age" name="age" min="1" max="120">
        </div>
        <button type="submit">提交</button>
    </form>

    <script>
        // 用户名自定义消息
        const usernameInput = document.getElementById('username');
        usernameInput.addEventListener('invalid', function () {
            if (this.validity.valueMissing) {
                this.setCustomValidity('请输入用户名');
            } else if (this.validity.tooShort) {
                this.setCustomValidity('用户名至少需要 3 个字符');
            } else if (this.validity.tooLong) {
                this.setCustomValidity('用户名不能超过 20 个字符');
            } else {
                this.setCustomValidity(''); // 清除自定义消息
            }
        });

        // 输入时清除自定义消息
        usernameInput.addEventListener('input', function () {
            this.setCustomValidity('');
        });
    </script>
</body>
</html>

reportValidity 和 checkValidity

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>reportValidity 和 checkValidity</title>
</head>
<body>
    <h2>验证方法对比</h2>
    <form id="demo-form">
        <label for="email">邮箱:</label>
        <input type="email" id="email" name="email" required>
        <br><br>
        <button type="button" id="check-btn">checkValidity()</button>
        <button type="button" id="report-btn">reportValidity()</button>
        <button type="submit">提交</button>
    </form>
    <p id="result"></p>

    <script>
        const form = document.getElementById('demo-form');
        const result = document.getElementById('result');

        // checkValidity:检查但不显示浏览器提示
        document.getElementById('check-btn').addEventListener('click', function () {
            const valid = form.checkValidity();
            result.textContent = `checkValidity 返回:${valid}(不显示浏览器提示)`;
        });

        // reportValidity:检查并显示浏览器提示
        document.getElementById('report-btn').addEventListener('click', function () {
            const valid = form.reportValidity();
            result.textContent = `reportValidity 返回:${valid}(显示浏览器提示)`;
        });

        // 表单提交前拦截
        form.addEventListener('submit', function (e) {
            e.preventDefault();
            if (form.checkValidity()) {
                result.textContent = '表单验证通过,可以提交';
            } else {
                form.reportValidity();
            }
        });
    </script>
</body>
</html>

详细说明

自定义消息匹配验证类型

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>多类型自定义消息</title>
    <style>
        body { font-family: sans-serif; max-width: 600px; margin: 40px auto; }
        .form-group { margin-bottom: 16px; }
        label { display: block; font-weight: 600; margin-bottom: 4px; }
        input, select {
            width: 100%;
            padding: 8px 12px;
            border: 2px solid #ddd;
            border-radius: 6px;
            box-sizing: border-box;
        }
        button { padding: 10px 24px; background: #4a90d9; color: #fff; border: none; border-radius: 6px; cursor: pointer; }
    </style>
</head>
<body>
    <h2>自定义验证消息</h2>
    <form id="form">
        <div class="form-group">
            <label for="name">姓名:</label>
            <input type="text" id="name" name="name" required minlength="2">
        </div>
        <div class="form-group">
            <label for="phone">手机号:</label>
            <input type="tel" id="phone" name="phone" pattern="1[3-9]\d{9}" required>
        </div>
        <div class="form-group">
            <label for="age">年龄:</label>
            <input type="number" id="age" name="age" min="1" max="120">
        </div>
        <button type="submit">提交</button>
    </form>

    <script>
        // 为每个输入框设置自定义消息
        function setupValidation(inputId, rules) {
            const input = document.getElementById(inputId);
            input.addEventListener('invalid', function () {
                // 清除之前的消息
                this.setCustomValidity('');
                // 逐条检查验证规则
                for (const rule of rules) {
                    if (this.validity[rule.check]) {
                        this.setCustomValidity(rule.message);
                        break;
                    }
                }
            });
            input.addEventListener('input', function () {
                this.setCustomValidity('');
            });
        }

        setupValidation('name', [
            { check: 'valueMissing', message: '姓名不能为空' },
            { check: 'tooShort', message: '姓名至少需要 2 个字符' }
        ]);

        setupValidation('phone', [
            { check: 'valueMissing', message: '手机号不能为空' },
            { check: 'patternMismatch', message: '请输入有效的 11 位手机号' }
        ]);

        setupValidation('age', [
            { check: 'rangeUnderflow', message: '年龄不能小于 1' },
            { check: 'rangeOverflow', message: '年龄不能大于 120' },
            { check: 'typeMismatch', message: '请输入有效的数字' }
        ]);
    </script>
</body>
</html>

自定义气泡消息 vs 内联消息

浏览器内置的验证气泡(tooltip)在不同浏览器中样式不一致。许多开发者选择使用内联错误消息替代:

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>内联验证消息</title>
    <style>
        body { font-family: sans-serif; max-width: 600px; margin: 40px auto; }
        .form-group { margin-bottom: 16px; }
        label { display: block; font-weight: 600; margin-bottom: 4px; }
        input {
            width: 100%;
            padding: 8px 12px;
            border: 2px solid #ddd;
            border-radius: 6px;
            box-sizing: border-box;
        }
        .error {
            color: #e74c3c;
            font-size: 14px;
            margin-top: 4px;
            display: none;
        }
        input:not(:focus):invalid ~ .error {
            display: block;
        }
        button {
            padding: 10px 24px;
            background: #4a90d9;
            color: #fff;
            border: none;
            border-radius: 6px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <h2>内联验证消息</h2>
    <!-- 使用 novalidate 禁用浏览器默认验证 -->
    <form id="form" novalidate>
        <div class="form-group">
            <label for="email">邮箱:</label>
            <input type="email" id="email" name="email" required>
            <p class="error" id="email-error">请输入有效的邮箱地址</p>
        </div>
        <div class="form-group">
            <label for="password">密码:</label>
            <input type="password" id="password" name="password" required minlength="8">
            <p class="error" id="password-error">密码至少需要 8 位</p>
        </div>
        <button type="submit">提交</button>
    </form>

    <script>
        // 使用 CSS :invalid 伪类 + 相邻选择器实现内联消息
        // 无需 JavaScript,纯 CSS 方案
    </script>
</body>
</html>

validationMessage 属性

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>validationMessage</title>
</head>
<body>
    <h2>获取验证消息</h2>
    <form>
        <label for="email">邮箱:</label>
        <input type="email" id="email" name="email" required>
        <br><br>
        <button type="button" onclick="showMessage()">查看验证消息</button>
    </form>
    <p id="msg"></p>

    <script>
        function showMessage() {
            const input = document.getElementById('email');
            const valid = input.checkValidity();

            document.getElementById('msg').textContent =
                `验证通过:${valid},消息:${input.validationMessage}`;
        }
    </script>
</body>
</html>

实战示例

统一验证系统

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>统一验证系统</title>
    <style>
        body { font-family: sans-serif; max-width: 600px; margin: 40px auto; }
        .form-group { margin-bottom: 16px; position: relative; }
        label { display: block; font-weight: 600; margin-bottom: 4px; }
        input { width: 100%; padding: 8px 12px; border: 2px solid #ddd; border-radius: 6px; box-sizing: border-box; }
        .tip { font-size: 13px; color: #666; margin-top: 2px; }
        .error { font-size: 14px; color: #e74c3c; margin-top: 4px; display: none; }
        button { padding: 10px 24px; background: #4a90d9; color: #fff; border: none; border-radius: 6px; cursor: pointer; }
    </style>
</head>
<body>
    <h2>注册</h2>
    <form id="form" novalidate>
        <div class="form-group">
            <label for="username">用户名:</label>
            <input type="text" id="username" name="username" required minlength="3" maxlength="20"
                   data-error-required="用户名不能为空" data-error-minlength="用户名至少3个字符">
            <p class="error" id="username-error"></p>
        </div>
        <div class="form-group">
            <label for="email">邮箱:</label>
            <input type="email" id="email" name="email" required
                   data-error-required="邮箱不能为空" data-error-type="请输入有效的邮箱">
            <p class="error" id="email-error"></p>
        </div>
        <button type="submit">注册</button>
    </form>

    <script>
        // 统一验证逻辑
        document.getElementById('form').addEventListener('submit', function (e) {
            e.preventDefault();
            let valid = true;

            this.querySelectorAll('input').forEach(input => {
                const errorEl = document.getElementById(input.id + '-error');
                input.setCustomValidity('');

                if (!input.checkValidity()) {
                    valid = false;
                    // 先尝试 data-* 自定义消息
                    const customMsg = input.dataset['errorRequired'] ||
                                     input.dataset['errorMinlength'] ||
                                     input.dataset['errorType'] || '';
                    if (customMsg) {
                        input.setCustomValidity(customMsg);
                    }
                    errorEl.textContent = input.validationMessage;
                    errorEl.style.display = 'block';
                } else {
                    errorEl.style.display = 'none';
                }
            });

            if (valid) {
                alert('验证通过!');
            }
        });

        // 输入时清除错误
        document.querySelectorAll('#form input').forEach(input => {
            input.addEventListener('input', function () {
                this.setCustomValidity('');
                document.getElementById(this.id + '-error').style.display = 'none';
            });
        });
    </script>
</body>
</html>

注意事项

setCustomValidity 与 invalid 事件

  • setCustomValidity('') 清除自定义消息,恢复默认验证行为
  • 必须在 invalid 事件中设置自定义消息,否则默认消息已显示
  • input 事件中清除自定义消息,以便用户修正后重新验证

novalidate 禁用所有浏览器验证

<form novalidate> 禁用浏览器的默认验证,需要完全用 JavaScript 实现验证逻辑。

validationMessage 的语言

浏览器默认的 validationMessage 可能是英文的。使用 setCustomValidity() 可以设置为中文。

最佳实践

  1. 中文消息:通过 setCustomValidity() 替代浏览器默认的英文提示
  2. 输入时清除:在 input 事件中调用 setCustomValidity('')
  3. 统一验证框架:使用 data-* 属性配置验证规则和消息
  4. 内联消息优先:自定义内联消息比浏览器 tooltip 体验更好
  5. 配合 CSS 伪类:valid/:invalid 提供实时视觉反馈
  6. 服务端验证并行:客户端验证增强体验,服务端验证保障安全

下一节

继续学习:Constraint Validation API

参考链接