用户提交了错误数据就让整个系统崩溃?一个强大的表单验证引擎让你的数据入口固若金汤!
表单验证是Web应用中最基础也是最重要的功能之一。无论是用户注册、数据录入、订单提交还是内容发布,都需要确保用户输入的数据符合预期格式和业务规则。糟糕的表单验证不仅会让用户感到沫名,还可能导致脏数据进入系统,引发安全问题或业务逻辑错误。今天我们就来打造一个功能完备、灵活可扩展的表单验证引擎。
想象你在开发一个社交媒体的注册页面:
用户名:必须3-20个字符,只能包含字母数字
邮箱:必须是有效的邮箱格式
密码:至少8位,包含大小写字母、数字和特殊字符
确认密码:必须与密码完全一致
手机号:符合国际手机号格式
个人网站:可选,但如果填写必须是有效URL
如果没有好的验证系统:
在电商网站的结算页面:
收货地址:必填,长度限制
联系电话:必须是有效手机号
优惠码:可选,但需要验证有效性和使用条件
支付方式:必选
发票信息:企业发票需要填写税号,个人发票可选
这种场景需要:
在企业管理系统中录入员工信息:
员工工号:唯一性验证,格式检查
身份证号:15位或18位,校验位算法验证
入职日期:不能早于公司成立日期,不能晚于今天
薪资:数字范围验证,根据职级限制
部门:下拉选择,某些部门需要额外审批
紧急联系人:必须填写且不能是员工本人
这类系统对数据准确性要求极高,验证规则复杂,需要支持自定义业务规则。
// 传统方式:验证代码混在业务逻辑中
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;
}
// ... 更多验证代码
// 实际业务逻辑被埋没在验证代码中
}
这种方式的问题:
// 问题:只能在提交时验证,用户体验差
document.getElementById('form').addEventListener('submit', function(e) {
e.preventDefault();
// 用户填写完整个表单后才知道有错误
const errors = validateForm();
if (errors.length > 0) {
alert('表单有错误:' + errors.join(', '));
// 用户需要自己找错误在哪里
}
});
// 问题:硬编码的验证规则,难以扩展
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;
}
// 问题:错误显示方式各不相同
function showUsernameError() {
document.getElementById('username-error').textContent = '用户名无效';
}
function showEmailError() {
alert('邮箱格式错误'); // 用alert
}
function showPasswordError() {
document.getElementById('password').style.borderColor = 'red'; // 只改颜色
}
// 用户得到的反馈不一致,体验混乱
现在让我们来实现一个功能完备的表单验证引擎:
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);
};
}
}
让我们看看这个验证引擎的基本使用:
// 创建验证器实例
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结构:
<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样式:
.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;
}
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';
}
}
});
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);
// 处理订单提交逻辑
}
});
通过我们自制的表单验证引擎,我们实现了:
核心优势:
强大功能:
实际应用场景:
用户体验保障:
这个表单验证引擎不仅解决了数据验证的技术需求,更重要的是提供了优秀的用户体验和开发者体验。无论是简单的联系表单还是复杂的业务系统,都能提供稳定可靠的验证保障。
掌握了这个工具,你就能构建真正用户友好、数据可靠的表单系统,让数据质量从源头得到保证!
《JavaScript原生实战手册》专栏持续更新中,下期预告:《DOM操作工具库:现代化的元素操作方案》