Skip to content

综合案例:学习反馈 ​

本页对应 1-14综合案例-学习反馈,共 4 段视频,约 40 分钟。

学习目标 ​

  • 能够调用省份列表接口,并用 map 拼接 <option> 字符串、通过 innerHTML 渲染到下拉框,同时保留首项默认选项。
  • 能够为省份、城市下拉框注册 change 事件,在普通函数的回调中用 this.value 取到选中项的值,并据此发起下一级数据请求。
  • 能够使用 async/await 结合 axios 的 params 参数,按 pname、cname 依次获取城市与地区数据,并说明 await 必须写在 async 函数内部的原因。
  • 能够在更换上级选项时清空下级下拉框内容,只保留默认选项,并复现“不清空导致地区残留上一次状态”的问题。
  • 能够引入 form-serialize 插件,传入 form 表单与 { hash: true, empty: true } 配置,一次性收集表单数据,并与接口文档要求的字段名核对一致。
  • 能够用 axios 以 POST 方式提交反馈数据,配合 try/catch 捕获异常,分别用 alert 弹出成功信息或 error.response.data.message 中的错误提示。

知识点 ​

案例目标与效果 ​

本节要完成“学习反馈”案例中的省市区切换与反馈提交:省份默认加载;选择省份后加载城市;选择城市后加载地区;更换省份时城市更新、地区清空;最后收集表单数据提交并提示结果。

省市区切换效果

实现顺序按笔记拆成四步:省份列表、城市列表、地区列表、反馈提交。核心思路都是“注册事件 → 获取数据 → 渲染数据”,但每一步都有需要留意的细节。

省份列表 ​

为什么需要:页面打开后,省份下拉框不能是空的,否则用户没有可选入口;同时要保留一个默认“省份”选项,避免用户未选择时出现空值状态。

怎么想:先调接口拿省份数据,再把数组映射成多个 option,最后拼上默认选项一起渲染到下拉框。转写稿中反复强调“拿数据、渲染数据”两步。

怎么做:用 axios 调省份列表接口,await 等待结果;用 map 生成 option,用 join('') 拼接;通过 innerHTML 写入 .province。代码结构可整理为:

js
async function getProvince() {
  const res = await axios({
    url: '省份列表接口地址(按接口文档复制)',
    method: 'get'
  })

  const html = res.data.list
    .map(v => `<option value="${v}">${v}</option>`)
    .join('')

  document.querySelector('.province').innerHTML =
    `<option value="">省份</option>` + html
}
getProvince()

易错点:await 必须写在 async 函数内;默认“省份”选项要手动保留;value 和标签文本都要替换成真实数据,不能只改显示文本。转写稿中通过 console.log(res) 确认拿到了 data.list,再检查页面下拉框是否出现省份。

城市列表 ​

为什么需要:城市依赖省份。用户选择省份后,第二个下拉框才应该出现对应城市;同时更换省份时,旧城市不能残留。

怎么想:给省份下拉框注册 change 事件;在事件回调里通过 this.value 拿到选中的省份;再调城市接口;渲染城市列表,并保留默认“城市”选项。

怎么做:注意这里用普通函数而不是箭头函数,因为要用 this 指向触发事件的 select。核心代码如下:

js
document.querySelector('.province').addEventListener('change', async function () {
  const pname = this.value

  const res = await axios({
    url: '城市列表接口地址(按接口文档复制)',
    params: { pname }
  })

  const html = res.data.list
    .map(v => `<option value="${v}">${v}</option>`)
    .join('')

  document.querySelector('.city').innerHTML =
    `<option value="">城市</option>` + html
})

易错点:this.value 取到的是选中 option 的 value,所以生成 option 时必须设置 value;async 不能漏,否则 await 会报错。转写稿中先 console.log(this.value),分别选北京、天津验证取值,再打印接口结果确认城市数据。

地区列表 ​

