首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >JavaScript原生实战手册 · 表单验证引擎:构建企业级验证系统

JavaScript原生实战手册 · 表单验证引擎:构建企业级验证系统

作者头像
前端达人
发布2025-10-09 12:49:18
发布2025-10-09 12:49:18
3500
代码可运行
举报
文章被收录于专栏:前端达人前端达人
运行总次数:0
代码可运行

用户提交了错误数据就让整个系统崩溃?一个强大的表单验证引擎让你的数据入口固若金汤!

表单验证是Web应用中最基础也是最重要的功能之一。无论是用户注册、数据录入、订单提交还是内容发布,都需要确保用户输入的数据符合预期格式和业务规则。糟糕的表单验证不仅会让用户感到沫名,还可能导致脏数据进入系统,引发安全问题或业务逻辑错误。今天我们就来打造一个功能完备、灵活可扩展的表单验证引擎。

生活中的表单验证场景

场景一:用户注册系统

想象你在开发一个社交媒体的注册页面:

代码语言:javascript
代码运行次数:0
运行
复制
用户名:必须3-20个字符,只能包含字母数字
邮箱:必须是有效的邮箱格式
密码:至少8位,包含大小写字母、数字和特殊字符
确认密码:必须与密码完全一致
手机号:符合国际手机号格式
个人网站:可选,但如果填写必须是有效URL

如果没有好的验证系统:

  • 用户输入错误数据后提交,服务器返回错误
  • 用户不知道具体哪里出错,需要重新填写整个表单
  • 重复的网络请求浪费资源
  • 用户体验差,可能放弃注册

场景二:电商订单表单

在电商网站的结算页面:

代码语言:javascript
代码运行次数:0
运行
复制
收货地址:必填,长度限制
联系电话:必须是有效手机号
优惠码:可选,但需要验证有效性和使用条件
支付方式:必选
发票信息:企业发票需要填写税号,个人发票可选

这种场景需要:

  • 实时验证,用户输入时就给出反馈
  • 条件验证,根据选择显示不同验证规则
  • 异步验证,需要调用服务器验证优惠码
  • 友好的错误提示

场景三:企业数据录入系统

在企业管理系统中录入员工信息:

代码语言:javascript
代码运行次数:0
运行
复制
员工工号:唯一性验证,格式检查
身份证号:15位或18位,校验位算法验证
入职日期:不能早于公司成立日期,不能晚于今天
薪资:数字范围验证,根据职级限制
部门:下拉选择,某些部门需要额外审批
紧急联系人:必须填写且不能是员工本人

这类系统对数据准确性要求极高,验证规则复杂,需要支持自定义业务规则。

传统验证方式的痛点

痛点一:验证逻辑散落各处

代码语言:javascript
代码运行次数:0
运行
复制
// 传统方式:验证代码混在业务逻辑中
function handleSubmit() {
const username = document.getElementById('username').value;
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;

// 验证用户名
if (!username || username.length < 3 || username.length > 20) {
    alert('用户名必须是3-20个字符');
    return;
  }

// 验证邮箱
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    alert('请输入有效的邮箱地址');
    return;
  }

// 验证密码
if (!password || password.length < 8) {
    alert('密码至少8位');
    return;
  }

// ... 更多验证代码
// 实际业务逻辑被埋没在验证代码中
}

这种方式的问题:

  • 验证逻辑和业务逻辑混合,难以维护
  • 错误提示方式不一致
  • 无法复用验证规则
  • 代码重复,容易出错

痛点二:用户体验差

代码语言:javascript
代码运行次数:0
运行
复制
// 问题:只能在提交时验证,用户体验差
document.getElementById('form').addEventListener('submit', function(e) {
  e.preventDefault();
  
  // 用户填写完整个表单后才知道有错误
  const errors = validateForm();
  if (errors.length > 0) {
    alert('表单有错误:' + errors.join(', '));
    // 用户需要自己找错误在哪里
  }
});

痛点三:验证规则不灵活

代码语言:javascript
代码运行次数:0
运行
复制
// 问题:硬编码的验证规则,难以扩展
function validatePassword(password) {
// 规则写死在代码中
if (password.length < 8) returnfalse;
if (!/[A-Z]/.test(password)) returnfalse;
if (!/[a-z]/.test(password)) returnfalse;
if (!/\d/.test(password)) returnfalse;

// 如果需要新的规则,必须修改代码
return true;
}

痛点四:错误处理不统一

代码语言:javascript
代码运行次数:0
运行
复制
// 问题:错误显示方式各不相同
function showUsernameError() {
document.getElementById('username-error').textContent = '用户名无效';
}

function showEmailError() {
  alert('邮箱格式错误'); // 用alert
}

function showPasswordError() {
document.getElementById('password').style.borderColor = 'red'; // 只改颜色
}

