主题
2-19综合案例-注册页面相关功能
本页对应 第二章WebAPIs,共 8 段视频,约 1 小时 4 分。
学习目标
- 能够分析注册页面的表单结构,并将其拆分为发送验证码、表单验证、同意协议、提交验证四类业务。
- 能够使用点击事件和
setInterval实现验证码倒计时,在倒计时结束后清除定时器并恢复“重新获取”文字。 - 能够使用开关变量
flag防止倒计时期间重复点击发送验证码,并在倒计时结束时重新开启开关。 - 能够使用
change事件和正则表达式封装用户名、手机号、验证码、密码及确认密码验证函数,正确返回true/false并显示或清空对应错误提示。 - 能够使用
classList.toggle切换同意协议图标的选中样式,并使用classList.contains判断用户是否勾选协议。 - 能够注册表单的
submit事件,在未勾选协议或任一表单验证未通过时调用preventDefault阻止提交,全部通过时允许提交。
知识点
本节说明与效果图
本节笔记标题写作“正则插件”,但转写稿实际完成的是一个“注册页面综合案例”,用表单验证来练习正则表达式,并没有讲浏览器插件。下面按转写稿整理,不额外编造插件知识。转写稿中的“政策表达式”应纠正为“正则表达式”,“称职事件”应为“change事件”,“投稿”应为“toggle”,“conts”应为“contains”。