为什么需要:地区依赖城市,选择城市后才加载地区;更关键的是,用户先选过地区再更换省份时,旧地区必须清空,否则会出现“北京下面还显示新华区”的错误状态。

怎么想:地区列表本身仍是“注册城市 change → 获取地区数据 → 渲染”。清空地区则放在省份 change 里:一旦省份变化,就把地区下拉框重置为默认“地区”选项。

怎么做:地区接口需要省份和城市两个参数;城市值用 this.value,省份值从 .province 的 value 取。渲染时保留默认地区选项。清空逻辑可写在城市列表的省份事件末尾:

js
document.querySelector('.city').addEventListener('change', async function () {
  const cname = this.value
  const pname = document.querySelector('.province').value

  const res = await axios({
    url: '地区列表接口地址(按接口文档复制)',
    params: { pname, cname }
  })

  const html = res.data.list
    .map(v => `<option value="${v}">${v}</option>`)
    .join('')

  document.querySelector('.area').innerHTML =
    `<option value="">地区</option>` + html
})

// 在省份 change 中补充:更换省份时清空地区
document.querySelector('.area').innerHTML = `<option value="">地区</option>`

易错点:地区清空不要漏。转写稿的复现过程是:选河北省—石家庄市—新华区,再换北京,城市更新了但地区仍停在新华区;补上清空后再次测试,地区恢复为默认选项。接口参数名以文档为准,转写稿中地区使用 pname、cname 两个查询参数。

反馈提交 ​

为什么需要:省市区只是表单的一部分,最终要把省、市、区、称呼、建议一起提交,并根据服务器返回提示成功或失败。

怎么想:给提交按钮注册点击事件;用 form-serialize 快速收集表单数据;用 axios 的 post 提交;成功 alert(res.data.message),失败用 try/catch 捕获并提示 error.response.data.message。

怎么做:表单需有 name 属性,插件才能收集到 province、city、area、nickname、feedback。核心代码:

js
document.querySelector('.submit').addEventListener('click', async () => {
  const form = document.querySelector('.info-form')
  const data = serialize(form, { hash: true, empty: true })

  try {
    const res = await axios({
      url: '学习反馈接口地址(按接口文档复制)',
      method: 'post',
      data
    })
    alert(res.data.message)
  } catch (error) {
    alert(error.response.data.message)
  }
})

易错点:表单元素没有 name 就收集不到;提交接口是 post;await 所在函数必须是 async;失败分支不要只看控制台报错,要取服务器响应里的 message 提示用户。转写稿中先测成功提交,再删除建议测试失败,最后用 try/catch 完成成功、失败两种弹框。

小结 ​

本节四个需求都围绕“事件驱动数据更新”:省份默认渲染;省份变化更新城市并清空地区;城市变化更新地区;提交按钮收集并提交表单。最容易被忽略的是 this.value、async/await 配对、默认选项保留,以及更换省份时清空地区。

本节目标与效果 ​

本节要完成学习反馈案例的最后一个需求:收集用户选择/输入的数据,提交到服务器,并根据返回结果提示用户。效果如下:选择省市区、填写称呼和建议后点击提交;成功时弹框提示成功,失败时弹框提示错误信息。

学习反馈提交效果

为什么需要:前面已经完成省份、城市、地区列表,但数据还停留在页面上,只有提交保存,反馈信息才真正进入服务器。
怎么想:点击提交按钮后才执行提交逻辑,所以先注册点击事件;表单数据用插件快速收集;再用 axios 发请求;最后用 alert 显示成功或失败。
怎么做:按“注册事件 → 收集表单数据 → 提交并提示结果”三步走。
易错点:表单元素必须有 name,且名称要与接口参数一致;await 必须写在 async 函数里。

步骤拆解:先事件,再收集,再提交提示 ​

转写稿里把步骤拆得很清楚:第一步注册事件,第二步收集表单数据并提交,第三步根据服务器响应提示用户。