// 用户得到的反馈不一致,体验混乱

我们的表单验证引擎

现在让我们来实现一个功能完备的表单验证引擎:

代码语言:javascript
代码运行次数:0
运行
复制
class FormValidator {
constructor(options = {}) {
    this.options = {
      validateOnInput: true,        // 输入时实时验证
      validateOnBlur: true,         // 失去焦点时验证
      showErrorsImmediately: false, // 是否立即显示错误
      errorClass: 'error',          // 错误状态CSS类
      validClass: 'valid',          // 有效状态CSS类
      errorContainer: null,         // 错误信息容器
      scrollToError: true,          // 是否滚动到第一个错误
      ...options
    };
    
    // 验证器存储
    this.validators = newMap();
    this.customRules = newMap();
    this.errors = newMap();
    this.asyncValidators = newMap();
    
    // 初始化内置验证规则
    this.initializeBuiltInRules();
  }

// 初始化内置验证规则
  initializeBuiltInRules() {
    // 必填验证
    this.addRule('required', (value) => {
      if (Array.isArray(value)) return value.length > 0;
      if (typeof value === 'boolean') return value;
      return value !== null && value !== undefined && String(value).trim() !== '';
    }, '此字段为必填项');
    
    // 邮箱验证
    this.addRule('email', (value) => {
      if (!value) returntrue; // 空值通过,由required规则处理
      const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
      return emailRegex.test(value);
    }, '请输入有效的邮箱地址');
    
    // 最小长度/值
    this.addRule('min', (value, min) => {
      if (!value && value !== 0) returntrue;
      if (typeof value === 'string') return value.length >= min;
      if (typeof value === 'number') return value >= min;
      if (Array.isArray(value)) return value.length >= min;
      return true;
    }, '输入值过短或过小');
    
    // 最大长度/值
    this.addRule('max', (value, max) => {
      if (!value && value !== 0) returntrue;
      if (typeof value === 'string') return value.length <= max;
      if (typeof value === 'number') return value <= max;
      if (Array.isArray(value)) return value.length <= max;
      return true;
    }, '输入值过长或过大');
    
    // 正则模式验证
    this.addRule('pattern', (value, pattern) => {
      if (!value) returntrue;
      const regex = newRegExp(pattern);
      return regex.test(value);
    }, '格式不正确');
    
    // 数字验证
    this.addRule('numeric', (value) => {
      if (!value) returntrue;
      return/^\d+$/.test(value);
    }, '只能输入数字');
    
    // 字母验证
    this.addRule('alpha', (value) => {
      if (!value) returntrue;
      return/^[a-zA-Z]+$/.test(value);
    }, '只能输入字母');
    
    // 字母数字验证
    this.addRule('alphanumeric', (value) => {
      if (!value) returntrue;
      return/^[a-zA-Z0-9]+$/.test(value);
    }, '只能输入字母和数字');
    
    // URL验证
    this.addRule('url', (value) => {
      if (!value) returntrue;
      try {
        new URL(value);
        returntrue;
      } catch {
        return false;
      }
    }, '请输入有效的URL地址');
    
    // 中国手机号验证
    this.addRule('mobile', (value) => {
      if (!value) returntrue;
      const mobileRegex = /^1[3-9]\d{9}$/;
      return mobileRegex.test(value.replace(/[\s\-]/g, ''));
    }, '请输入有效的手机号码');
    
    // 身份证号验证
    this.addRule('idcard', (value) => {
      if (!value) returntrue;
      returnthis.validateIdCard(value);
    }, '请输入有效的身份证号码');
    
    // 日期验证
    this.addRule('date', (value) => {
      if (!value) returntrue;
      const date = newDate(value);
      return date instanceofDate && !isNaN(date.getTime());
    }, '请输入有效的日期');
    
    // 日期范围验证
    this.addRule('dateRange', (value, startDate, endDate) => {
      if (!value) returntrue;
      const date = newDate(value);
      const start = startDate ? newDate(startDate) : null;
      const end = endDate ? newDate(endDate) : null;
      
      if (start && date < start) returnfalse;
      if (end && date > end) returnfalse;
      return true;
    }, '日期超出允许范围');
  }

// 添加自定义验证规则
  addRule(name, validator, defaultMessage) {
    this.customRules.set(name, { validator, defaultMessage });
    returnthis;
  }

// 添加异步验证规则
  addAsyncRule(name, asyncValidator, defaultMessage) {
    this.asyncValidators.set(name, { validator: asyncValidator, defaultMessage });
    return this;
  }

// 验证单个字段
async validateField(element, rules) {
    const value = this.getFieldValue(element);
    const fieldName = element.name || element.id;
    const fieldErrors = [];
    
    // 同步验证
    for (const rule of rules) {
      const { name, params = [], message } = this.parseRule(rule);
      
      // 检查是否是异步规则
      if (this.asyncValidators.has(name)) {
        continue; // 异步规则稍后处理
      }
      
      const ruleConfig = this.customRules.get(name);
      
      if (!ruleConfig) {
        console.warn(`未知的验证规则: ${name}`);
        continue;
      }
      
      const isValid = ruleConfig.validator(value, ...params);
      
      if (!isValid) {
        fieldErrors.push(message || ruleConfig.defaultMessage);
        break; // 遇到第一个错误就停止
      }
    }
    
    // 如果同步验证通过,执行异步验证
    if (fieldErrors.length === 0) {
      for (const rule of rules) {
        const { name, params = [], message } = this.parseRule(rule);
        
        if (this.asyncValidators.has(name)) {
          const ruleConfig = this.asyncValidators.get(name);
          
          try {
            const isValid = await ruleConfig.validator(value, ...params);
            
            if (!isValid) {
              fieldErrors.push(message || ruleConfig.defaultMessage);
              break;
            }
          } catch (error) {
            console.error(`异步验证失败 (${name}):`, error);
            fieldErrors.push('验证过程中发生错误');
            break;
          }
        }
      }
    }
    
    // 更新错误状态
    if (fieldErrors.length > 0) {
      this.errors.set(fieldName, fieldErrors);
      this.showFieldError(element, fieldErrors[0]);
      return false;
    } else {
      this.errors.delete(fieldName);
      this.showFieldSuccess(element);
      return true;
    }
  }

// 验证整个表单
async validateForm(form) {
    const elements = this.getFormElements(form);
    let isValid = true;
    const validationPromises = [];
    
    // 并行执行所有字段验证
    elements.forEach(element => {
      const rules = this.getElementRules(element);
      if (rules.length > 0) {
        validationPromises.push(
          this.validateField(element, rules).then(fieldValid => {
            if (!fieldValid) isValid = false;
            return { element, valid: fieldValid };
          })
        );
      }
    });
    
    // 等待所有验证完成
    const results = awaitPromise.all(validationPromises);
    
    // 滚动到第一个错误
    if (!isValid && this.options.scrollToError) {
      const firstError = results.find(result => !result.valid);
      if (firstError) {
        firstError.element.scrollIntoView({ 
          behavior: 'smooth', 
          block: 'center'
        });
        firstError.element.focus();
      }
    }
    
    return isValid;
  }

// 获取表单元素的值
  getFieldValue(element) {
    switch (element.type) {
      case'checkbox':
        return element.checked;
        
      case'radio':
        const radioGroup = document.querySelectorAll(`input[name="${element.name}"]`);
        const checked = Array.from(radioGroup).find(r => r.checked);
        return checked ? checked.value : '';
        
      case'select-multiple':
        returnArray.from(element.selectedOptions).map(opt => opt.value);
        
      case'file':
        return element.files;
        
      case'number':
        return element.value ? Number(element.value) : '';
        
      default:
        return element.value;
    }
  }

// 获取表单中的所有可验证元素
  getFormElements(form) {
    returnArray.from(form.querySelectorAll('input, select, textarea'))
      .filter(el => {
        return !el.disabled && 
               el.type !== 'submit' && 
               el.type !== 'button' && 
               el.type !== 'reset' &&
               !el.hasAttribute('data-no-validate');
      });
  }

// 获取元素的验证规则
  getElementRules(element) {
    const rulesAttr = element.getAttribute('data-rules');
    if (!rulesAttr) return [];
    
    return rulesAttr.split('|').filter(rule => rule.trim()).map(rule => rule.trim());
  }

// 解析验证规则字符串
  parseRule(ruleString) {
    // 支持格式:ruleName:param1,param2|[自定义错误消息]
    const messageMatch = ruleString.match(/^(.+?)\[(.+)\]$/);
    let rule = ruleString;
    let customMessage = null;
    
    if (messageMatch) {
      rule = messageMatch[1];
      customMessage = messageMatch[2];
    }
    
    const [name, ...paramsPart] = rule.split(':');
    const params = paramsPart.length > 0 ? 
      paramsPart[0].split(',').map(p => {
        const trimmed = p.trim();
        // 尝试转换为数字
        if (!isNaN(trimmed) && trimmed !== '') returnNumber(trimmed);
        // 布尔值转换
        if (trimmed === 'true') returntrue;
        if (trimmed === 'false') returnfalse;
        return trimmed;
      }) : [];
    
    return {
      name: name.trim(),
      params,
      message: customMessage
    };
  }

// 显示字段错误
  showFieldError(element, message) {
    // 更新元素样式
    element.classList.remove(this.options.validClass);
    element.classList.add(this.options.errorClass);
    
    // 显示错误消息
    this.updateErrorMessage(element, message);
    
    // 设置aria属性,提升无障碍体验
    element.setAttribute('aria-invalid', 'true');
    element.setAttribute('aria-describedby', this.getErrorId(element));
  }

// 显示字段成功状态
  showFieldSuccess(element) {
    element.classList.remove(this.options.errorClass);
    element.classList.add(this.options.validClass);
    
    this.clearErrorMessage(element);
    
    element.removeAttribute('aria-invalid');
    element.removeAttribute('aria-describedby');
  }

// 更新错误消息
  updateErrorMessage(element, message) {
    const errorId = this.getErrorId(element);
    let errorElement = document.getElementById(errorId);
    
    if (!errorElement) {
      errorElement = document.createElement('div');
      errorElement.id = errorId;
      errorElement.className = 'validation-error';
      errorElement.setAttribute('role', 'alert');
      errorElement.setAttribute('aria-live', 'polite');
      
      // 寻找合适的插入位置
      const insertPoint = this.findErrorInsertPoint(element);
      insertPoint.parentNode.insertBefore(errorElement, insertPoint.nextSibling);
    }
    
    errorElement.textContent = message;
    errorElement.style.display = 'block';
  }

// 清除错误消息
  clearErrorMessage(element) {
    const errorId = this.getErrorId(element);
    const errorElement = document.getElementById(errorId);
    
    if (errorElement) {
      errorElement.style.display = 'none';
    }
  }

// 获取错误元素ID
  getErrorId(element) {
    return`${element.id || element.name}-error`;
  }

// 寻找错误消息插入点
  findErrorInsertPoint(element) {
    // 优先寻找包装容器
    const wrapper = element.closest('.form-group, .form-field, .input-group');
    return wrapper || element;
  }

// 绑定表单验证
  bindToForm(form, options = {}) {
    const formOptions = { ...this.options, ...options };
    const elements = this.getFormElements(form);
    
    // 为每个元素绑定事件
    elements.forEach(element => {
      // 实时验证(输入时)
      if (formOptions.validateOnInput) {
        const inputHandler = this.debounce(async () => {
          if (formOptions.showErrorsImmediately || this.errors.has(element.name || element.id)) {
            const rules = this.getElementRules(element);
            if (rules.length > 0) {
              awaitthis.validateField(element, rules);
            }
          }
        }, 300);
        
        element.addEventListener('input', inputHandler);
        element.addEventListener('change', inputHandler);
      }
      
      // 失焦验证
      if (formOptions.validateOnBlur) {
        element.addEventListener('blur', async () => {
          const rules = this.getElementRules(element);
          if (rules.length > 0) {
            awaitthis.validateField(element, rules);
          }
        });
      }
    });
    
    // 表单提交事件
    form.addEventListener('submit', async (e) => {
      e.preventDefault();
      
      const isValid = awaitthis.validateForm(form);
      
      if (isValid) {
        // 表单验证通过
        const formData = this.getFormData(form);
        
        if (formOptions.onSuccess) {
          await formOptions.onSuccess(formData, form);
        } else {
          console.log('表单验证通过,数据:', formData);
        }
      } else {
        // 表单验证失败
        if (formOptions.onError) {
          formOptions.onError(this.getErrors(), form);
        } else {
          console.log('表单验证失败,错误:', this.getErrors());
        }
      }
    });
    
    returnthis;
  }

// 获取表单数据
  getFormData(form) {
    const formData = new FormData(form);
    const data = {};
    
    // 处理普通字段
    for (const [key, value] of formData.entries()) {
      if (data[key]) {
        // 处理多选字段
        if (Array.isArray(data[key])) {
          data[key].push(value);
        } else {
          data[key] = [data[key], value];
        }
      } else {
        data[key] = value;
      }
    }
    
    // 处理checkbox(未选中的不会出现在FormData中)
    form.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
      if (!checkbox.name) return;
      
      if (!data.hasOwnProperty(checkbox.name)) {
        data[checkbox.name] = checkbox.checked;
      }
    });
    
    return data;
  }

