提供完整的JavaScript表单验证源代码实例,涵盖用户名、电子邮箱、手机号及密码强度的正则表达式规则设计,解析失焦即时校验、错误提示DOM渲染与提交时全量拦截的实现机制。
JavaScript表单验证源代码:正则表达式与错误提示实例
时间:2026-11-03
浏览:236
文章目录
在Web前端开发中,表单验证是拦截无效输入、提升交互质量的第一道关卡。一个健壮的验证模块既需要精确的正则表达式以确保数据合规,也需要清晰及时的错误提示UI,告知用户未通过的原因及修改方向。本文提供一套基于原生JavaScript编写的完整表单验证源代码,包含常用输入项的正则表达式配置、失焦与提交双重校验逻辑,以及对应的DOM错误提示状态渲染方案。
一、表单验证架构与执行流程设计
表单验证的常见误区在于“只在点击提交按钮时才报错”或“边输入边频繁弹出警报”。良好的用户体验应当采取分层验证策略:单字段失焦(blur)触发局部核验,用户专注于某个输入项时不过早打断;输入过程中(input)在检测到合规后即时撤销错误样式;而在用户触发提交(submit)时,执行全局聚合遍历,若有任意项未通过,则拦截表单提交并自动将光标聚焦于首个错误输入框。
整个验证流程的核心执行步骤如下:
- 事件拦截:在表单元素上监听
submit事件,通过event.preventDefault()拦截浏览器默认的原生提交行为,将提交控制权完全交给JavaScript脚本。 - 字段映射与规则匹配:为每个输入控件配置独立的校验器对象,包含非空检测、正则表达式规则、最大/最小长度边界以及对应的错误提示文案。
- UI状态同步:当校验失败时,向输入框挂载
is-invalid状态类,更新对应的aria-invalid="true"无障碍属性,并在相邻的提示容器中注入具体错误文案;当校验通过时,清除错误状态并重置文本。
二、核心字段正则表达式设计与匹配规则
正则表达式(Regular Expression)是检测文本结构是否符合特定格式的核心工具。在构建表单验证规则时,必须显式声明起始符 ^ 与结束符 $,确保对用户输入的整行内容进行全量匹配,避免因部分子串匹配而放行非法输入。
| 验证字段 | 正则表达式 (Pattern) | 规则说明与边界约束 |
|---|---|---|
| 用户名 / 账号 | ^[a-zA-Z0-9_一-龥]{3,16}$ |
允许中文字符、英文字母、阿拉伯数字及下划线,字符总长度限制在3到16位之间。 |
| 电子邮箱 | ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ |
以字母数字或常见点号开头,强制包含 @ 符号,主机域名后紧跟点号与至少2位字母构成的顶级域名。 |
| 手机号码 | ^1[3-9]\d{9}$ |
匹配中国大陆11位手机号:第一位必须为数字1,第二位限定在3至9之间,后续连续跟随9位任意数字。 |
| 强密码规则 | ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d@$!%*?&]{8,20}$ |
使用正向前瞻断言 (?=...) 约束密码必须同时包含小写字母、大写字母与数字,总长度为8到20位。 |
/pattern/)声明正则表达式时,无需对常规反斜杠进行双重转义;但如果通过 new RegExp("...") 构造函数动态生成规则,字符串参数中的反斜杠必须写为 \d。推荐在规则固定的场景下优先使用字面量语法。
三、输入框状态机与可访问性错误提示机制
交互式错误提示不能仅仅停留在“把文字变成红色”,这不仅在视觉上缺乏层次感,对于使用屏幕阅读器的视障用户也是不可感知的。一套规范的错误提示体系应当在DOM结构与无障碍规范(WAI-ARIA)上达成闭环。
在设计HTML与CSS交互时,建议遵循以下三项规范:
- 关联提示容器:在每个
<input>标签中设置aria-describedby="[字段ID]-error",并在下方的错误提示容器上标记id="[字段ID]-error"。屏幕阅读器在光标移入时能直接读取该输入框关联的错误状态。 - 动态属性开关:当校验未通过时,为输入框赋予
aria-invalid="true";校验通过或输入框为空时恢复为aria-invalid="false"。 - 平滑清除机制:监听输入框的
input事件,当用户重新键入内容且内容逐步符合规范时,应当即时隐藏红色提示文本,消除用户的挫败感。
四、完整表单验证源代码实例
以下代码提供了一个开箱即用的完整示例,包含注册表单的HTML标记、CSS状态样式以及模块化组织的JavaScript校验脚本。你可以将以下代码直接保存为 .html 文件在浏览器中打开验证。
<!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>
/* 表单基础排版 */
.form-wrapper {
max-width: 420px;
margin: 40px auto;
padding: 24px;
background: #FFFFFF;
border: 1px solid #CCD2D8;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(79, 98, 114, 0.1);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.form-wrapper h2 {
margin-bottom: 20px;
font-size: 20px;
color: #4F6272;
text-align: center;
}
.form-group {
margin-bottom: 18px;
display: flex;
flex-direction: column;
}
.form-label {
font-size: 13px;
font-weight: 600;
color: #4F6272;
margin-bottom: 6px;
}
.form-control {
height: 38px;
padding: 6px 12px;
font-size: 14px;
border: 1px solid #CCD2D8;
border-radius: 4px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-control:focus {
border-color: #477DAD;
box-shadow: 0 0 0 3px rgba(71, 125, 173, 0.2);
}
/* 校验状态样式 */
.form-control.is-invalid {
border-color: #D9534F;
background-color: #FFF9F9;
}
.form-control.is-invalid:focus {
box-shadow: 0 0 0 3px rgba(217, 83, 79, 0.2);
}
.form-control.is-valid {
border-color: #477DAD;
}
.error-feedback {
display: none;
margin-top: 5px;
font-size: 12px;
color: #D9534F;
line-height: 1.4;
}
.form-group.has-error .error-feedback {
display: block;
}
.btn-submit {
width: 100%;
height: 40px;
background-color: #477DAD;
color: #FFFFFF;
border: none;
border-radius: 4px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-submit:hover {
background-color: #3A6790;
}
.btn-submit:disabled {
background-color: #9AA5B1;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="form-wrapper">
<h2>账号注册</h2>
<form id="registerForm" novalidate>
<!-- 用户名 -->
<div class="form-group" id="group-username">
<label class="form-label" for="username">用户名</label>
<input type="text" id="username" class="form-control" autocomplete="off"
placeholder="3-16位汉字、字母、数字" aria-describedby="username-error" aria-invalid="false">
<span class="error-feedback" id="username-error" role="alert"></span>
</div>
<!-- 电子邮箱 -->
<div class="form-group" id="group-email">
<label class="form-label" for="email">电子邮箱</label>
<input type="email" id="email" class="form-control" autocomplete="off"
placeholder="example@domain.com" aria-describedby="email-error" aria-invalid="false">
<span class="error-feedback" id="email-error" role="alert"></span>
</div>
<!-- 手机号码 -->
<div class="form-group" id="group-phone">
<label class="form-label" for="phone">手机号码</label>
<input type="tel" id="phone" class="form-control" autocomplete="off"
placeholder="11位手机号码" aria-describedby="phone-error" aria-invalid="false">
<span class="error-feedback" id="phone-error" role="alert"></span>
</div>
<!-- 登录密码 -->
<div class="form-group" id="group-password">
<label class="form-label" for="password">登录密码</label>
<input type="password" id="password" class="form-control"
placeholder="8-20位,含大小写字母与数字" aria-describedby="password-error" aria-invalid="false">
<span class="error-feedback" id="password-error" role="alert"></span>
</div>
<!-- 确认密码 -->
<div class="form-group" id="group-confirmPwd">
<label class="form-label" for="confirmPwd">确认密码</label>
<input type="password" id="confirmPwd" class="form-control"
placeholder="请再次输入登录密码" aria-describedby="confirmPwd-error" aria-invalid="false">
<span class="error-feedback" id="confirmPwd-error" role="alert"></span>
</div>
<button type="submit" class="btn-submit" id="submitBtn">提交注册</button>
</form>
</div>
<script>
// 验证规则配置字典
const validationRules = {
username: {
required: true,
pattern: /^[a-zA-Z0-9_一-龥]{3,16}$/,
emptyMsg: "请输入用户名",
errorMsg: "用户名须为3-16位汉字、字母、数字或下划线"
},
email: {
required: true,
pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
emptyMsg: "请输入电子邮箱",
errorMsg: "请输入格式正确的电子邮箱地址"
},
phone: {
required: true,
pattern: /^1[3-9]\d{9}$/,
emptyMsg: "请输入手机号码",
errorMsg: "请输入有效的11位中国大陆手机号码"
},
password: {
required: true,
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d@$!%*?&]{8,20}$/,
emptyMsg: "请设置登录密码",
errorMsg: "密码长度须为8-20位,且必须包含大小写字母与数字"
},
confirmPwd: {
required: true,
customValidator: (value) => {
const pwd = document.getElementById("password").value;
if (!value) return { valid: false, msg: "请再次输入密码以确认" };
if (value !== pwd) return { valid: false, msg: "两次输入的密码不一致" };
return { valid: true };
}
}
};
// 显示错误提示
function showError(fieldId, message) {
const input = document.getElementById(fieldId);
const group = document.getElementById(`group-${fieldId}`);
const errorSpan = document.getElementById(`${fieldId}-error`);
input.classList.remove("is-valid");
input.classList.add("is-invalid");
input.setAttribute("aria-invalid", "true");
errorSpan.textContent = message;
group.classList.add("has-error");
}
// 清除错误提示并标记合法
function clearError(fieldId, markValid = true) {
const input = document.getElementById(fieldId);
const group = document.getElementById(`group-${fieldId}`);
const errorSpan = document.getElementById(`${fieldId}-error`);
input.classList.remove("is-invalid");
input.setAttribute("aria-invalid", "false");
errorSpan.textContent = "";
group.classList.remove("has-error");
if (markValid && input.value.trim() !== "") {
input.classList.add("is-valid");
} else {
input.classList.remove("is-valid");
}
}
// 单字段校验逻辑
function validateField(fieldId) {
const config = validationRules[fieldId];
const input = document.getElementById(fieldId);
if (!config || !input) return true;
const value = input.value.trim();
// 1. 自定义校验函数优先执行(如确认密码比对)
if (typeof config.customValidator === "function") {
const result = config.customValidator(value);
if (!result.valid) {
showError(fieldId, result.msg);
return false;
}
clearError(fieldId);
return true;
}
// 2. 必填性判断
if (config.required && value === "") {
showError(fieldId, config.emptyMsg);
return false;
}
// 3. 正则表达式模式匹配
if (config.pattern && !config.pattern.test(value)) {
showError(fieldId, config.errorMsg);
return false;
}
// 校验全部通过
clearError(fieldId);
return true;
}
// 初始化事件绑定
document.addEventListener("DOMContentLoaded", () => {
const form = document.getElementById("registerForm");
const fieldKeys = Object.keys(validationRules);
// 为每个字段绑定 blur (失焦校验) 与 input (边输边清除错误)
fieldKeys.forEach((key) => {
const input = document.getElementById(key);
if (!input) return;
input.addEventListener("blur", () => {
validateField(key);
});
input.addEventListener("input", () => {
// 用户正在输入时,如果输入框处于错误态,且当前值已满足正则,则清除错误
const value = input.value.trim();
const config = validationRules[key];
if (config.pattern && config.pattern.test(value)) {
clearError(key);
}
});
});
// 表单全量提交校验
form.addEventListener("submit", (e) => {
e.preventDefault(); // 拦截默认提交行为
let isFormValid = true;
let firstErrorField = null;
// 遍历所有规则字段
fieldKeys.forEach((key) => {
const isValid = validateField(key);
if (!isValid) {
isFormValid = false;
if (!firstErrorField) {
firstErrorField = document.getElementById(key);
}
}
});
// 若有字段校验不通过,自动将焦点移动至第一个错误输入框
if (!isFormValid) {
if (firstErrorField) {
firstErrorField.focus();
}
return;
}
// 全部验证通过,执行数据提交业务
alert("表单验证全部通过,数据准备提交!");
});
});
</script>
</body>
</html>
五、表单提交批量聚合校验与执行总结
在表单提交阶段,不能只检测并提示第一个错误就提前中断循环。最佳的交互实践是:全量执行校验,让所有出错字段的错误提示同时展示给用户,同时将浏览器视口与输入光标自动平滑滚动并聚焦(focus)到首个错误项上。这样既能让用户一次性获知所有需要修正的信息,又明确指示了下一步的操作入口。
六、常见边界问题与防御处理
在实际生产部署中,除了常规的格式正则匹配,还有几处细节需要特别注意:
- HTML5原生提示拦截:表单标签必须添加
novalidate属性(例如<form id="registerForm" novalidate>)。若不添加,浏览器自带的气泡弹窗提示会干扰自定义的error-feedback容器展示,导致UI冲突。 - 首尾空白字符注入:在进行
test()正则判断前,务必调用value.trim()。用户在移动端通过输入法自动补全或复制内容时,经常携带尾部空格,若不清理会导致本该合规的内容验证失败。 - 关联字段依赖校验:“确认密码”高度依赖“设置密码”字段。当用户修改了“设置密码”后,即便没有点击“确认密码”框,在提交时也必须重新触发一次一致性比对。
- 防重复点击与网络并发:在全量验证通过并发起
fetch()异步请求期间,应立即设置submitBtn.disabled = true并添加“提交中...”文案,防止用户因网络延迟连续多次点击产生重复提交。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。