Skip to content

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 事件就能稳定用于注册页表单验证。

课件图示 ​

课件第35页:步骤 小兔鲜页面注册 业务②: 表单验证业务 1. 用户名验证(注意封装函数 v

课件第28页:可以帮我们快速生成正则表达式 any-rule 推荐一个vscode正则插件

课件第5页:1.1.1 什么是正则表达式 l 正则表达式(Regular Expressio

课件第6页:1.1.2 正则基本使用 l 正则基本使用分为两步: 定义规则 使用正则 l c

课件第7页:总结 总结 1.正则表达式是什么? Ø 是一种字符串匹配的模式(规则) 2.正则

课件第20页:案例 用户名验证案例 需求:用户名要求用户英文字母,数字,下划线或者短横线组成,

课件第26页:1.3 替换和修饰符 l 修饰符约束正则执行的某些细节行为,如是否区分大小写、是

课件第27页:案例 利用正则隐藏手机号中间四位 分析: ①:手机号前三位显示,中间四位隐藏为*

代码示例 ​

示例一:发送验证码 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 不要漏写。

常见错误 ​

  1. 倒计时期间狂点按钮,验证码被重复"发送" 现象:连点几下,秒数被重置、短信连发。 原因:点击回调里没有任何"是否允许再次执行"的判断,每次点击都新开一个定时器。 修法:在回调外层定义 let flag = true;进入回调先 if (flag),执行完立刻 flag = false;在 i === 0 的分支里恢复 flag = true。注意 flag = false 必须写在 if 的大括号内部。

  2. 点击后要愣一秒才变成"05秒后重新获取" 现象:点下去没反应,一秒后才跳字。 原因:修改文字只写在定时器回调里,第一次执行要等 1000ms。 修法:把"改文字"的语句在开启定时器之前先手动执行一遍,定时器内再执行同样的语句。

  3. 定时器里写 this.innerHTML 不生效或报错 现象:文字不变,控制台报错或改到了别处。 原因:定时器回调由 window 调用,this 指向 window,不是被点的元素。 修法:统一用提前保存好的元素变量(如 codeBtn),在定时器内外都不使用 this。

  4. 倒计时到 0 后文字变成"重新获取",但数字还在往下走 现象:出现 -1、-2 秒。 原因:只改了文字,没有 clearInterval,且定时器 id 没保存。 修法:const timerId = setInterval(...),在 i === 0 分支里 clearInterval(timerId),与文字复位写在同一个分支。

  5. 所有表单的错误提示都跑到用户名下面去了 现象:验证手机号,红字却出现在第一个输入框下方。 原因:用 querySelector('.message') 取提示盒子,页面上有多个同名元素,只能拿到第一个。 修法:用 DOM 关系定位——input.nextElementSibling,验证哪个框就写哪个框的下一个兄弟。

  6. 点"下一步"时校验函数不生效,页面直接跳走 现象:明明写错也提交了。 原因:验证函数只在某个分支 return false,通过分支没写 return true(返回 undefined);或者忘了 e.preventDefault()。 修法:每个验证函数的所有分支都要有明确返回值;submit 回调第一行就 e.preventDefault(),再用 if (!verifyXxx()) 判断。

  7. 复制粘贴写完四个表单后,验证一个框其他框跟着报错 现象:改手机号,用户名的提示也变了。 原因:复制后漏改元素获取、正则变量、函数名、提示文案中的某一处。 修法:固定"四处替换"清单——元素、正则、函数名、文案;每改完一个立刻保存并单独测试该框。

练习 ​

  1. 倒计时补零:把 5 秒改成 60 秒,要求任何时刻都显示两位数字(60、09、00),提示文字格式为"09秒后重新获取"。改造完成后,测试点击瞬间是否立即显示 60。

  2. 空值分支:给用户名校验增加一条规则——输入为空时提示"用户名不能为空",且空值也返回 false。写完后回答:用 change 事件时,用户"点进去又直接点出来、一个字没打",这条提示会不会出现?为什么?如果希望它出现,应该换什么事件?

  3. change vs blur 对比实验:把用户名验证临时改成 blur 事件,分别测试以下四种操作并列表记录两者的触发情况:①未输入直接离开;②输入 123 后离开;③再次点回输入框、不做修改直接离开;④输入 123 后改成 1234 再离开。根据结果写出结论:本案例为什么选 change。

  4. 提交时定位第一个错误:在 submit 回调中依次调用五个校验函数,若某个返回 false,除了阻止提交外,还要让该输入框获得焦点(focus())。要求:只聚焦第一个未通过的框,后面的不再继续检查或不再抢焦点。

  5. (进阶)配置化改造:把用户名、手机号、验证码、密码四个校验函数合并成一个通用函数,用一个数组保存 { 元素, 正则, 提示文案 },循环绑定 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

小结 ​

  1. 表单验证的通用骨架是"绑事件 → 取值 → 校验 → 反馈 → 返回布尔值",其中返回值是给提交环节用的契约,缺一个分支就会让后面的判断静默失效。
  2. 一个校验一个具名函数,命名清晰可读,复制粘贴才是有意义的复用;复制后必须按"元素 / 正则 / 函数名 / 文案"四处逐一替换并单独验证。
  3. 定时器必须有明确的停止条件,停止动作要和 UI 复位写在同一个分支,否则会出现"文字复位了、定时器还在跑"的错位状态。
  4. 凡是"进行期间不允许重复触发"的交互,开关变量的优先级高于样式禁用;开关的关闭要放在首次执行之后,打开要放在流程真正结束之处。
  5. 提示信息优先用 DOM 关系(nextElementSibling)定位,而不是全局选择器——这样同一套结构可以无限复用,也不会出现"提示串台"。
  6. submit 回调里先阻止默认行为再校验,是顺序问题不是习惯问题:一旦让页面跳走,后面的提示和聚焦都来不及执行。
  7. 类名在本案例中同时承担"样式开关"和"状态存储"两个职责,判断状态用 classList.contains,操作状态用 add / remove / toggle,不要靠读样式反推状态。
  8. 事件选型要看"业务语义":change 表达的是"值变了且确认完毕",天然避免了用户没输入、没改动时的无效校验;blur 表达的是"离开",两者不等价。