// 获取所有验证错误
  getErrors() {
    returnObject.fromEntries(this.errors);
  }

// 检查是否有错误
  hasErrors() {
    returnthis.errors.size > 0;
  }

// 清除所有错误
  clearErrors() {
    this.errors.clear();
    document.querySelectorAll('.validation-error').forEach(el => {
      el.style.display = 'none';
    });
    
    document.querySelectorAll(`.${this.options.errorClass}`).forEach(el => {
      el.classList.remove(this.options.errorClass);
    });
  }

// 身份证验证算法
  validateIdCard(idCard) {
    const id = idCard.toString();
    
    // 15位身份证
    if (id.length === 15) {
      return/^\d{15}$/.test(id);
    }
    
    // 18位身份证
    if (id.length === 18) {
      if (!/^\d{17}[\dXx]$/.test(id)) returnfalse;
      
      // 验证校验位
      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(id[i]) * weights[i];
      }
      
      const expectedCheck = checkCodes[sum % 11];
      return id[17].toUpperCase() === expectedCheck;
    }
    
    returnfalse;
  }

// 防抖函数
  debounce(func, wait) {
    let timeout;
    returnfunction executedFunction(...args) {
      const later = () => {
        clearTimeout(timeout);
        func(...args);
      };
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
    };
  }
}