为什么需要:如果不先注册点击事件,后续逻辑没有触发入口;如果不先收集数据,axios 没有可提交的内容。
怎么想:先拿到提交按钮和表单元素,再在点击回调里做收集和提交。
怎么做:提交按钮类名是 submit,表单类名是 info-form;表单里省、市、区、称呼、建议都要有 name,分别对应接口参数 province、city、area、nickname、feedback。
易错点:不要等到提交时才去找表单;表单元素缺少 name 时,serialize 收集不到对应字段。

注册提交按钮点击事件 ​

js
const submitBtn = document.querySelector('.submit')

submitBtn.addEventListener('click', async () => {
  // 后续收集与提交逻辑
})

为什么需要:用户只有点击“提交”时才需要保存反馈,不能在页面打开时自动提交。
怎么想:事件源是提交按钮,事件类型是 click;本节回调里不需要 this,所以可以用箭头函数。
怎么做:用 querySelector 找到 .submit,再用 addEventListener('click', ...) 注册。
易错点:转写稿里提到后面要用 await,因此回调函数要加 async;漏写会在使用 await 时报错。

用 form-serialize 收集表单数据 ​

js
const form = document.querySelector('.info-form')
const data = serialize(form, { hash: true, empty: true })
console.log(data)

为什么需要:手动一个个取输入框的值很繁琐,插件可以把表单内带 name 的控件一次性收集成对象。
怎么想:先拿到表单,再调用 serialize;hash: true 让结果变成对象,empty: true 表示空值也收集,方便后端校验。
怎么做:页面已经导入 form-serialize 插件,直接调用 serialize(form, { hash: true, empty: true })。调试时先 console.log(data),检查是否正好有 5 个字段。
易错点:转写稿里的“form a nice”是 form-serialize;如果字段少,先检查表单元素的 name 是否与接口参数一致。

用 axios 提交并区分成功失败 ​

js
submitBtn.addEventListener('click', async () => {
  try {
    const data = serialize(form, { hash: true, empty: true })

    const res = await axios({
      url: '从接口文档复制的学习反馈接口地址',
      method: 'post',
      data
    })

    alert(res.data.message)
  } catch (error) {
    console.dir(error)
    alert(error.response.data.message)
  }
})

为什么需要:提交可能成功,也可能因为未填写建议等原因失败;成功和失败都要告诉用户。
怎么想:接口是 POST,参数就是收集到的 data,变量名与属性名相同可简写为 data;用 await 等待结果,用 try/catch 捕获异常。
怎么做:成功时读取 res.data.message 并 alert;失败时先 console.dir(error) 调试,再读取 error.response.data.message 并 alert。转写稿测试时,删除建议后提交,控制台能看到服务器返回的错误信息。
易错点:await 必须放在 async 函数内;错误信息不在 error.message,而在 error.response.data.message;接口地址以接口文档为准,不要手写错。

调试复盘与易错点 ​

为什么需要:这类提交题目的问题通常出在“数据没收集全”或“异常没捕获”。
怎么想:先看 serialize 打印的对象是否有 5 个字段,再看 axios 返回的 res.data.message,失败时看 error.response.data.message。
怎么做:先测成功流程:选北京、北京市、朝阳区,填写称呼和建议,点击提交,看到成功弹框;再测失败流程:清空建议后提交,看到错误弹框。
易错点:async/await 不要漏;try/catch 要包住 await axios;表单 name 与接口参数 province、city、area、nickname、feedback 要一一对应;本节按转写稿直接监听按钮点击,若你的按钮是 submit 类型并出现页面刷新,再检查表单默认提交行为。

课件图示 ​

课件第8页:需求: 展示数据到下拉框中 1. 获取省份数据并展示第1个省 2. 获取第1个省

