表单是网页和用户打交道最多的元素,但"能提交"和"好用的表单"之间差着十万八千里:密码太弱要提醒、邮箱格式错了要即时标红、点提交不能连点两次……本文用纯 JavaScript 实现一个完整的注册表单,把校验、反馈、提交这一整套流程讲透,代码可以直接复制到你的项目里改造使用。
1. 表单的 HTML 骨架
先搭一个最基础的注册表单。每个输入框配一个错误提示元素(默认是空的),用命名规律把输入框和它的错误提示对应起来,方便 JS 成对处理:
<form id="registerForm" novalidate>
<div>
<label for="username">用户名</label>
<input id="username" name="username" type="text">
<p class="error" id="usernameError"></p>
</div>
<div>
<label for="email">邮箱</label>
<input id="email" name="email" type="email">
<p class="error" id="emailError"></p>
</div>
<div>
<label for="password">密码</label>
<input id="password" name="password" type="password">
<p class="error" id="passwordError"></p>
</div>
<div>
<label for="confirm">确认密码</label>
<input id="confirm" name="confirm" type="password">
<p class="error" id="confirmError"></p>
</div>
<button type="submit">注册</button>
</form>
novalidate 关掉浏览器自带的提示气泡,把校验控制权完全交给 JS,这样错误提示的样式和文案才能统一。注意每个输入框都带了 name 属性——这是后面 FormData 能自动收集数据的前提。
还有两个容易被忽略的细节:一是 label 的 for 属性指向输入框 id,点击文字就能聚焦输入框,对鼠标和屏幕阅读器都友好;二是错误提示元素最好加上 aria-live="polite" 标记,这样校验失败时屏幕阅读器会自动读出提示,视力障碍用户也能知道哪里填错了——可访问性不是加分项,是基本要求。
样式上,错误提示默认用红色小字加输入框红边框就足够醒目;如果表单很长,提交失败时还可以用 scrollIntoView 把页面滚到第一个出错字段,用户不用自己满屏找——这个细节很多人做了才知道香。
2. 收集表单数据:FormData
拿到表单 DOM 后,new FormData(form) 会把所有带 name 属性的输入框自动收集起来。配合 Object.fromEntries 能一键转成普通对象,操作起来非常方便:
const form = document.getElementById('registerForm');
form.addEventListener('submit', function (event) {
event.preventDefault();
const data = new FormData(form);
const values = Object.fromEntries(data.entries());
console.log(values);
});
event.preventDefault() 阻止表单默认的整页刷新提交——这是所有 JS 表单处理的第一步,忘了它,你写的所有校验都会在提交瞬间被"跳过"。FormData 对 checkbox、radio、file 等控件同样适用,比逐个 getElementById 取值省事得多。
FormData 还提供 get()、has()、append() 等方法:data.get('username') 取单个值,data.has('agree') 判断勾选框是否勾上,append 可以追加同名多值(比如多选标签)。处理文件上传时,直接把 <input type="file"> 的 File 对象 append 进去,再以 FormData 作为 body 提交,浏览器会自动设置 multipart 编码——这是上传文件的标准姿势。另外提醒一句:密码框用 type="password" 只是防偷看,数据在网络上依然是明文,真正的安全靠 HTTPS。
3. 用正则定义校验规则
校验规则的本质是"用正则判断字符串是否符合格式"。把规则集中放在一个对象里,再写一个通用的 validate 函数,规则和逻辑就分开了,以后加字段只改一处:
const rules = {
username: /^[a-zA-Z0-9_]{3,16}$/,
email: /^[\w.-]+@[\w-]+(\.[\w-]+)+$/,
password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,20}$/
};
function validate(field, value) {
if (!value) {
return field === 'username' ? '请输入用户名' : '此项不能为空';
}
if (!rules[field].test(value)) {
const messages = {
username: '用户名需为 3-16 位字母、数字或下划线',
email: '邮箱格式不正确',
password: '密码需 8-20 位,且同时包含大小写字母和数字'
};
return messages[field];
}
return '';
}
console.log(validate('username', 'linwan'));
console.log(validate('email', 'lin@example.com'));
console.log(validate('password', 'abc'));
console.log(validate('password', 'Abc12345'));
密码那条正则是重点:(?=.*[a-z]) 是"零宽断言",它只检查"后面有没有小写字母"而不消耗字符。三个断言叠加,就实现了"必须同时包含小写、大写、数字"的强密码规则,而且顺序无关——这是用单个正则写复杂条件的关键技巧。
正则不是万能的,它只做"格式"检查,做不了"语义"检查。比如邮箱正则能拦住 abc@ 这种明显错误,但拦不住 not-a-real@domain.com 这种格式合法却不存在的地址——那需要发验证邮件才能确认。所以别把正则写得过于复杂,能覆盖 95% 的正常输入就够了,剩下的交给后端。写正则时建议打开 DevTools 控制台,用 /正则/.test('测试字符串') 一行一行试,比写完直接上生产靠谱得多。
4. 实时反馈:输入即校验
只等提交时才报错,体验太差。监听 input 事件,每次输入都重新校验,有错就显示红字,无错就清空提示。这样用户还没点提交,就已经知道哪里不对了:
function bindLiveValidation(inputId, errorId, field) {
const input = document.getElementById(inputId);
const error = document.getElementById(errorId);
input.addEventListener('input', function () {
const message = validate(field, input.value.trim());
error.textContent = message;
input.classList.toggle('invalid', Boolean(message));
});
}
bindLiveValidation('username', 'usernameError', 'username');
bindLiveValidation('email', 'emailError', 'email');
bindLiveValidation('password', 'passwordError', 'password');
input 事件在每次内容变化时触发,比 change 更及时(change 要等失焦才触发)。classList.toggle('invalid', condition) 的第二个参数是开关:条件为真就加类,为假就移除,比手动 if/else 干净多了,配合 CSS 就能给输入框加红边框。
实时校验有个体验细节:用户刚点进输入框、还没来得及打字时,就弹出"此项不能为空"会很烦。常见的做法是"失焦后开始校验,输入时实时刷新":第一次用 blur 事件触发校验,之后每次 input 都重新校验并更新提示。上面的 bindLiveValidation 其实已经接近这个效果——input 事件只在你真正输入时才触发,配合 trim() 处理首尾空格,基本不会误报。
三种校验时机各有分工:blur 管"离开时提醒",input 管"修改时实时更新",submit 管"提交前最终把关"。组合使用,体验才完整。还有个小细节:校验通过时清空错误提示的同时,别忘了把输入框的 invalid 类也去掉,否则边框会一直红着,用户会以为自己还在报错状态。
5. 提交:总校验 + 防重复 + 发请求
点击提交时,要对所有字段做一次"总校验",任何一项不过就整体拦截。通过后禁用按钮防止重复提交,再用前面学过的 fetch 把数据发给后端——这里是完整流程:
form.addEventListener('submit', async function (event) {
event.preventDefault();
const data = new FormData(form);
const values = Object.fromEntries(data.entries());
values.username = values.username.trim();
values.email = values.email.trim();
const errors = {};
for (const field of ['username', 'email', 'password']) {
const message = validate(field, values[field]);
if (message) {
errors[field] = message;
}
}
if (values.password !== values.confirm) {
errors.confirm = '两次输入的密码不一致';
}
if (Object.keys(errors).length > 0) {
console.log('校验失败:', errors);
return;
}
const button = form.querySelector('button[type="submit"]');
button.disabled = true;
button.textContent = '提交中…';
try {
const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values)
});
if (!response.ok) {
throw new Error('注册失败,请稍后再试');
}
console.log('注册成功!');
} catch (err) {
console.error(err.message);
} finally {
button.disabled = false;
button.textContent = '注册';
}
});
注意两个细节:确认密码的比对放在正则循环之后,因为 confirm 字段没有独立规则,只能做一致性检查;提交期间 disabled 按钮 + finally 恢复,能同时防住"连点"和"失败后无法重试"两个问题。另外请记住,真实项目里后端校验永远不能省——前端校验只是用户体验。
上面的例子把错误打进了 console,真实项目里要把错误显示到页面上——既然错误提示元素已经按"字段名 + Error"命名,遍历 errors 对象就能批量渲染,还能复用上一节的实时校验逻辑,保证"提交时"和"输入时"看到的是同一套提示:
function showErrors(errors) {
for (const [field, message] of Object.entries(errors)) {
const errorEl = document.getElementById(field + 'Error');
if (errorEl) {
errorEl.textContent = message;
}
const input = document.getElementById(field);
if (input) {
input.classList.toggle('invalid', Boolean(message));
}
}
}
showErrors({ username: '用户名已被占用', email: '邮箱格式不正确' });
showErrors 把字段名映射成输入框和提示元素的 id,一次遍历同时更新两处。field + 'Error' 这种命名约定在这里体现出了价值:只要有统一的命名规则,批量操作就只需要一行循环。把 showErrors 接进提交回调的 errors 分支,错误提示就完整了。
6. 总结与练习
一套完整的表单流程是:HTML 骨架(带错误提示位)→ FormData 收集 → 正则规则表 → input 实时校验 → 提交时总校验 → 防重复 + fetch 发送。规则与逻辑分离、错误信息即时可见,是体验的关键。
练习:
- 给表单加一个"手机号"字段,规则为 11 位数字且以 1 开头,并在实时校验中生效。
- 把错误提示从"文本框下方"改成"输入框边框变红 + 顶部汇总提示",体会不同反馈方式的取舍。
- 加一个显示密码强度的进度条:根据正则命中情况分"弱/中/强"三档,动态更新样式。
💡 表单校验永远分两层:前端校验管体验,后端校验管安全。任何"前端已经拦过了"的想法,都是安全漏洞的温床。