基础功能展示

让我们看看这个验证引擎的基本使用:

代码语言:javascript
代码运行次数:0
运行
复制
// 创建验证器实例
const validator = new FormValidator({
validateOnInput: true,
validateOnBlur: true,
showErrorsImmediately: false,
scrollToError: true
});

// 添加自定义验证规则
validator.addRule('strongPassword', (value) => {
if (!value) returntrue;

const hasUpper = /[A-Z]/.test(value);
const hasLower = /[a-z]/.test(value);
const hasNumber = /\d/.test(value);
const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>?]/.test(value);
const isLongEnough = value.length >= 8;

return hasUpper && hasLower && hasNumber && hasSpecial && isLongEnough;
}, '密码必须包含大小写字母、数字和特殊字符,且不少于8位');

// 密码确认验证
validator.addRule('confirmPassword', (value, originalFieldName) => {
const originalField = document.querySelector(`[name="${originalFieldName}"]`);
return originalField ? value === originalField.value : false;
}, '两次输入的密码不一致');

// 异步用户名验证(检查是否已存在)
validator.addAsyncRule('uniqueUsername', async (username) => {
if (!username) returntrue;

try {
    const response = await fetch('/api/check-username', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username })
    });
    
    const result = await response.json();
    return result.available;
  } catch (error) {
    console.error('用户名验证失败:', error);
    returnfalse;
  }
}, '用户名已被使用');

