主题
综合案例:学习反馈
本页对应 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 类型并出现页面刷新,再检查表单默认提交行为。
课件图示


代码示例
代码 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,点击会触发表单默认提交、页面刷新。
常见错误
现象:给省份下拉框绑定
change后,控制台报错await is only valid in async functions,或者城市列表完全不刷新。 原因:在回调里写了await axios(...),但回调函数本身没加async;或者误用箭头函数导致this不是 select 元素。 修法:把回调声明为async function () {...},async写在function前面而不是里面;需要this.value就必须用普通函数,不能改成箭头函数。现象:
select.value打印出来是空字符串,或者拿到的是显示文字而非提交值。 原因:动态拼接option时只写了标签内容<option>北京</option>,漏了value="北京";或者默认那个“请选择省份”的 option 忘了给 value。 修法:模板字符串里同时写value="${v}"和>${v}<,默认项写value="",保证this.value能取到值、取不到时是空串而不是文字。现象:先选河北省石家庄市新华区,再切到北京,城市变了但地区还是“新华区”,提交上去省市区对不上。 原因:切换省份时只更新了城市列表,没有清空地区下拉框,第三个下拉框保留了上一次的选项。 修法:在省份
change回调末尾,把地区 select 的innerHTML重置为只含默认 option 的字符串(用变量存一份默认模板,避免手写重复)。测法:选完三级再换省,看地区是否回落为默认。现象:点击提交,
form-serialize返回{}或只有部分字段,接口报参数缺失。 原因:serialize只收集带name属性的表单控件,页面上漏写name,或name与接口文档的字段名(province/city/area/nickname/feedback)拼写不一致。 修法:逐个核对五个控件的name,与接口文档对照;同时确认serialize(form, { hash: true, empty: true })的form是真正的<form>元素而非包裹层。现象:提交失败时
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兜底。现象:页面一打开就请求城市/地区接口,或接口返回 400。 原因:把取城市的逻辑写在了页面加载时执行的位置,而不是
change回调内部;或者切换省份时先请求城市、后读this.value,参数传成空。 修法:所有二级、三级请求都放在上一级change回调内,并且在回调开头先判断if (!this.value) return,避免默认项触发无参请求。
练习
- 只保留省份列表和城市列表,删掉地区部分,把“切换省份清空城市”改成用
select.innerHTML = '<option value="">请选择城市</option>'实现,并验证连点两次同一个省份不会报错。 - 把三级联动改造成“任何一级改变都清空它下面所有级别”:写一个
resetSelect(selector, placeholder)工具函数,省、市两处复用,替换掉现有的重复拼接代码。 - 给提交按钮加一层前端校验:省、市、区、昵称、建议任一为空时不发请求,直接在页面上显示提示文案(不用 alert),全部填写后才走 axios。
- 把
try/catch中的成功与失败提示改为在页面上渲染一段文字(成功绿色、失败红色,3 秒后自动消失),并保证连续快速点击提交不会出现提示叠加。 - 用
console.log打印一次“提交前的完整参数对象”和“服务器返回的 data”,对比字段名与接口文档是否一一对应;再故意删掉昵称输入框的name属性,观察serialize结果的变化并说明原因。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 案例-学习反馈01-省份列表 | 08:04 |
| 2 | 案例-学习反馈02-城市列表 | 11:26 |
| 3 | 案例-学习反馈03-地区列表 | 11:25 |
| 4 | 案例-学习反馈04-发表反馈 | 09:53 |
小结
- 三级联动的本质是“上一级
change→ 清空下一级 → 请求下一级数据 → 渲染”,缺任何一环都会留下脏状态,而脏状态往往在“往回改选”时才暴露。 select的值来自被选中option的value,渲染时漏写value是这类案例最高频的静默失败——页面看起来正常,逻辑却拿不到数据。- 默认占位 option 必须保留且
value为空串,它既是用户视角的“未选择”,也是代码里判断“该不该发请求”的开关。 async/await的async永远加在“包含 await 的那个函数”上,回调嵌套时最容易漏,报错信息已经直接指出了这一点。form-serialize依赖name属性工作,因此“表单字段名与接口文档对齐”要在写 HTML 阶段就确认,而不是提交后再排查。- axios 的失败分支信息在
error.response.data里,try/catch捕获后先console.dir看清结构再取值,能省掉大量猜测。 - 这个案例的四步(省→市→区→提交)是同一个模式的四次重复,把前两级做扎实,后两级几乎可以复制粘贴,练习时应优先保证前两级无 bug。