页面布局与业务拆分
为什么需要:综合案例代码量比单个知识点大,先看结构再写逻辑,能避免一上来就堆代码。
怎么想:转写稿先通过调试工具看核心区域:一个form包住若干div,每个div里有输入框、前面的span小图标,以及后面用于显示错误信息的message盒子。
怎么做:把业务拆成四块:发送短信验证码倒计时、各表单验证、阅读并勾选协议、点击“下一步”时汇总验证并阻止非法提交。
易错点:多个表单项都有message,不能用一次querySelector随便取;验证某个输入框时,应取它自己的下一个兄弟span来写提示。
发送验证码倒计时
为什么需要:点击“发送验证码”后要进入倒计时,避免用户以为没反应而重复点击。
怎么想:点击后先立刻把文字改成“05秒后重新获取”,再开启定时器每秒递减;减到0时改成“重新获取”并清除定时器。
怎么做:按转写稿片段整理如下:
js
const codeBtn = document.querySelector('.code');
let i = 5;
let timerId;
codeBtn.addEventListener('click', function () {
codeBtn.innerHTML = '05秒后重新获取';
timerId = setInterval(function () {
i--;
codeBtn.innerHTML = `${i}秒后重新获取`;
if (i === 0) {
codeBtn.innerHTML = '重新获取';
clearInterval(timerId);
}
}, 1000);
});易错点:定时器回调里this指向window,转写稿特别提醒不要把codeBtn改成this;到0时一定clearInterval,否则会继续减成负数。
防止倒计时期间重复点击
为什么需要:倒计时没结束时反复点击,会反复发送验证码。
怎么想:定义一个开关变量flag,初始为true;点击后先判断if (flag),执行一次后立刻把flag设为false;倒计时结束再设回true。
怎么做:把点击业务包进if (flag) { ... },在定时器i === 0的分支里恢复开关。
易错点:关闭开关的代码要写在if (flag)大括号内部;恢复开关要写在时间到的分支里,否则下一轮永远点不了。
用户名验证与change事件
为什么需要:用户名要符合“字母、数字、短横线、下划线,6到10位”的规则,不合法要提示。
怎么想:转写稿选择change事件,而不是blur或input。change要求值发生变化并且失去焦点才触发,用户没改内容时不必重复验证,效率更好。
怎么做:把验证封装成verifyName函数,事件触发时调用;不合法时给输入框的下一个兄弟span写提示并return false,合法时清空提示并return true。
易错点:change与blur不同;多个message不能只取第一个;合法时先清空提示再return true,否则提示不会消失。
其余表单验证的复制修改
为什么需要:手机号、验证码、密码的验证思路相同,只是正则和提示不同。
怎么想:复制用户名验证函数,重点改四处:获取的元素、函数名、正则变量、错误提示文字。
怎么做:手机号用verifyPhone,验证码用verifyCode,密码用verifyPwd;确认密码不套正则,而是判断两次输入值是否一致。
易错点:复制后容易漏改元素名或正则名;确认密码的判断条件是“两个输入框的值不相等”,不是再写一套密码正则。
勾选协议与提交汇总
为什么需要:协议不是复选框,而是用图标类名切换高亮;提交时既要检查协议,也要检查所有表单函数是否通过。
怎么想:点击协议图标用classList.toggle('icon-confirm2')切换类;提交时用classList.contains('icon-confirm2')判断有没有勾选。各验证函数返回true/false,提交事件里逐个调用,只要有一个返回false就阻止提交。
怎么做:按转写稿片段整理:
js
confirm.addEventListener('click', function () {
confirm.classList.toggle('icon-confirm2');
});
form.addEventListener('submit', function (e) {
if (!confirm.classList.contains('icon-confirm2')) {
e.preventDefault();
alert('请勾选同意协议');
}
if (!verifyName()) e.preventDefault();
if (!verifyPhone()) e.preventDefault();
if (!verifyCode()) e.preventDefault();
if (!verifyPwd()) e.preventDefault();
if (!verifyConfirm()) e.preventDefault();
});易错点:contains返回布尔值,没勾选时前面要加!;preventDefault要写在提交事件对象上;验证函数必须明确return true/false,否则提交汇总拿不到结果。
change 事件的基本触发规则
本节笔记只有一句核心:给 input 注册 change 事件,值被修改并且失去焦点后触发。这句话看起来短,但在注册页表单验证里非常关键。
为什么需要 change?因为表单验证不能“用户每敲一个字符就判错”,也不能“只要离开输入框就重复验证”。用户还没输完就提示错误,体验会很差;用户没有修改内容、只是把光标移开,也没必要重新验证。change 的触发条件正好是“值发生了变化”并且“失去焦点”,所以适合做用户名的格式校验。
怎么想?先明确 change 不是输入时触发,也不是单纯失焦触发。它要求两个条件同时满足:值变了,并且焦点离开了。若用户没有输入内容直接离开,值没变,不会触发;若用户输入后又改回原值,通常也不认为发生了有效修改。易错点是把 change 当成 input 用,结果发现输入过程中没有反应;或者把它当成 blur 用,结果发现没改内容时不会触发。
在用户名验证中注册 change
综合案例里,用户名输入框通过 name 属性获取:
js
const userName = document.querySelector('[name="userName"]');为什么用属性选择器?因为案例中的 input 没有直接给一个方便获取的类名,但它有 name="userName",用属性选择器同样能拿到元素。
怎么想?验证逻辑后面还要在“下一步提交”时复用,所以不能只写匿名回调,而要把验证封装成有名字的函数 verifyName。怎么做?给 input 注册 change,回调里调用 verifyName:
js
userName.addEventListener('change', verifyName);
function verifyName() {
if (!regName.test(userName.value)) {
userName.nextElementSibling.innerHTML = '请输入6到10位...';
return false;
}
userName.nextElementSibling.innerHTML = '';
return true;
}这里 regName 是案例中提前准备好的用户名正则,规则是英文字母、数字、短横线、下划线,6 到 10 位。易错点是错误信息不能随便用 querySelector('.message') 获取,因为页面里多个表单项都有 message 盒子,直接查会拿到第一个。正确思路是:当前 input 的下一个兄弟元素就是它对应的提示 span,所以用 userName.nextElementSibling。
调试过程:错误提示出现后还要会清空
转写稿里的调试过程很典型:先输入不符合规则的内容,再让输入框失去焦点,页面出现“请输入6到10位……”的提示,说明 change 和错误分支生效了。但接着输入合法内容再失焦,发现之前的错误提示没有消失。原因是没有在合法分支里清空提示盒子。
怎么改?在 return true 之前,把当前 input 的下一个兄弟 span 清空:
js
userName.nextElementSibling.innerHTML = '';
return true;易错点是清空和 return true 的先后顺序。必须先把提示清空,再返回 true;如果先 return true,后面的清空代码就不会执行。另一个易错点是 return false 要写在错误提示之后,因为函数遇到 return 会立即结束,后面的代码不再执行。这也是为什么案例里先写“不合法”的分支,合法时直接往下执行并返回 true,从而省掉一个 else。
与 blur、input 的对比
为什么案例不直接用 blur?blur 只要失去焦点就触发,即使用户没有输入内容、值没有变化,也会验证一次。change 要求值发生变化再失焦,能避免无效验证。为什么不用 input?input 在每次输入时都触发,用户还没输完就可能不断提示错误。change 更适合“输完再校验”的场景。
易错点总结:change 的触发条件是“值被修改并且失去焦点”;验证函数要返回 true/false,为后续提交模块复用;提示信息写到当前 input 的下一个兄弟元素;合法时先清空提示再 return true。掌握这些,change 事件就能稳定用于注册页表单验证。
课件图示