// 绑定到表单
const registrationForm = document.getElementById('registration-form');
validator.bindToForm(registrationForm, {
onSuccess: async (data, form) => {
    console.log('注册数据验证通过:', data);
    
    // 显示loading状态
    const submitButton = form.querySelector('button[type="submit"]');
    const originalText = submitButton.textContent;
    submitButton.textContent = '注册中...';
    submitButton.disabled = true;
    
    try {
      // 提交注册数据
      const response = await fetch('/api/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      });
      
      if (response.ok) {
        alert('注册成功!');
        form.reset();
      } else {
        const error = await response.json();
        alert('注册失败: ' + error.message);
      }
    } catch (error) {
      alert('网络错误,请重试');
    } finally {
      submitButton.textContent = originalText;
      submitButton.disabled = false;
    }
  },

onError: (errors, form) => {
    console.log('表单验证失败:', errors);
    
    // 统计错误数量
    const errorCount = Object.keys(errors).length;
    const message = `表单中有${errorCount}个错误,请检查后重新提交`;
    
    // 显示全局错误提示
    const errorContainer = document.getElementById('form-errors');
    if (errorContainer) {
      errorContainer.textContent = message;
      errorContainer.style.display = 'block';
    }
  }
});

对应的HTML结构:

代码语言:javascript
代码运行次数:0
运行
复制
<form id="registration-form" novalidate>
  <div id="form-errors" class="form-errors" style="display: none;"></div>

<div class="form-group">
    <label for="username">用户名</label>
    <input 
      type="text" 
      id="username" 
      name="username" 
      data-rules="required|min:3|max:20|alphanumeric|uniqueUsername"
      placeholder="请输入用户名"
    />
</div>

<div class="form-group">
    <label for="email">邮箱</label>
    <input 
      type="email" 
      id="email" 
      name="email" 
      data-rules="required|email"
      placeholder="请输入邮箱地址"
    />
</div>

<div class="form-group">
    <label for="mobile">手机号</label>
    <input 
      type="tel" 
      id="mobile" 
      name="mobile" 
      data-rules="required|mobile"
      placeholder="请输入手机号"
    />
</div>

<div class="form-group">
    <label for="password">密码</label>
    <input 
      type="password" 
      id="password" 
      name="password" 
      data-rules="required|strongPassword"
      placeholder="请输入密码"
    />
</div>

<div class="form-group">
    <label for="confirmPassword">确认密码</label>
    <input 
      type="password" 
      id="confirmPassword" 
      name="confirmPassword" 
      data-rules="required|confirmPassword:password"
      placeholder="请再次输入密码"
    />
</div>

<div class="form-group">
    <label for="birthdate">出生日期</label>
    <input 
      type="date" 
      id="birthdate" 
      name="birthdate" 
      data-rules="required|date|dateRange:1900-01-01,2010-12-31[年龄必须在14-124岁之间]"
    />
</div>

<div class="form-group">
    <label for="website">个人网站</label>
    <input 
      type="url" 
      id="website" 
      name="website" 
      data-rules="url"
      placeholder="http://example.com(可选)"
    />
</div>

<div class="form-group">
    <label>
      <input 
        type="checkbox" 
        name="agree" 
        data-rules="required[您必须同意用户协议]"
      />
      我已阅读并同意用户协议
    </label>
</div>

<button type="submit">注册</button>
</form>

配套的CSS样式:

代码语言:javascript
代码运行次数:0
运行
复制
.form-group {
margin-bottom: 20px;
}

.form-grouplabel {
display: block;
margin-bottom: 5px;
font-weight: bold;
}

.form-groupinput,
.form-groupselect,
.form-grouptextarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}

