表单验证是前端开发的高频刚需场景 —— 登录、注册、支付、信息提交等场景都离不开它。市面上的 UI 库虽有现成组件,但理解底层实现逻辑,才能灵活应对复杂业务需求。本文将手把手带你封装一个轻量、可扩展、无依赖的原生 JS 表单验证组件,覆盖正则校验、自定义规则、实时反馈、错误提示优化等核心能力,新手可直接复刻,进阶开发者可无缝集成到项目中!
一、案例核心目标 🎯
- 掌握表单验证核心逻辑:实时校验、失去焦点校验、提交最终校验;
- 实现通用校验规则:手机号、邮箱、密码强度、身份证、非空、长度限制等;
- 支持自定义校验规则:适配个性化业务场景(如 “邀请码格式”“新旧密码一致”);
- 优化用户体验:实时错误提示、校验状态动画、提交按钮禁用 / 启用;
- 封装可复用组件:配置化设计,支持多表单复用,低耦合高扩展;
- 兼容处理:适配主流浏览器,支持键盘操作(回车提交)。
二、最终效果预览
✅ 输入框实时校验:输入过程中即时反馈格式是否正确;
✅ 失去焦点强化校验:离开输入框时显示详细错误提示;
✅ 提交前全量校验:未通过则定位到第一个错误项,阻止提交;
✅ 密码强度实时检测:弱 / 中 / 强分级显示,不同强度对应不同样式;
✅ 自定义规则适配:如 “邀请码必须以 88 开头,长度 8 位”;
✅ 提交按钮智能状态:表单未通过校验时禁用,全部通过后可点击;
✅ 错误提示友好:位置贴合输入框,样式醒目不刺眼,支持自定义文案。
三、核心技术知识点
- 正则表达式:常用表单字段(手机号 / 邮箱 / 身份证)正则编写与优化;
- 事件处理:input/blur/ submit 事件监听,事件委托减少监听数量;
- 组件封装:面向对象(Class)设计,配置化参数,统一调用接口;
- DOM 操作:动态修改样式、插入错误提示、滚动定位错误项;
- 边界处理:空值校验、重复校验拦截、特殊字符处理;
- 体验优化:防抖处理(避免输入时频繁校验)、状态过渡动画。
四、完整代码实现(附详细注释)
1. HTML 结构(语义化 + 易扩展)
html
预览
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能表单验证组件 | 原生JS实现</title>
<style>
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft Yahei", sans-serif;
}
body {
background-color: #f5f7fa;
padding: 50px 0;
}
/* 表单容器 */
.form-container {
width: 500px;
margin: 0 auto;
padding: 30px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
.form-title {
font-size: 20px;
color: #333;
text-align: center;
margin-bottom: 24px;
font-weight: 600;
}
/* 表单项样式 */
.form-item {
margin-bottom: 20px;
position: relative;
}
.form-label {
display: block;
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.form-input {
width: 100%;
height: 40px;
padding: 0 12px;
border: 1px solid #e5e6eb;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-input:focus {
outline: none;
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64,158,255,0.2);
}
/* 校验状态样式 */
.form-input.success {
border-color: #67c23a;
}
.form-input.error {
border-color: #f56c6c;
}
/* 错误提示 */
.error-tip {
position: absolute;
left: 0;
bottom: -20px;
font-size: 12px;
color: #f56c6c;
line-height: 1;
transition: opacity 0.3s;
opacity: 0;
}
.error-tip.show {
opacity: 1;
}
/* 密码强度 */
.password-strength {
margin-top: 8px;
height: 6px;
display: flex;
gap: 4px;
}
.strength-item {
flex: 1;
height: 100%;
border-radius: 3px;
background: #e5e6eb;
transition: background-color 0.3s;
}
.strength-item.weak {
background: #f56c6c;
}
.strength-item.medium {
background: #e6a23c;
}
.strength-item.strong {
background: #67c23a;
}
/* 提交按钮 */
.submit-btn {
width: 100%;
height: 44px;
background: #409eff;
color: #fff;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.submit-btn:disabled {
background: #b3d8ff;
cursor: not-allowed;
}
.submit-btn:not(:disabled):hover {
background: #66b1ff;
}
/* 响应式适配 */
@media (max-width: 520px) {
.form-container {
width: 90%;
padding: 20px;
}
}
</style>
</head>
<body>
<div class="form-container">
<h2 class="form-title">用户注册表单</h2>
<form id="registerForm">
<!– 手机号 –>
<div class="form-item">
<label class="form-label">手机号</label>
<input
type="tel"
class="form-input"
name="phone"
placeholder="请输入手机号"
data-rules="required|phone"
>
<div class="error-tip"></div>
</div>
<!– 邮箱 –>
<div class="form-item">
<label class="form-label">邮箱</label>
<input
type="email"
class="form-input"
name="email"
placeholder="请输入邮箱"
data-rules="required|email"
>
<div class="error-tip"></div>
</div>
<!– 密码 –>
<div class="form-item">
<label class="form-label">密码</label>
<input
type="password"
class="form-input"
name="password"
placeholder="请输入密码(6-16位,含字母+数字)"
data-rules="required|password"
>
<div class="error-tip"></div>
<div class="password-strength">
<div class="strength-item"></div>
<div class="strength-item"></div>
<div class="strength-item"></div>
</div>
</div>
<!– 确认密码 –>
<div class="form-item">
<label class="form-label">确认密码</label>
<input
type="password"
class="form-input"
name="confirmPwd"
placeholder="请再次输入密码"
data-rules="required|confirmPwd"
>
<div class="error-tip"></div>
</div>
<!– 邀请码(自定义规则) –>
<div class="form-item">
<label class="form-label">邀请码(选填,88开头8位数字)</label>
<input
type="text"
class="form-input"
name="inviteCode"
placeholder="请输入邀请码(选填)"
data-rules="inviteCode"
>
<div class="error-tip"></div>
</div>
<!– 提交按钮 –>
<button type="submit" class="submit-btn" disabled>提交注册</button>
</form>
</div>
<script>
// ===================== 表单验证组件核心代码 =====================
class FormValidator {
/**
* 构造函数
* @param {String} formSelector – 表单选择器
* @param {Object} options – 配置项
*/
constructor(formSelector, options = {}) {
this.form = document.querySelector(formSelector);
if (!this.form) throw new Error('表单元素不存在!');
// 默认配置
this.config = {
// 默认校验规则
rules: {
required: {
regex: /.+/,
message: '此项不能为空'
},
phone: {
regex: /^1[3-9]\\d{9}$/,
message: '请输入正确的手机号'
},
email: {
regex: /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$/,
message: '请输入正确的邮箱'
},
password: {
regex: /^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d]{6,16}$/,
message: '密码需6-16位,包含字母和数字'
},
confirmPwd: {
message: '两次密码输入不一致',
// 自定义校验函数
validator: (value, form) => {
return value === form.querySelector('[name="password"]').value;
}
},
inviteCode: {
regex: /^(88\\d{6})?$/, // 88开头8位数字,或空
message: '邀请码需以88开头,且为8位数字'
}
},
// 自定义提示文案(覆盖默认)
customMessages: options.customMessages || {},
// 防抖时间(实时校验)
debounceTime: options.debounceTime || 300,
…options
};
// 缓存已校验的表单项状态
this.validateStatus = new Map();
// 防抖定时器
this.debounceTimer = null;
// 初始化
this.init();
}
/**
* 初始化:绑定事件
*/
init() {
// 1. 实时校验(input事件)
this.form.addEventListener('input', (e) => {
const target = e.target;
if (target.classList.contains('form-input')) {
this.debounce(() => {
this.validateItem(target);
this.checkAllStatus();
}, this.config.debounceTime)();
// 密码强度单独处理
if (target.name === 'password') {
this.checkPasswordStrength(target.value);
}
}
});
// 2. 失去焦点校验(blur事件)
this.form.addEventListener('blur', (e) => {
const target = e.target;
if (target.classList.contains('form-input')) {
this.validateItem(target, true);
}
}, true); // 捕获阶段触发,避免事件冒泡问题
// 3. 表单提交校验
this.form.addEventListener('submit', (e) => {
e.preventDefault();
const isAllValid = this.validateAll();
if (isAllValid) {
// 校验通过,提交表单(实际项目中替换为AJAX请求)
const formData = this.getFormData();
alert('表单校验通过!提交数据:\\n' + JSON.stringify(formData, null, 2));
// this.form.submit(); // 原生提交
}
});
// 初始化密码强度样式
this.initPasswordStrength();
}
/**
* 防抖函数
* @param {Function} fn – 执行函数
* @param {Number} delay – 延迟时间
* @returns {Function}
*/
debounce(fn, delay) {
return (…args) => {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
/**
* 初始化密码强度容器
*/
initPasswordStrength() {
const strengthContainer = this.form.querySelector('.password-strength');
if (strengthContainer) {
strengthContainer.style.opacity = '0';
}
}
/**
* 检测密码强度
* @param {String} password – 密码值
*/
checkPasswordStrength(password) {
const strengthContainer = this.form.querySelector('.password-strength');
if (!strengthContainer || !password) {
strengthContainer.style.opacity = '0';
return;
}
strengthContainer.style.opacity = '1';
const items = strengthContainer.querySelectorAll('.strength-item');
// 重置样式
items.forEach(item => {
item.className = 'strength-item';
});
// 强度判断:弱(仅字母/数字)、中(字母+数字)、强(字母+数字+特殊字符)
let strength = 0;
if (/(?=.*[a-zA-Z])(?=.*\\d)/.test(password)) {
strength = 1; // 中
}
if (/(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^&*])/.test(password)) {
strength = 2; // 强
}
// 设置样式
for (let i = 0; i <= strength; i++) {
if (i === 0) {
items[i].classList.add('weak');
} else if (i === 1) {
items[i].classList.add('medium');
} else if (i === 2) {
items[i].classList.add('strong');
}
}
}
/**
* 校验单个表单项
* @param {HTMLElement} item – 输入框元素
* @param {Boolean} isBlur – 是否失去焦点(失去焦点时强制显示错误)
* @returns {Boolean} 校验结果
*/
validateItem(item, isBlur = false) {
const value = item.value.trim();
const rulesStr = item.dataset.rules || '';
const rules = rulesStr.split('|').filter(Boolean);
let isValid = true;
let errorMsg = '';
// 遍历所有规则
for (const ruleKey of rules) {
const rule = this.config.rules[ruleKey];
if (!rule) continue;
// 自定义校验函数
if (rule.validator) {
isValid = rule.validator(value, this.form);
} else {
// 正则校验
isValid = rule.regex.test(value);
}
// 校验失败,获取错误提示
if (!isValid) {
errorMsg = this.config.customMessages[ruleKey] || rule.message;
break;
}
}
// 更新样式和提示
this.updateItemStatus(item, isValid, errorMsg, isBlur);
// 缓存校验状态
this.validateStatus.set(item.name, isValid);
return isValid;
}
/**
* 更新表单项状态(样式+提示)
* @param {HTMLElement} item – 输入框元素
* @param {Boolean} isValid – 是否校验通过
* @param {String} errorMsg – 错误提示
* @param {Boolean} isBlur – 是否失去焦点
*/
updateItemStatus(item, isValid, errorMsg, isBlur) {
const errorTip = item.nextElementSibling;
if (!errorTip || !errorTip.classList.contains('error-tip')) return;
// 重置样式
item.classList.remove('success', 'error');
errorTip.classList.remove('show');
errorTip.textContent = '';
// 校验通过
if (isValid) {
item.classList.add('success');
} else {
// 校验失败:失去焦点/有值时显示错误
if (isBlur || item.value.trim()) {
item.classList.add('error');
errorTip.textContent = errorMsg;
errorTip.classList.add('show');
}
}
}
/**
* 校验所有表单项
* @returns {Boolean} 所有项是否都通过
*/
validateAll() {
const inputs = this.form.querySelectorAll('.form-input');
let isAllValid = true;
let firstErrorItem = null;
// 遍历所有输入框校验
inputs.forEach(item => {
const isValid = this.validateItem(item, true);
if (!isValid && !firstErrorItem) {
firstErrorItem = item;
isAllValid = false;
}
});
// 定位到第一个错误项
if (firstErrorItem) {
firstErrorItem.focus();
// 滚动到错误项(适配长表单)
firstErrorItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
return isAllValid;
}
/**
* 检查所有项状态,更新提交按钮
*/
checkAllStatus() {
const submitBtn = this.form.querySelector('.submit-btn');
if (!submitBtn) return;
// 所有必填项都通过校验才启用按钮
let canSubmit = true;
this.form.querySelectorAll('.form-input').forEach(item => {
const rulesStr = item.dataset.rules || '';
const isRequired = rulesStr.includes('required');
const status = this.validateStatus.get(item.name);
// 必填项未校验/校验失败
if (isRequired && (status === undefined || !status)) {
canSubmit = false;
}
});
submitBtn.disabled = !canSubmit;
}
/**
* 获取表单数据
* @returns {Object} 表单键值对
*/
getFormData() {
const formData = {};
this.form.querySelectorAll('.form-input').forEach(item => {
formData[item.name] = item.value.trim();
});
return formData;
}
/**
* 重置表单
*/
reset() {
this.form.reset();
this.validateStatus.clear();
this.form.querySelectorAll('.form-input').forEach(item => {
item.classList.remove('success', 'error');
const errorTip = item.nextElementSibling;
if (errorTip && errorTip.classList.contains('error-tip')) {
errorTip.classList.remove('show');
errorTip.textContent = '';
}
});
this.initPasswordStrength();
this.checkAllStatus();
}
}
// ===================== 初始化表单验证组件 =====================
window.addEventListener('load', () => {
// 创建验证实例
const validator = new FormValidator('#registerForm', {
// 自定义提示文案(可选)
customMessages: {
phone: '手机号格式不正确,请检查!',
password: '密码太弱啦,需6-16位且包含字母+数字~'
},
debounceTime: 200
});
// 如需手动重置表单,可调用:validator.reset();
});
</script>
</body>
</html>
五、核心功能拆解与讲解
1. 组件化设计思路
采用 Class 封装验证逻辑,核心优势:
- 配置化:支持自定义规则、提示文案、防抖时间,适配不同业务场景;
- 低耦合:组件仅依赖表单选择器,不侵入业务代码,可复用;
- 易扩展:新增校验规则只需在 config.rules 中添加,无需修改核心逻辑。
2. 核心校验逻辑
- 规则解析:通过 data-rules 属性声明表单项的校验规则(如 required|phone),支持多规则组合;
- 两种校验时机:
- 实时校验(input 事件 + 防抖):避免输入时频繁触发,提升性能;
- 失去焦点校验(blur 事件):强制显示错误提示,强化用户感知;
- 自定义校验函数:如 “确认密码” 规则,支持依赖其他字段的复杂校验。
3. 体验优化细节
- 密码强度检测:根据密码复杂度分级显示,引导用户设置强密码;
- 错误定位:提交时定位到第一个错误项,滚动到可视区,提升长表单体验;
- 按钮状态控制:未通过校验时禁用提交按钮,避免无效提交;
- 响应式适配:适配移动端,表单宽度自适应屏幕。
4. 扩展能力说明
新增自定义校验规则
javascript
运行
// 初始化组件时添加新规则
const validator = new FormValidator('#registerForm', {
rules: {
// 新增:年龄规则(18-60岁)
age: {
regex: /^[1-5]\\d|60$/,
message: '年龄需在18-60岁之间'
},
// 新增:自定义函数规则(如“邀请码已存在”)
inviteCodeExist: {
message: '邀请码不存在,请检查',
validator: async (value) => {
// 模拟AJAX请求校验
// const res = await fetch('/api/checkInviteCode', { method: 'POST', body: { code: value } });
// return res.data.exist;
return value === '88888888'; // 模拟通过
}
}
}
});
适配异步校验
如需支持异步校验(如校验手机号是否已注册),只需修改 validateItem 方法为异步,并调整提交逻辑:
javascript
运行
async validateItem(item, isBlur = false) {
// 原有逻辑…
// 异步校验示例
if (rule.asyncValidator) {
isValid = await rule.asyncValidator(value, this.form);
}
// 原有逻辑…
}
// 提交时改为异步
this.form.addEventListener('submit', async (e) => {
e.preventDefault();
const isAllValid = await this.validateAll();
if (isAllValid) {
// 提交逻辑
}
});
六、常见问题与避坑指南
1. 重复校验问题
- 问题:快速输入时频繁触发校验,性能损耗;
- 解决方案:添加防抖函数,控制校验频率(默认 300ms)。
2. 事件冒泡 / 委托问题
- 问题:blur 事件不支持冒泡,直接绑定可能失效;
- 解决方案:使用事件捕获(addEventListener 第三个参数为 true)。
3. 自定义规则依赖问题
- 问题:如 “确认密码” 依赖 “密码” 字段,直接校验可能获取不到值;
- 解决方案:自定义校验函数接收 form 参数,直接从表单获取最新值。
4. 低版本浏览器兼容
- 问题:IntersectionObserver/Class 等特性在 IE 低版本不支持;
- 解决方案:
- 引入 babel-polyfill 兼容 Class 和 Promise;
- 正则规则简化,避免使用 ES6 + 特性;
- 防抖函数改用传统写法。
七、总结与扩展
本文实现的表单验证组件覆盖了90% 的业务场景,核心亮点:
扩展方向
- 集成 UI 库:适配 ElementUI、AntD 等组件库的表单样式;
- 支持多表单:一个实例管理多个表单,避免重复创建;
- 国际化:支持多语言错误提示;
- 可视化配置:通过 JSON 配置生成表单 + 校验规则,无需手写 HTML。
这个案例不仅能帮你掌握表单验证的核心逻辑,还能强化 “组件化思维” 和 “用户体验优化” 意识 —— 前端开发不仅要实现功能,更要让代码健壮、用户用得舒服。建议你基于此案例二次开发,适配自己的业务场景,真正做到 “知其然,知其所以然”!

