级联下拉框常见的流程是:选中一个主分类,把它发给后端,后端返回两组子分类,再分别更新两个 select。关键不只是 AJAX 能发出去,还要保证前后端理解同一份数据,并避免旧请求覆盖新选择。
先把请求和响应约定写清楚
本文沿用两组列表的响应形式,约定:
{
"middleCategory": "A"
}
响应的第一组为 minorCategory,第二组为 subCategory:
[
[{"categoryCd": "A1", "categoryNm": "子分类一"}],
[]
]
JSON.stringify 把 JavaScript 值序列化为文本。contentType 表示发送正文的媒体类型,dataType 表示 jQuery 对响应的解析预期,两者职责不同。jQuery AJAX
前端:清空旧值,并只接受最后一次选择的结果
以下示例用于已有 jQuery 1.7+ 的页面,页面包含三个具名 select、#categoryStatus 状态区域,以及服务端按当前会话渲染的 _csrf_header、_csrf 两个 meta。运行环境应支持示例使用的 JavaScript 语法。
$(function () {
var sequence = 0;
var pending = null;
var parent = $("select[name='middleCategory']");
var minor = $("select[name='minorCategory']");
var sub = $("select[name='subCategory']");
var status = $("#categoryStatus");
function validGroup(items) {
return Array.isArray(items) && items.every(function (item) {
return item && typeof item.categoryCd === "string"
&& typeof item.categoryNm === "string";
});
}
function fill(select, items) {
select.empty();
items.forEach(function (item) {
select.append(new Option(item.categoryNm, item.categoryCd));
});
}
parent.on("change.categories", function () {
var current = ++sequence;
if (pending) pending.abort();
minor.add(sub).empty().prop("disabled", true);
status.text("");
var value = parent.val();
if (!value) {
minor.add(sub).prop("disabled", false);
pending = null;
return;
}
var header = $("meta[name='_csrf_header']").attr("content");
var token = $("meta[name='_csrf']").attr("content");
if (!header || !token) {
status.text("页面缺少请求防护信息,请重新加载");
pending = null;
return;
}
var headers = {};
headers[header] = token;
pending = $.ajax({
url: "/minorCategory/search",
type: "POST",
contentType: "application/json; charset=UTF-8",
dataType: "json",
headers: headers,
data: JSON.stringify({ middleCategory: value })
}).done(function (data) {
if (current !== sequence) return;
if (!Array.isArray(data) || data.length !== 2
|| !validGroup(data[0]) || !validGroup(data[1])) {
status.text("分类响应格式不符合约定");
return;
}
fill(minor, data[0]);
fill(sub, data[1]);
}).fail(function (xhr, reason) {
if (current !== sequence || reason === "abort") return;
status.text("分类加载失败,请稍后重试");
}).always(function () {
if (current !== sequence) return;
minor.add(sub).prop("disabled", false);
pending = null;
});
});
});
先递增序号,再取消旧请求;即使取消来不及,返回回调也只处理当前序号。清空旧选项意味着空结果不会继续显示上一次的数据。new Option 将名称作为文本设置,避免拼接 HTML 字符串解释特殊字符。
这里假定服务端启用了匹配的 CSRF 防护。头名称和令牌由服务端配置决定,不应硬编码真实令牌,也不应因请求失败关闭防护;CSRF 不能替代身份认证和数据访问授权。
后端:专用请求对象,不直接绑定整张业务实体
下面是 Spring MVC 5.x 风格的控制器示例。CategoryLookup 由项目提供并注册为受控查询服务,返回两组 CategoryOption;示例不包含真实数据库实现。
import java.util.Arrays;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
@RestController
@RequestMapping("/minorCategory")
public class CategoryController {
private final CategoryLookup lookup;
public CategoryController(CategoryLookup lookup) {
this.lookup = lookup;
}
@PostMapping(value = "/search",
consumes = "application/json", produces = "application/json")
public List<List<CategoryOption>> search(
@RequestBody CategoryRequest request) {
String code = request.getMiddleCategory();
if (code == null || code.trim().isEmpty() || code.length() > 64) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
return Arrays.asList(lookup.findMinor(code), lookup.findSub(code));
}
public interface CategoryLookup {
List<CategoryOption> findMinor(String code);
List<CategoryOption> findSub(String code);
}
public static class CategoryRequest {
private String middleCategory;
public String getMiddleCategory() { return middleCategory; }
public void setMiddleCategory(String value) { middleCategory = value; }
}
public static class CategoryOption {
private final String categoryCd;
private final String categoryNm;
public CategoryOption(String code, String name) {
categoryCd = code;
categoryNm = name;
}
public String getCategoryCd() { return categoryCd; }
public String getCategoryNm() { return categoryNm; }
}
}
lookup 要执行参数校验后的授权查询,返回空集合而不是随意混用 null;服务端还应限制结果规模。输入 DTO 只包含当前查询需要的字段,不直接把整张产品实体暴露为可绑定对象。
@RequestBody 通过已配置的消息转换器读取请求体,@RestController 使返回值按响应体处理;还需具备对应 JSON 转换依赖。Spring MVC 请求体转换
两组列表的顺序也是接口契约
data[0] 与 data[1] 依赖固定顺序。以后可改成带名称的对象字段提高可读性,但必须前后端一起调整,不能只看“仍然返回合法 JSON”。
验收应覆盖空数组、无效响应、请求失败和快速连续切换,分别观察请求正文、响应结构以及最后显示的选择结果。