第 8 节:表单 --- 前端校验的第一道防线
在前两节中,我们已经能让页面响应点击、输入等事件,并动态更新 DOM。但要构建一个完整的交互系统(比如学生信息录入),还缺少关键一环:收集用户输入的数据,并确保它符合规则。表单就是这个环节的主角,而前端校验则是它的第一道防线。
本节将结合第 6 节(DOM 操作)和第 7 节(事件)的知识,建立一套可复用的表单校验模式。
1. 为什么需要前端校验
后端校验是不可替代的最终防线,但前端校验的价值在于:
- 即时反馈:用户敲完字段就能看到对错,无需等待网络往返。
- 减少无效请求:把明显不合格的数据挡在浏览器内,减轻后端压力。
- 引导用户:清晰的提示能帮助用户更快完成表单。
作为后端开发者,你早已习惯用注解(@NotBlank、@Pattern、@Range)验证数据。前端校验不过是将这套规则从服务端迁移到客户端,并让它在用户输入的那一刻起作用。
数据流向 :
用户输入 → 前端实时/提交校验 → 通过后发送请求 → 后端二次校验 → 入库
2. 取出表单数据:FormData 与手动取值
表单校验的第一步是把用户填写的数据读出来。JS 提供了两种主要方式。
2.1 手动取值:按 ID 逐个读取
javascript
const data = {
studentId: document.getElementById('studentId').value, // "2024001"
name: document.getElementById('name').value,
age: document.getElementById('age').value, // "20" 注意:永远是字符串!
// 复选框需要单独处理
hobby: [...document.querySelectorAll('input[name="hobby"]:checked')].map(el => el.value)
};
注意点:
.value始终返回字符串(即使输入的是数字类型,如年龄)。- 复选框(checkbox)、多选列表等需特殊处理,不能直接
.value。 - 适合只取个别字段或需要加工值的场景。
2.2 FormData:一次性收集整个表单
javascript
const form = document.getElementById('studentForm');
const fd = new FormData(form);
fd.get('name'); // "张三"
fd.getAll('hobby'); // ["篮球", "音乐"] ← 多值字段必须用 getAll
Object.fromEntries(fd); // 转换为普通对象 { name: "张三", ... }
FormData 的规则:
- 控件必须有
name属性,否则会被忽略(就像菜品没有名字,系统不认)。 - 多值字段(复选框、多选列表)用
getAll()获取所有值,fromEntries只会保留最后一个。 - 未选中的复选框不会出现在
FormData中。
选择建议:
| 场景 | 方式 |
|---|---|
| 整体提交(字段完整) | FormData,一行搞定 |
| 只取 1~2 个字段,或需要数字转换 | 手动取值 |
| 包含文件上传 | FormData 自动支持 File 对象 |
后端对照 :
new FormData(form)类似 Spring MVC 的@ModelAttribute自动绑定------表单控件名(name)对应 DTO 属性名。
3. 实时校验:input 事件 + 正则表达式
3.1 实时反馈的原理
第 7 节讲过,input 事件在输入框内容每次变化时都会触发。这为"边输入边检查"提供了引擎。
javascript
const idInput = document.getElementById('studentId');
const ID_PATTERN = /^\d{7}$/; // 7 位纯数字
idInput.addEventListener('input', (e) => {
const value = e.target.value.trim();
if (value === '') {
// 空值状态
} else if (ID_PATTERN.test(value)) {
// 格式正确
} else {
// 格式错误
}
});
3.2 JS 正则:够用的基础知识
与后端正则类似,但 JS 中正则通常用字面量 写法(/pattern/),核心方法为 .test(str)。
javascript
// 常用正则示例
/^\d{7}$/ // 学号:恰好 7 位数字
/^1[3-9]\d{9}$/ // 手机号:1 开头,第二位 3~9,后 9 位数字
.trim() !== '' // 非空判断(去掉前后空格)
// test() 返回布尔值
/^\d{7}$/.test('2024001'); // true
/^\d{7}$/.test('123'); // false
必须写
^和$:/^\d{7}/只能保证开头 有 7 位数字,无法阻止12345678通过;加上$才强制整个字符串匹配。
3.3 input vs change:选择哪个事件?
| 事件 | 触发时机 | 适用场景 |
|---|---|---|
input |
每次键击(实时) | 需要边输入边提示,如注册、搜索 |
change |
内容改变后失焦时 | 下拉框切换、复选框勾选、整体选择完毕 |
口诀 :input 是边打边报,change 是选完才报。
实时校验核心字段(如学号、手机号)使用 input;下拉框、单选框等使用 change。
4. 三种校验模式:什么时候验证?
并非所有字段都适合每敲一个字就检查,我们可以根据场景组合三种模式。
4.1 三种时机
| 模式 | 事件 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 实时校验 | input |
反馈最快,体验最好 | 频繁触发,可能需要防抖 | 关键字段(用户名、密码、手机) |
| 失焦校验 | blur |
不打断输入,一次完整输入后检查 | 反馈略慢 | 长表单、复杂规则字段(如身份证) |
| 提交校验 | submit |
逻辑集中,一次性检查所有字段 | 用户可能填很多才提示 | 可作为最终兜底,配合前两种模式 |
4.2 组合策略(实际项目推荐)
- 核心字段 :实时校验(
input),给即时指引。 - 次要字段 :失焦校验(
blur),减少干扰。 - 全局把关:提交时再跑一次全量验证,防止漏网之鱼(例如用户直接跳过某些输入框点提交)。
三种模式可以调用同一个校验函数,避免逻辑重复。
5. 设计可复用的校验函数(表驱动)
5.1 坏味道:散落的 if-else
如果每个字段、每种事件里都写一段判断,代码会迅速膨胀,且新增字段要改多处。
5.2 好模式:规则表 + 统一校验器
仿照后端 DTO 注解,我们将所有校验规则抽取到一个规则表对象中,然后用一个函数遍历执行。
① 定义规则表(RULES)
javascript
const RULES = {
studentId: {
label: '学号', required: true,
pattern: /^\d{7}$/, message: '学号必须为 7 位纯数字'
},
name: {
label: '姓名', required: true, message: '姓名不能为空'
},
age: {
label: '年龄', required: true,
validator(value) { // 自定义校验函数
const num = Number(value);
if (!Number.isInteger(num)) return '年龄必须是整数';
if (num < 1 || num > 100) return '年龄必须在 1~100 之间';
return ''; // 返回空串表示通过
}
},
phone: {
label: '手机号', required: false, // 选填
pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确'
},
};
每条规则可包含:
required: 是否必填(对应@NotBlank)pattern: 正则格式(对应@Pattern)validator(value): 任意自定义逻辑(对应@Min、@Max等)
② 统一校验函数 validateField
javascript
function validateField(fieldName) {
const input = document.getElementById(fieldName);
const rule = RULES[fieldName];
const value = input.value.trim();
// 1. 必填检查
if (rule.required && value === '') {
return rule.label + '不能为空';
}
// 2. 正则检查(仅当有值和正则规则时)
if (rule.pattern && value !== '' && !rule.pattern.test(value)) {
return rule.message;
}
// 3. 自定义检查
if (rule.validator && value !== '') {
return rule.validator(value);
}
return ''; // 无错误
}
③ 显示/清除错误 (结合第 6 节的 classList 知识)
javascript
function showError(fieldName, message) {
const input = document.getElementById(fieldName);
input.classList.add('invalid');
document.getElementById(fieldName + '-error').textContent = message;
}
function clearError(fieldName) {
document.getElementById(fieldName).classList.remove('invalid');
document.getElementById(fieldName + '-error').textContent = '';
}
④ 绑定事件:一套代码覆盖所有字段和时机
javascript
Object.keys(RULES).forEach(fieldName => {
const input = document.getElementById(fieldName);
input.addEventListener('input', () => handleField(fieldName)); // 实时
input.addEventListener('blur', () => handleField(fieldName)); // 失焦
});
function handleField(fieldName) {
const error = validateField(fieldName);
if (error) showError(fieldName, error);
else clearError(fieldName);
}
⑤ 提交通关:全量验证
javascript
form.addEventListener('submit', (e) => {
e.preventDefault();
let firstError = '';
Object.keys(RULES).forEach(name => {
const err = validateField(name);
if (err) { showError(name, err); firstError = firstError || name; }
else clearError(name);
});
if (firstError) {
document.getElementById(firstError).focus(); // 定位到第一个错误字段
return;
}
// 全部通过 → 收集数据,发送请求
const fd = new FormData(form);
console.log('提交数据:', Object.fromEntries(fd));
});
优势:
- 新增字段只需在
RULES中加一行,事件绑定和校验逻辑无需改动。 - 实时、失焦、提交三种模式共用同一套校验代码。
- 可以方便地扩展更多校验类型(如跨字段校验)。
6. 完整示例:学生新增表单
结合以上所有知识,一个典型的学生信息录入表单将包含:
- 实时校验学号、姓名、年龄、手机号。
- 班级下拉框(使用
change事件)检查是否选择。 - 提交时再次全面检查,并利用
FormData收集数据。 - 实时统计通过项数,控制提交按钮状态。
核心代码无需再重复,只需按照上述模式将具体字段规则填入 RULES,并绑定事件即可。完整实现可参考代码文件 05-student-form.html(双击运行,无需服务器)。
关键细节补充:
- 下拉框必选 :将默认提示项设置为
<option value="">请选择班级</option>,配合required: true,值依然为空时会触发"不能为空"。 - 空字符串陷阱 :
Number('')结果为0,因此年龄必填检查必须在转换前进行。 - 复用思想 :
handleField既被实时/失焦事件调用,也可以用在提交后的全量检查中(只需遍历RULES调用validateField)。
7. 小结
- 数据获取 :整体提交首选
FormData(必须带name属性),少量字段或需类型转换时手动取值。 - 校验时机 :实时校验(
input)提供最佳体验,失焦校检(blur)减少干扰,提交时全局把关。 - 校验规则 :使用规则表集中定义,再通过统一函数执行,避免散落的条件判断,易于扩展和维护。
- 与后端对照:前端校验是后端校验的补充,不是替代,最终安全永远在后端。
掌握了表单校验,你就能构建出既友好又稳健的数据录入界面。