后台表单、招生官网、招聘系统都离不开「省-市-学校」三连下拉。本文用 js原生代码实现一套零依赖、可复用、可扩展的三级联动,数据与渲染彻底解耦,换一批数据也能秒上线。
一、数据约定
把全国行政区划抽象成三层嵌套对象:
js
const province = { '11': '北京市', '12': '天津市', ... }
const city = {
'11': { '014': '天津市' },
'12': {
'015': '漳州市',
'016': '厦门市',
...
}
}
const allschool = {
'015': ['闽南师范大学', '厦门大学嘉庚学院', ...],
'016': ['厦门大学', '集美大学', ...]
}
- 一级 key → 省编号
- 二级对象 → 市编号 : 市名称
- 三级数组 → 学校名称列表
数据结构一旦固定,渲染逻辑永远不变。
二、链式渲染三步走
1.初始化:渲染省
js
for (const code in province) {
provinceDOM.append(new Option(province[code], code))
}
new Option(text, value)
比 createElement('option')
少写两行,还能自动映射 <option>
的 text 与 value。
2.省变化 → 渲染市 + 默认学校
js
provinceDOM.onchange = () => {
cityDOM.innerHTML = schoolDOM.innerHTML = '' // 清空下游
const cityObj = city[provinceDOM.value]
if (!cityObj) return // 边界:无数据
for (const code in cityObj) {
cityDOM.append(new Option(cityObj[code], code))
}
// 立即渲染默认市下的学校
const schoolArr = allschool[cityDOM.value] || []
schoolArr.forEach(name => schoolDOM.append(new Option(name)))
}
清空下游是防止「幽灵选项」:用户先选北京,再选天津,若不重置,城市下拉会残留「和平区」。
3.市变化 → 只渲染学校
js
cityDOM.onchange = () => {
schoolDOM.innerHTML = ''
const schoolArr = allschool[cityDOM.value] || []
schoolArr.forEach(name => schoolDOM.append(new Option(name)))
}
市变化时不再清空省,因为上游已经确定;只处理下游,减少 DOM 抖动。
三、边界与优化细节
省无数据
if (!cityObj) return
直接退出,下拉保持空白,避免后续报错。
市无学校
|| []
兜底,防止 undefined.forEach
抛错;用户看到的是空下拉,逻辑一致。
异步数据
把 onchange
换成 fetch('/api/city/' + provCode).then(...)
即可接入后端分页,前端逻辑不变。
键盘可用
<select>
原生支持上下键、回车、空格,无需额外代码即可满足无障碍需求。
四、代码示例
1.html骨架
html
<select id="province"></select>
<select id="city"></select>
<select id="school"></select>
2.核心js
js
const p = document.getElementById('province')
const c = document.getElementById('city')
const s = document.getElementById('school')
// 1. 渲染省
for (const code in province) p.append(new Option(province[code], code))
// 2. 省变化 → 市 + 默认学校
p.onchange = () => {
c.innerHTML = s.innerHTML = ''
const cityObj = city[p.value]
if (!cityObj) return
for (const code in cityObj) c.append(new Option(cityObj[code], code))
const schoolArr = allschool[c.value] || []
schoolArr.forEach(name => s.append(new Option(name)))
}
// 3. 市变化 → 学校
c.onchange = () => {
s.innerHTML = ''
const schoolArr = allschool[c.value] || []
schoolArr.forEach(name => s.append(new Option(name)))
}