.form-groupinput.error {
border-color: #e74c3c;
background-color: #fdf2f2;
}

.form-groupinput.valid {
border-color: #27ae60;
background-color: #f2fff2;
}

.validation-error {
color: #e74c3c;
font-size: 12px;
margin-top: 5px;
display: none;
}

.form-errors {
background-color: #fdf2f2;
color: #e74c3c;
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
border-left: 4px solid #e74c3c;
}

实际项目应用示例

1. 企业员工信息管理系统

代码语言:javascript
代码运行次数:0
运行
复制
class EmployeeFormValidator extends FormValidator {
constructor(options = {}) {
    super(options);
    
    // 添加企业特定的验证规则
    this.initializeEnterpriseRules();
  }

  initializeEnterpriseRules() {
    // 员工工号验证
    this.addRule('employeeId', (value) => {
      if (!value) returntrue;
      // 工号格式:EMP + 6位数字
      return/^EMP\d{6}$/.test(value);
    }, '工号格式错误(EMP + 6位数字)');
    
    // 部门代码验证
    this.addRule('departmentCode', (value, validCodes) => {
      if (!value) returntrue;
      const codes = validCodes.split(',');
      return codes.includes(value);
    }, '无效的部门代码');
    
    // 薪资范围验证
    this.addRule('salaryRange', (value, minSalary, maxSalary) => {
      if (!value) returntrue;
      const salary = Number(value);
      return salary >= minSalary && salary <= maxSalary;
    }, '薪资超出允许范围');
    
    // 入职日期验证
    this.addRule('hireDate', (value) => {
      if (!value) returntrue;
      
      const hireDate = newDate(value);
      const companyFoundingDate = newDate('2010-01-01');
      const today = newDate();
      
      return hireDate >= companyFoundingDate && hireDate <= today;
    }, '入职日期必须在公司成立日期之后且不晚于今天');
    
    // 异步验证:检查工号唯一性
    this.addAsyncRule('uniqueEmployeeId', async (employeeId) => {
      if (!employeeId) returntrue;
      
      try {
        const response = await fetch('/api/employees/check-id', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ employeeId })
        });
        
        const result = await response.json();
        return result.available;
      } catch (error) {
        console.error('工号验证失败:', error);
        returnfalse;
      }
    }, '工号已存在');
    
    // 异步验证:身份证号查重
    this.addAsyncRule('uniqueIdCard', async (idCard) => {
      if (!idCard) returntrue;
      
      try {
        const response = await fetch('/api/employees/check-idcard', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ idCard })
        });
        
        const result = await response.json();
        return result.available;
      } catch (error) {
        console.error('身份证验证失败:', error);
        returnfalse;
      }
    }, '该身份证号已登记');
  }
}

// 使用企业验证器
const employeeValidator = new EmployeeFormValidator({
validateOnInput: true,
validateOnBlur: true,
scrollToError: true
});

// 绑定员工信息表单
const employeeForm = document.getElementById('employee-form');
employeeValidator.bindToForm(employeeForm, {
onSuccess: async (data, form) => {
    console.log('员工信息验证通过:', data);
    
    // 显示确认对话框
    const confirmMessage = `
      请确认员工信息:
      姓名:${data.name}
      工号:${data.employeeId}
      部门:${data.department}
      职位:${data.position}
      入职日期:${data.hireDate}
    `;
    
    if (confirm(confirmMessage)) {
      try {
        const response = await fetch('/api/employees', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data)
        });
        
        if (response.ok) {
          const result = await response.json();
          alert(`员工信息保存成功!员工ID:${result.id}`);
          form.reset();
          // 清除验证状态
          employeeValidator.clearErrors();
        } else {
          const error = await response.json();
          alert('保存失败: ' + error.message);
        }
      } catch (error) {
        console.error('保存员工信息失败:', error);
        alert('网络错误,请重试');
      }
    }
  },