代码示例
示例一:发送验证码 5 秒倒计时(含防止多次点击)
js
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>发送验证码倒计时</title>
<style>
.code-btn { color: #27ba9b; text-decoration: none; cursor: pointer; }
</style>
</head>
<body>
<a href="javascript:;" class="code-btn" id="codeBtn">发送验证码</a>
<script>
// 1. 获取元素(给 A 链接注册点击事件)
const codeBtn = document.querySelector('#codeBtn')
// 2. 开关变量:用来防止多次点击
let flag = true
codeBtn.addEventListener('click', function () {
// flag 为 true 才允许执行
if (flag) {
let i = 5
// 点击后立马变化文字,省掉 1 秒等待
codeBtn.innerHTML = `${i}秒后重新获取`
// 关闭开关,倒计时期间再点击就不执行了
flag = false
// 3. 用定时器做倒计时
const timerId = setInterval(function () {
i--
codeBtn.innerHTML = `${i}秒后重新获取`
// 时间到了
if (i === 0) {
codeBtn.innerHTML = '重新获取'
clearInterval(timerId) // 停止定时器,不然会一直减下去
flag = true // 打开开关,允许下一轮点击
}
}, 1000)
}
})
</script>
</body>
</html>关键行与易错点
flag = false必须写在if (flag) { }里面,代码执行完一次后立即关闭开关,否则连点会重复发送验证码。- 定时器里不能用
this,因为定时器回调的调用者是window,统一用codeBtn更稳妥。 clearInterval(timerId)一定要写,并配合flag = true一起放在i === 0的分支里,否则时间到了也点不了下一轮。- 先改文字再开定时器,点击后立即出现“05 秒后重新获取”,否则要空等 1 秒才变化。
示例二:change 事件验证用户名
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>用户名验证</title>
<style>
.message { color: red; font-size: 12px; }
</style>
</head>
<body>
<div>
<input type="text" name="username" placeholder="请输入用户名">
<span class="message"></span>
</div>
<script>
// 正则:6~10 位 英文字母、数字、短横线、下划线
const REG_NAME = /^[a-zA-Z0-9_-]{6,10}$/
// 用属性选择器获取表单
const userName = document.querySelector('[name="username"]')
function verifyName() {
// 不符合条件:写错误信息,并 return false
if (!REG_NAME.test(userName.value)) {
userName.nextElementSibling.innerHTML = '请输入6到10位的用户名'
return false
}
// 符合条件:先清空提示,再 return true
userName.nextElementSibling.innerHTML = ''
return true
}
// change:值被修改 并且 失去焦点后触发
userName.addEventListener('change', verifyName)
</script>
</body>
</html>关键行与易错点
change与blur、input不同:input是每输入一个字符就触发;blur只要失焦就触发(没输入内容也会验证);change要求值发生变化且失去焦点才触发,效率更高。- 页面上有多个
.message,不能用querySelector('.message')单独获取(只能拿到第一个),要用nextElementSibling拿到当前 input 的下一个兄弟span。 - 清空提示的
innerHTML = ''一定要写在return true之前,return后面的代码不再执行。 - 不管成功失败都要
return true / return false,因为后面提交按钮要靠这个返回值判断表单是否通过。
示例三:同意协议切换 + 提交验证
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>注册页面提交验证</title>
<style>
.form-item { margin-bottom: 12px; }
.message { color: red; font-size: 12px; margin-left: 6px; }
.icon-queren {
display: inline-block; width: 16px; height: 16px;
border: 1px solid #999; border-radius: 50%;
cursor: pointer; vertical-align: middle;
}
.icon-queren2 { background: #27ba9b; border-color: #27ba9b; }
</style>
</head>
<body>
<form class="register-form">
<div class="form-item">
<input type="text" name="username" placeholder="设置用户名">
<span class="message"></span>
</div>
<div class="form-item">
<input type="password" name="password" placeholder="设置密码">
<span class="message"></span>
</div>
<div class="form-item">
<input type="password" name="confirm" placeholder="再次确认密码">
<span class="message"></span>
</div>
<div class="form-item">
<i class="icon-queren"></i> 我已阅读并同意协议
</div>
<button type="submit">下一步</button>
</form>
<script>
const REG_NAME = /^[a-zA-Z0-9_-]{6,10}$/
const REG_PWD = /^[a-zA-Z0-9_-]{6,20}$/
const userName = document.querySelector('[name="username"]')
const password = document.querySelector('[name="password"]')
const confirm = document.querySelector('[name="confirm"]')
const queren = document.querySelector('.icon-queren')
const form = document.querySelector('.register-form')
function verifyName() {
if (!REG_NAME.test(userName.value)) {
userName.nextElementSibling.innerHTML = '请输入6到10位的用户名'
return false
}
userName.nextElementSibling.innerHTML = ''
return true
}
function verifyPwd() {
if (!REG_PWD.test(password.value)) {
password.nextElementSibling.innerHTML = '请输入6到20位的密码'
return false
}
password.nextElementSibling.innerHTML = ''
return true
}
// 再次确认密码:不需要正则,只判断两次输入是否一致
function verifyConfirm() {
if (confirm.value !== password.value) {
confirm.nextElementSibling.innerHTML = '两次密码不一致'
return false
}
confirm.nextElementSibling.innerHTML = ''
return true
}
userName.addEventListener('change', verifyName)
password.addEventListener('change', verifyPwd)
confirm.addEventListener('change', verifyConfirm)
// 阅读同意业务:有就删,没有就加
queren.addEventListener('click', function () {
queren.classList.toggle('icon-queren2')
})
// 提交业务
form.addEventListener('submit', function (e) {
// 1. 没勾选协议,阻止提交并提示
if (!queren.classList.contains('icon-queren2')) {
e.preventDefault()
alert('请勾选同意协议')
return
}
// 2. 调用验证函数,返回 false 说明没通过,阻止提交
if (!verifyName()) { e.preventDefault() }
if (!verifyPwd()) { e.preventDefault() }
if (!verifyConfirm()) { e.preventDefault() }
})
</script>
</body>
</html>关键行与易错点
classList.toggle('icon-queren2'):类名有就去掉、没有就加上,正好实现“点一下高亮、再点取消”。classList.contains('icon-queren2')返回布尔值,取反!后才是“没勾选”,这一步方向写反会导致逻辑完全颠倒。- 提交时先判断协议,再依次调用各验证函数;
if (!verifyName())中,函数返回false说明没通过,取反成true才进入阻止提交的分支。 - 阻止表单跳转用
e.preventDefault(),注意是方法调用要带括号;submit事件的形参e不要漏写。
常见错误
倒计时期间狂点按钮,验证码被重复"发送" 现象:连点几下,秒数被重置、短信连发。 原因:点击回调里没有任何"是否允许再次执行"的判断,每次点击都新开一个定时器。 修法:在回调外层定义
let flag = true;进入回调先if (flag),执行完立刻flag = false;在i === 0的分支里恢复flag = true。注意flag = false必须写在if的大括号内部。点击后要愣一秒才变成"05秒后重新获取" 现象:点下去没反应,一秒后才跳字。 原因:修改文字只写在定时器回调里,第一次执行要等 1000ms。 修法:把"改文字"的语句在开启定时器之前先手动执行一遍,定时器内再执行同样的语句。
定时器里写
this.innerHTML不生效或报错 现象:文字不变,控制台报错或改到了别处。 原因:定时器回调由 window 调用,this指向 window,不是被点的元素。 修法:统一用提前保存好的元素变量(如codeBtn),在定时器内外都不使用this。倒计时到 0 后文字变成"重新获取",但数字还在往下走 现象:出现 -1、-2 秒。 原因:只改了文字,没有
clearInterval,且定时器 id 没保存。 修法:const timerId = setInterval(...),在i === 0分支里clearInterval(timerId),与文字复位写在同一个分支。所有表单的错误提示都跑到用户名下面去了 现象:验证手机号,红字却出现在第一个输入框下方。 原因:用
querySelector('.message')取提示盒子,页面上有多个同名元素,只能拿到第一个。 修法:用 DOM 关系定位——input.nextElementSibling,验证哪个框就写哪个框的下一个兄弟。点"下一步"时校验函数不生效,页面直接跳走 现象:明明写错也提交了。 原因:验证函数只在某个分支
return false,通过分支没写return true(返回undefined);或者忘了e.preventDefault()。 修法:每个验证函数的所有分支都要有明确返回值;submit 回调第一行就e.preventDefault(),再用if (!verifyXxx())判断。复制粘贴写完四个表单后,验证一个框其他框跟着报错 现象:改手机号,用户名的提示也变了。 原因:复制后漏改元素获取、正则变量、函数名、提示文案中的某一处。 修法:固定"四处替换"清单——元素、正则、函数名、文案;每改完一个立刻保存并单独测试该框。
练习
倒计时补零:把 5 秒改成 60 秒,要求任何时刻都显示两位数字(
60、09、00),提示文字格式为"09秒后重新获取"。改造完成后,测试点击瞬间是否立即显示60。空值分支:给用户名校验增加一条规则——输入为空时提示"用户名不能为空",且空值也返回
false。写完后回答:用change事件时,用户"点进去又直接点出来、一个字没打",这条提示会不会出现?为什么?如果希望它出现,应该换什么事件?change vs blur 对比实验:把用户名验证临时改成
blur事件,分别测试以下四种操作并列表记录两者的触发情况:①未输入直接离开;②输入123后离开;③再次点回输入框、不做修改直接离开;④输入123后改成1234再离开。根据结果写出结论:本案例为什么选change。提交时定位第一个错误:在 submit 回调中依次调用五个校验函数,若某个返回
false,除了阻止提交外,还要让该输入框获得焦点(focus())。要求:只聚焦第一个未通过的框,后面的不再继续检查或不再抢焦点。(进阶)配置化改造:把用户名、手机号、验证码、密码四个校验函数合并成一个通用函数,用一个数组保存
{ 元素, 正则, 提示文案 },循环绑定change事件。改造完成后,新增一条"邮箱"校验只需要往数组里加一项。思考:二次确认密码为什么不适合放进这个数组?
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 综合案例-注册页面布局分析和业务分析 | 03:58 |
| 2 | 综合案例-注册页面倒计时效果制作 | 08:15 |
| 3 | 综合案例-注册页面-防止多次点击倒计时思路 | 06:08 |
| 4 | 综合案例-注册页面验证用户名 | 14:28 |
| 5 | 综合案例-注册页面其余表单验证 | 09:48 |
| 6 | 综合案例-注册页面同意协议业务制作 | 05:18 |
| 7 | 综合案例-注册页面勾选同意协议之制作 | 09:03 |
| 8 | 综合案例-注册页面提交模块完成 | 07:49 |
小结
- 表单验证的通用骨架是"绑事件 → 取值 → 校验 → 反馈 → 返回布尔值",其中返回值是给提交环节用的契约,缺一个分支就会让后面的判断静默失效。
- 一个校验一个具名函数,命名清晰可读,复制粘贴才是有意义的复用;复制后必须按"元素 / 正则 / 函数名 / 文案"四处逐一替换并单独验证。
- 定时器必须有明确的停止条件,停止动作要和 UI 复位写在同一个分支,否则会出现"文字复位了、定时器还在跑"的错位状态。
- 凡是"进行期间不允许重复触发"的交互,开关变量的优先级高于样式禁用;开关的关闭要放在首次执行之后,打开要放在流程真正结束之处。
- 提示信息优先用 DOM 关系(
nextElementSibling)定位,而不是全局选择器——这样同一套结构可以无限复用,也不会出现"提示串台"。 submit回调里先阻止默认行为再校验,是顺序问题不是习惯问题:一旦让页面跳走,后面的提示和聚焦都来不及执行。- 类名在本案例中同时承担"样式开关"和"状态存储"两个职责,判断状态用
classList.contains,操作状态用add / remove / toggle,不要靠读样式反推状态。 - 事件选型要看"业务语义":
change表达的是"值变了且确认完毕",天然避免了用户没输入、没改动时的无效校验;blur表达的是"离开",两者不等价。