课件第14页:Promise-链式调用-解决回调地狱 需求:展示数据到下拉框中(使用Promi

代码示例 ​

代码 1:省份列表渲染 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>学习反馈 - 省份列表</title>
</head>
<body>
  <select class="province"></select>

  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    // 接口地址以接口文档为准
    const URL_PROVINCE = '/api/province'

    async function getProvince() {
      // 第一步:拿数据
      const res = await axios({ url: URL_PROVINCE, method: 'get' })

      // 第二步:渲染数据,省份在 res.data.list
      const html = res.data.list
        .map(v => `<option value="${v}">${v}</option>`)
        .join('')

      // 最前面保留一个默认选项,用户不选时也有初始显示
      document.querySelector('.province').innerHTML =
        '<option value="">省份</option>' + html
    }

    getProvince()
  </script>
</body>
</html>

关键行与易错点

  • await 必须写在 async 函数内部,漏写 async 会直接报错(“await is only valid in async function”)。
  • axios({ url }) 不写 method 时默认就是 GET,笔记里强调“方法 get 不写”。
  • 渲染时要手动拼上默认选项 <option value="">省份</option>,否则用户没选时下拉框是空白的。
  • option 一定要写 value,后面 select.value 才能取到选中的值;不写 value 时取到的是空字符串。

代码 2:城市 / 地区列表联动(含清空地区) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>学习反馈 - 城市 / 地区列表</title>
</head>
<body>
  <select class="province"></select>
  <select class="city"></select>
  <select class="area"></select>

  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    // 接口地址以接口文档为准
    const URL_PROVINCE = '/api/province'
    const URL_CITY = '/api/city'
    const URL_AREA = '/api/area'

    // 1. 省份列表
    async function getProvince() {
      const res = await axios({ url: URL_PROVINCE })
      const html = res.data.list.map(v => `<option value="${v}">${v}</option>`).join('')
      document.querySelector('.province').innerHTML = '<option value="">省份</option>' + html
    }
    getProvince()

    // 2. 切换省份 -> 城市列表
    document.querySelector('.province').addEventListener('change', async function () {
      const res = await axios({ url: URL_CITY, params: { pname: this.value } })
      const html = res.data.list.map(v => `<option value="${v}">${v}</option>`).join('')
      document.querySelector('.city').innerHTML = '<option value="">城市</option>' + html

      // 2.4 换省时必须清空地区列表(只留默认项)
      document.querySelector('.area').innerHTML = '<option value="">地区</option>'
    })

    // 3. 切换城市 -> 地区列表
    document.querySelector('.city').addEventListener('change', async function () {
      const res = await axios({
        url: URL_AREA,
        params: {
          cname: this.value,                              // 城市
          pname: document.querySelector('.province').value // 省份
        }
      })
      const html = res.data.list.map(v => `<option value="${v}">${v}</option>`).join('')
      document.querySelector('.area').innerHTML = '<option value="">地区</option>' + html
    })
  </script>
</body>
</html>

关键行与易错点

  • 事件回调写成普通函数 function () {},才能用 this.value 拿到当前 select 中选中 option 的 value;写成箭头函数 this 就丢了。
  • 城市接口参数名是 pname(省份),地区接口是 cname(城市)+ pname(省份,从 .province 里取),参数放在 params(查询参数)里。
  • 切换省份必须清空地区列表,否则会出现“选了北京、地区还停在上一次的石家庄新华区”的脏数据,笔记里专门用 todo 标记这一步。
  • 三个下拉框都要保留各自的默认选项(省份 / 城市 / 地区)。
  • 事件用 change,不是 click 也不是 input。

代码 3:学习反馈数据提交 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>学习反馈 - 数据提交</title>
</head>
<body>
  <form class="info-form">
    <select class="province" name="province"></select>
    <select class="city" name="city"></select>
    <select class="area" name="area"></select>
    <input type="text" name="nickname" placeholder="请输入您的称呼" />
    <textarea name="feedback" placeholder="请输入您的宝贵建议"></textarea>
    <button type="button" class="submit">提交</button>
  </form>

  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/form-serialize"></script>
  <script>
    // 接口地址以接口文档为准
    const URL_PROVINCE = '/api/province'
    const URL_CITY = '/api/city'
    const URL_AREA = '/api/area'
    const URL_FEEDBACK = '/api/feedback'

    // —— 省市区渲染(同代码 2)——
    async function getProvince() {
      const res = await axios({ url: URL_PROVINCE })
      const html = res.data.list.map(v => `<option value="${v}">${v}</option>`).join('')
      document.querySelector('.province').innerHTML = '<option value="">省份</option>' + html
    }
    getProvince()

    document.querySelector('.province').addEventListener('change', async function () {
      const res = await axios({ url: URL_CITY, params: { pname: this.value } })
      const html = res.data.list.map(v => `<option value="${v}">${v}</option>`).join('')
      document.querySelector('.city').innerHTML = '<option value="">城市</option>' + html
      document.querySelector('.area').innerHTML = '<option value="">地区</option>'
    })

    document.querySelector('.city').addEventListener('change', async function () {
      const res = await axios({
        url: URL_AREA,
        params: { cname: this.value, pname: document.querySelector('.province').value }
      })
      const html = res.data.list.map(v => `<option value="${v}">${v}</option>`).join('')
      document.querySelector('.area').innerHTML = '<option value="">地区</option>' + html
    })

    // 1. 注册提交事件
    document.querySelector('.submit').addEventListener('click', async () => {
      const form = document.querySelector('.info-form')

      // 2. 依靠 form-serialize 插件收集表单数据
      const data = serialize(form, { hash: true, empty: true })

      try {
        // 3. 基于 axios 提交,成功提示
        const res = await axios({ url: URL_FEEDBACK, method: 'post', data })
        alert(res.data.message)
      } catch (error) {
        // 失败提示(服务器返回的错误信息)
        alert(error.response.data.message)
      }
    })
  </script>
</body>
</html>

关键行与易错点

  • serialize(form, { hash: true, empty: true }):hash: true 返回对象(不是字符串),empty: true 保留空值字段,插件返回的对象直接符合接口要求。
  • 表单里每个控件必须有 name,且与接口参数名一一对应(province、city、area、nickname、feedback),否则收集出来的对象字段对不上。
  • POST 请求体用 data 传,不是 params;变量名恰好也叫 data,所以可以简写成 data。
  • 用了 await 就要用 try...catch 兜住异常,错误信息在 error.response.data.message(先 error.response,再 data,再 message,不要写漏层级)。
  • 提交按钮要写 type="button",否则在 form 里默认是 submit,点击会触发表单默认提交、页面刷新。

常见错误 ​

  1. 现象:给省份下拉框绑定 change 后,控制台报错 await is only valid in async functions,或者城市列表完全不刷新。 原因:在回调里写了 await axios(...),但回调函数本身没加 async;或者误用箭头函数导致 this 不是 select 元素。 修法:把回调声明为 async function () {...},async 写在 function 前面而不是里面;需要 this.value 就必须用普通函数,不能改成箭头函数。

  2. 现象:select.value 打印出来是空字符串,或者拿到的是显示文字而非提交值。 原因:动态拼接 option 时只写了标签内容 <option>北京</option>,漏了 value="北京";或者默认那个“请选择省份”的 option 忘了给 value。 修法:模板字符串里同时写 value="${v}" 和 >${v}<,默认项写 value="",保证 this.value 能取到值、取不到时是空串而不是文字。

  3. 现象:先选河北省石家庄市新华区,再切到北京,城市变了但地区还是“新华区”,提交上去省市区对不上。 原因:切换省份时只更新了城市列表,没有清空地区下拉框,第三个下拉框保留了上一次的选项。 修法:在省份 change 回调末尾,把地区 select 的 innerHTML 重置为只含默认 option 的字符串(用变量存一份默认模板,避免手写重复)。测法:选完三级再换省,看地区是否回落为默认。

  4. 现象:点击提交,form-serialize 返回 {} 或只有部分字段,接口报参数缺失。 原因:serialize 只收集带 name 属性的表单控件,页面上漏写 name,或 name 与接口文档的字段名(province/city/area/nickname/feedback)拼写不一致。 修法:逐个核对五个控件的 name,与接口文档对照;同时确认 serialize(form, { hash: true, empty: true }) 的 form 是真正的 <form> 元素而非包裹层。

  5. 现象:提交失败时 alert 弹出 undefined,或者控制台报 Cannot read properties of undefined (reading 'data')。 原因:axios 对非 2xx 状态会抛错,错误对象挂在 error 上,数据在 error.response.data;直接写 error.data.message 或 error.message 都取不到服务器文案。 修法:catch (err) { alert(err.response.data.message) },先用 console.dir(err) 确认层级再取值;必要时加 err.response?.data?.message 兜底。

  6. 现象:页面一打开就请求城市/地区接口,或接口返回 400。 原因:把取城市的逻辑写在了页面加载时执行的位置,而不是 change 回调内部;或者切换省份时先请求城市、后读 this.value,参数传成空。 修法:所有二级、三级请求都放在上一级 change 回调内,并且在回调开头先判断 if (!this.value) return,避免默认项触发无参请求。

练习 ​

  1. 只保留省份列表和城市列表,删掉地区部分,把“切换省份清空城市”改成用 select.innerHTML = '<option value="">请选择城市</option>' 实现,并验证连点两次同一个省份不会报错。
  2. 把三级联动改造成“任何一级改变都清空它下面所有级别”:写一个 resetSelect(selector, placeholder) 工具函数,省、市两处复用,替换掉现有的重复拼接代码。
  3. 给提交按钮加一层前端校验:省、市、区、昵称、建议任一为空时不发请求,直接在页面上显示提示文案(不用 alert),全部填写后才走 axios。
  4. 把 try/catch 中的成功与失败提示改为在页面上渲染一段文字(成功绿色、失败红色,3 秒后自动消失),并保证连续快速点击提交不会出现提示叠加。
  5. 用 console.log 打印一次“提交前的完整参数对象”和“服务器返回的 data”,对比字段名与接口文档是否一一对应;再故意删掉昵称输入框的 name 属性,观察 serialize 结果的变化并说明原因。

原视频对照 ​

#视频时长
1案例-学习反馈01-省份列表08:04
2案例-学习反馈02-城市列表11:26
3案例-学习反馈03-地区列表11:25
4案例-学习反馈04-发表反馈09:53

小结 ​

  1. 三级联动的本质是“上一级 change → 清空下一级 → 请求下一级数据 → 渲染”,缺任何一环都会留下脏状态,而脏状态往往在“往回改选”时才暴露。
  2. select 的值来自被选中 option 的 value,渲染时漏写 value 是这类案例最高频的静默失败——页面看起来正常,逻辑却拿不到数据。
  3. 默认占位 option 必须保留且 value 为空串,它既是用户视角的“未选择”,也是代码里判断“该不该发请求”的开关。
  4. async/await 的 async 永远加在“包含 await 的那个函数”上,回调嵌套时最容易漏,报错信息已经直接指出了这一点。
  5. form-serialize 依赖 name 属性工作,因此“表单字段名与接口文档对齐”要在写 HTML 阶段就确认,而不是提交后再排查。
  6. axios 的失败分支信息在 error.response.data 里,try/catch 捕获后先 console.dir 看清结构再取值,能省掉大量猜测。
  7. 这个案例的四步(省→市→区→提交)是同一个模式的四次重复,把前两级做扎实,后两级几乎可以复制粘贴,练习时应优先保证前两级无 bug。