onError: (errors) => {
    console.log('员工信息验证失败:', errors);
    
    // 生成错误摘要
    const errorSummary = Object.entries(errors).map(([field, messages]) => {
      const fieldLabel = document.querySelector(`[name="${field}"]`)
        ?.previousElementSibling?.textContent || field;
      return`${fieldLabel}: ${messages[0]}`;
    }).join('\n');
    
    // 显示错误摘要
    const summaryElement = document.getElementById('validation-summary');
    if (summaryElement) {
      summaryElement.innerHTML = `
        <h4>请修正以下错误:</h4>
        <ul>
          ${Object.entries(errors).map(([field, messages]) => {
            const fieldLabel = document.querySelector(`[name="${field}"]`)
              ?.previousElementSibling?.textContent || field;
            return `<li>${fieldLabel}: ${messages[0]}</li>`;
          }).join('')}
        </ul>
      `;
      summaryElement.style.display = 'block';
    }
  }
});

2. 动态表单验证系统

代码语言:javascript
代码运行次数:0
运行
复制
class DynamicFormValidator extends FormValidator {
constructor(options = {}) {
    super(options);
    
    // 条件验证规则映射
    this.conditionalRules = newMap();
    
    // 字段依赖关系
    this.fieldDependencies = newMap();
  }

// 添加条件验证规则
  addConditionalRule(fieldName, condition, rules) {
    if (!this.conditionalRules.has(fieldName)) {
      this.conditionalRules.set(fieldName, []);
    }
    
    this.conditionalRules.get(fieldName).push({
      condition,
      rules
    });
  }

// 添加字段依赖
  addFieldDependency(fieldName, dependsOn) {
    this.fieldDependencies.set(fieldName, dependsOn);
  }

// 重写validateField方法,支持条件验证
async validateField(element, rules) {
    const fieldName = element.name || element.id;
    
    // 检查条件验证规则
    if (this.conditionalRules.has(fieldName)) {
      const conditionalRules = this.conditionalRules.get(fieldName);
      
      for (const condRule of conditionalRules) {
        if (condRule.condition()) {
          // 条件满足,添加额外验证规则
          rules = [...rules, ...condRule.rules];
        }
      }
    }
    
    // 检查字段依赖
    if (this.fieldDependencies.has(fieldName)) {
      const dependsOn = this.fieldDependencies.get(fieldName);
      const dependentField = document.querySelector(`[name="${dependsOn}"]`);
      
      if (dependentField && !dependentField.value) {
        // 依赖字段未填写,跳过验证
        returntrue;
      }
    }
    
    returnsuper.validateField(element, rules);
  }

// 字段值变化时,重新验证依赖字段
  bindToForm(form, options = {}) {
    super.bindToForm(form, options);
    
    // 监听字段变化,处理依赖验证
    const elements = this.getFormElements(form);
    
    elements.forEach(element => {
      element.addEventListener('change', () => {
        this.onFieldChange(element, form);
      });
    });
    
    returnthis;
  }

// 字段值变化处理
async onFieldChange(changedElement, form) {
    const changedFieldName = changedElement.name || changedElement.id;
    
    // 查找依赖于此字段的其他字段
    for (const [fieldName, dependsOn] ofthis.fieldDependencies.entries()) {
      if (dependsOn === changedFieldName) {
        const dependentField = form.querySelector(`[name="${fieldName}"]`);
        if (dependentField) {
          const rules = this.getElementRules(dependentField);
          if (rules.length > 0) {
            awaitthis.validateField(dependentField, rules);
          }
        }
      }
    }
    
    // 处理条件验证规则
    for (const [fieldName] ofthis.conditionalRules.keys()) {
      if (fieldName !== changedFieldName) {
        const field = form.querySelector(`[name="${fieldName}"]`);
        if (field) {
          const rules = this.getElementRules(field);
          if (rules.length > 0) {
            awaitthis.validateField(field, rules);
          }
        }
      }
    }
  }

// 显示/隐藏字段
  showField(fieldName, show = true) {
    const field = document.querySelector(`[name="${fieldName}"]`);
    const fieldGroup = field?.closest('.form-group');
    
    if (fieldGroup) {
      fieldGroup.style.display = show ? 'block' : 'none';
      
      if (!show) {
        // 隐藏时清除验证错误
        this.errors.delete(fieldName);
        this.clearErrorMessage(field);
        field.classList.remove(this.options.errorClass, this.options.validClass);
      }
    }
  }

// 动态添加字段
  addField(fieldConfig) {
    const { name, type, label, rules, parent } = fieldConfig;
    const parentElement = document.querySelector(parent);
    
    if (!parentElement) {
      console.error(`父元素 ${parent} 不存在`);
      return;
    }
    
    const fieldGroup = document.createElement('div');
    fieldGroup.className = 'form-group dynamic-field';
    fieldGroup.innerHTML = `
      <label for="${name}">${label}</label>
      <input type="${type}" id="${name}" name="${name}" data-rules="${rules}">
    `;
    
    parentElement.appendChild(fieldGroup);
    
    // 为新字段绑定验证事件
    const newField = fieldGroup.querySelector(`[name="${name}"]`);
    this.bindFieldEvents(newField);
  }

// 为单个字段绑定验证事件
  bindFieldEvents(element) {
    if (this.options.validateOnInput) {
      const inputHandler = this.debounce(async () => {
        if (this.options.showErrorsImmediately || this.errors.has(element.name || element.id)) {
          const rules = this.getElementRules(element);
          if (rules.length > 0) {
            awaitthis.validateField(element, rules);
          }
        }
      }, 300);
      
      element.addEventListener('input', inputHandler);
      element.addEventListener('change', inputHandler);
    }
    
    if (this.options.validateOnBlur) {
      element.addEventListener('blur', async () => {
        const rules = this.getElementRules(element);
        if (rules.length > 0) {
          awaitthis.validateField(element, rules);
        }
      });
    }
    
    // 字段变化处理
    element.addEventListener('change', () => {
      this.onFieldChange(element, element.form);
    });
  }
}

// 使用动态表单验证器
const dynamicValidator = new DynamicFormValidator({
validateOnInput: true,
validateOnBlur: true
});

// 设置条件验证规则
// 当选择"企业发票"时,税号字段变为必填
dynamicValidator.addConditionalRule('taxNumber', 
  () => {
    const invoiceType = document.querySelector('[name="invoiceType"]')?.value;
    return invoiceType === 'enterprise';
  },
  ['required']
);

// 当选择"需要发票"时,显示发票相关字段
document.querySelector('[name="needInvoice"]')?.addEventListener('change', function() {
const needInvoice = this.checked;

  dynamicValidator.showField('invoiceType', needInvoice);
  dynamicValidator.showField('invoiceTitle', needInvoice);
  dynamicValidator.showField('taxNumber', needInvoice);

if (!needInvoice) {
    // 清空发票相关字段
    ['invoiceType', 'invoiceTitle', 'taxNumber'].forEach(fieldName => {
      const field = document.querySelector(`[name="${fieldName}"]`);
      if (field) field.value = '';
    });
  }
});

// 设置字段依赖:税号依赖于发票类型
dynamicValidator.addFieldDependency('taxNumber', 'invoiceType');

// 绑定表单
const orderForm = document.getElementById('order-form');
dynamicValidator.bindToForm(orderForm, {
onSuccess: async (data) => {
    console.log('订单数据验证通过:', data);
    // 处理订单提交逻辑
  }
});

总结

通过我们自制的表单验证引擎,我们实现了:

核心优势:

  • 声明式验证:通过HTML data属性配置验证规则,保持结构与逻辑分离
  • 实时反馈:支持输入时和失焦时的实时验证,提供即时用户反馈
  • 灵活扩展:支持自定义验证规则和异步验证,满足复杂业务需求
  • 无障碍支持:遵循ARIA标准,提供良好的屏幕阅读器支持

强大功能:

  • 内置20+常用验证规则,覆盖大部分场景
  • 支持同步和异步验证规则
  • 智能错误提示定位和样式管理
  • 条件验证和字段依赖管理
  • 完整的表单数据处理和错误统计

实际应用场景:

  • 用户注册登录:用户名、邮箱、密码等基础验证
  • 企业管理系统:员工信息、业务数据的复杂验证
  • 电商订单:地址、支付、发票信息的动态验证
  • 数据录入:各种业务表单的数据质量控制

用户体验保障:

  • 友好的错误提示和成功反馈
  • 智能的错误定位和滚动
  • 防抖处理避免频繁验证
  • 完整的加载状态和错误恢复

这个表单验证引擎不仅解决了数据验证的技术需求,更重要的是提供了优秀的用户体验和开发者体验。无论是简单的联系表单还是复杂的业务系统,都能提供稳定可靠的验证保障。

掌握了这个工具,你就能构建真正用户友好、数据可靠的表单系统,让数据质量从源头得到保证!


《JavaScript原生实战手册》专栏持续更新中,下期预告:《DOM操作工具库:现代化的元素操作方案》

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-08-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 生活中的表单验证场景
    • 场景一:用户注册系统
    • 场景二:电商订单表单
    • 场景三:企业数据录入系统
  • 传统验证方式的痛点
    • 痛点一:验证逻辑散落各处
    • 痛点二:用户体验差
    • 痛点三:验证规则不灵活
    • 痛点四:错误处理不统一
  • 我们的表单验证引擎
  • 基础功能展示
  • 实际项目应用示例
    • 1. 企业员工信息管理系统
    • 2. 动态表单验证系统
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档