主题
界面
主题
页面"不刷新就更新数据"的技术,统称 Ajax。今天的主流写法是 fetch ✓
传统表单: 提交 → 整页刷新 → 服务器返回新 HTML
Ajax: JS 发请求 → 拿 JSON → JS 更新页面局部 ([S3-021](../web/S3-021) 的"分离")✓本节同时是前后端协作的"桥" ——你将真正站在前端的视角,看你的接口是怎么被调用的 ✓
fetch 发 GET / POST 请求 ★★;fetch 不会对 4xx/5xx 抛异常 ★★★(联调第一大坑);r.json())与错误(try/catch + r.ok)★★;Promise 与 async/await 吗?(《JavaScript 基础》 的 5.6 节)★@RequestBody / @ResponseBody 吗?(《Spring MVC 请求映射与参数绑定》)联调时一半的"扯皮"源于对交互层理解不一致:
| 扯皮现场 | 实际原因 |
|---|---|
| "你们接口返回了错误,但页面没反应" | 前端没处理非 2xx(fetch 不抛异常)✗ |
| "CORS 报错,是你们的问题" | 后端没配 Access-Control-Allow-Origin ⚠️(双方都要懂 ✓) |
| "token 传了为什么还 401" | Bearer 前缀丢了 / 放在了错误头里 ⚠️ |
本节让双方说同一种语言 ✓
fetch 的基本用法 ★★ // GET
const response = await fetch('/api/orders?page=1&size=20');
const data = await response.json(); // ★ 两步:先拿 response,再解析体
// POST(发 JSON)
const created = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }, // ★ 决定后端怎么解析体
body: JSON.stringify({ productId: 'P-1', quantity: 2, address: '上海' }),
});
const order = await created.json();★ 两个"两步":
| 步骤 | 拿到什么 |
|---|---|
await fetch(...) | 响应元信息(状态码、头)——此时体还没解析 ✓ |
await response.json() | 解析后的对象 ✓(体可能很大,是流) |
★ 类比 《Spring MVC 请求映射与参数绑定》:Content-Type: application/json 对应后端的 @RequestBody ✓ ——前端忘了设它,后端会 415 ✗(《Spring MVC 请求映射与参数绑定》 的 5.6 节)✓
fetch 不会对 4xx/5xx 抛异常 这是联调第一大坑 ✗✗
// ✗ 直觉写法(错误)
const r = await fetch('/api/orders/ORD-999'); // 服务器返回 404
const data = await r.json(); // ← 会正常走到这里!
console.log(data); // 打印的是错误响应体 ✗
// ✓ 正确写法:自己检查 r.ok
const r = await fetch('/api/orders/ORD-999');
if (!r.ok) { // ★ r.ok = 状态码 200~299
const err = await r.json();
console.error(err.code, err.message); // RESOURCE_NOT_FOUND / ...
return;
}
const order = await r.json();对比:axios / jQuery.ajax 会对 4xx/5xx 抛异常 ⚠️ ——换库时行为不同,要记得 ✓
★ 更工程化的封装(把"检查 + 解析 + 错误"封装一次):
async function api(path, options = {}) {
const token = localStorage.getItem('token');
const r = await fetch(path, {
...options,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}), // ★ 自动带 token
...options.headers,
},
});
if (r.status === 401) { location.href = '/login'; throw new Error('未登录'); } // ★ 统一处理
const body = await r.json().catch(() => null);
if (!r.ok) throw new Error(body?.message || `HTTP ${r.status}`);
return body;
}
// 使用:业务代码只关心"成功路径" ✓
const page = await api('/api/orders?page=1&size=20');★ 这个封装包含了三件事:token 注入、401 统一跳转、错误统一抛出 ✓ ——这正是 《Filter、Listener 与会话管理》 的 traceId/错误响应在客户端的"对端" ✓
同源 = 协议 + 域名 + 端口都相同 ✓
https://admin.example.com vs https://api.example.com
→ 域名相同? example.com 相同,但**完整域名不同** → 跨域 ⚠️
http://localhost:3000 vs http://localhost:8080
→ 端口不同 → 跨域 ⚠️(**开发环境最常见**)跨域时浏览器会:先发一个 OPTIONS 预检请求(带自定义头或非简单方法时)→ 服务端必须响应允许 → 否则 JS 拿不到响应 ✗
后端的 CORS 配置(Spring MVC):
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://admin.example.com") // ★ 精确来源(不要 "*",配 credentials 时禁止)✓
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true); // ★ 要带 Cookie 时必须
}
}三条必须同时满足(《Filter、Listener 与会话管理》 的 9.8 节)★:
| 条件 | 配置 |
|---|---|
| 前端 | credentials: 'include' |
| 后端 | Access-Control-Allow-Credentials: true |
| 后端 | Allow-Origin 不能是 * ✗ |
★ CORS 是"浏览器的策略",不是"服务器的限制" ✓
curl 直连后端 → 完全没有 CORS 概念,能拿到数据 ✓
浏览器里 fetch → 检查响应头 → 不允许就不给 JS所以排查 CORS 时:用 curl 能拿到 ≠ 浏览器能用 ✗ ——看 F12 Network 里 OPTIONS 请求的响应 ✓
【登录】
前端 POST /api/auth/login {"username":"...","password":"..."}
→ 后端验证 → 返回 {"token":"eyJhbGciOi..."}([S3-010](../security/S3-010))
前端:localStorage.setItem('token', ...)
【携带】
每次请求:Authorization: Bearer <token> ← 封装在 api() 里自动注入 ✓
【401 处理】
后端返回 401([S3-023](../spring/S3-023) 的统一错误体)
→ 前端封装层统一跳登录页 ✓
【错误展示】
后端返回 422 {"code":"VALIDATION_FAILED","details":[...]}
→ 前端遍历 details,在对应输入框下标红 ★
(这就是为什么后端要返回"逐字段错误"——[S3-023](../spring/S3-023) 的 5.6)★ 逐字段错误的"消费端":
catch (e) {
if (e.details) { // VALIDATION_FAILED
e.details.forEach(d => showFieldError(d.field, d.message)); // ← 精确标红 ✓
} else {
alert(e.message);
}
}前后端把"错误契约"设计好了,联调就是"填空题" ✓
F12 → Network 看那个失败的请求(《HTML 与 CSS 基础》)✓
| Network 里看到 | 结论 | 谁修 |
|---|---|---|
| 红色(连接失败/CORS) | 网络/服务/配置问题 | 先后端看服务起没起 ✓ |
| 404 | 路径不对(前端拼错 or 后端没这个路径) | 对照 《Spring MVC 请求映射与参数绑定》 的映射 ✓ |
| 415 | Content-Type 没设 JSON ✗ | 前端 ✓ |
| 400/422 | 校验失败 → 看响应体 details ✓ | 通常前端改参数 ✓ |
| 401 | token 缺失/过期 | 前端(或后端失效时间)⚠️ |
| 403 | 权限不够(《Spring Security 认证与授权》) | 后端改规则 or 前端改账号 ✓ |
| 500 | 后端异常 → 拿 traceId 去后端日志查 ★★ | 后端 ✓(《日志框架与日志规范》) |
| 200 但页面空 | 前端渲染问题(《HTML 与 CSS 基础》) | 前端 ✓ |
★ 这张表的本质:"状态码"就是前后端的"分工坐标" ✓(S3-REST 的 5.4)✓
const r = await fetch('/api/orders/ORD-1');
const order = await r.json();
console.log(order.orderNo);async function loadOrders() {
const page = await api('/api/orders?page=1&size=20');
renderRows(page.data); // [S3-012] 的表格
}
async function removeOrder(orderNo) {
await api(`/api/orders/${orderNo}`, { method: 'DELETE' }); // 204 → body 为空 ✓
await loadOrders(); // ★ 刷新列表
}⚠️ 204 的细节:DELETE 成功返回 204(无响应体)——r.json() 会报错 ✗
修:先判断状态:
if (r.status === 204) return null; // ✓
return r.json();(又一个"约定了状态码就要在客户端处理"的例子——S3-REST 的状态码约定在这里"变现")✓
GET 传复杂条件 GET /api/orders/search?keyword=索引&tags=java&tags=mysql&from=2026-01-01&to=...问题:《Spring MVC 请求映射与参数绑定》 的 6.3 节分析过——URL 长度、嵌套表达不了 ✗
修:POST /search + JSON body ✓
⚠️ 说明:
fetch需要"浏览器环境"或 Node 18+。本机 Node v24 支持fetch✓ 但访问真实接口需要先启动后端(spring-boot-web)——建议把 《Spring MVC 请求映射与参数绑定》 的九场景与本节互相印证 ✓
// labs/frontend-demo/fetch-demo.js(node 18+ 可直接运行)
// 演示"fetch 不抛异常"与"手动检查 r.ok"
const base = 'http://localhost:18090/api'; // 配合 web-basics 的 18090 端口
async function main() {
// ① 正常请求
let r = await fetch(`${base}/orders?page=1&size=2`);
console.log('GET 列表:', r.status, r.ok);
// ② 404(不会抛异常 ★)
r = await fetch(`${base}/orders/ORD-NOPE`);
console.log('GET 不存在:', r.status, 'r.ok =', r.ok);
// ③ 422(校验失败)
r = await fetch(`${base}/orders`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId: '', quantity: 0, address: '' }),
});
console.log('POST 校验失败:', r.status);
console.log(' 响应体:', JSON.stringify(await r.json()));
}
main().catch(e => console.error('✗', e.message));预期输出(先启动 《Filter、Listener 与会话管理》 的 web-basics.jar 后运行):
GET 列表: 200 true
GET 不存在: 404 r.ok = false ← ★ fetch 不会在这里抛异常
POST 校验失败: 422
响应体: {"code":"VALIDATION_FAILED","message":"参数校验失败",...,"details":[...]}★ 这个演示证明了 5.2 的"坑":404 也会走"正常路径" ✗ ——必须自己检查 r.ok ✓
Request Headers:确认 Content-Type 与 Authorization 真的发了吗 ✓("以为发了"是常见错觉)⚠️;Response 标签页:后端到底返回了什么(而不是前端展示的)✓;OPTIONS 请求(跨域时):它的响应头就是 CORS 的判定依据 ✓;| # | 错误 | 说明 |
|---|---|---|
| 1 | 不检查 r.ok ✗✗ | 4xx/5xx 走正常路径 ✗(5.2) |
| 2 | POST 忘设 Content-Type | 415 ✗(《Spring MVC 请求映射与参数绑定》) |
| 3 | JSON.stringify 忘了写 | body: obj → 发出去的是 [object Object] ✗✗ |
| 4 | 跨域只改了后端,前端没带 credentials | Cookie 带不上(《Filter、Listener 与会话管理》)✗ |
| 5 | 204 还去 r.json() | 解析空体报错 ✗(6.2) |
| 6 | 把 GET 的参数拼在 body 里 | GET 没有有效 body ✗ ——用查询参数 ✓ |
web-basics,跑第 7 节的 fetch-demo.js ✓;try/catch,观察 catch 不会触发 ✗ ——再用 r.ok 修 ✓;throw new RuntimeException → 观察 500 的响应体是"统一错误体"(《参数校验与全局异常处理》)→ 拿 traceId 去日志里查 ★✓;file:// 打开一个本地页面去 fetch localhost——观察 CORS 报错 ✓(然后按 5.3 配后端);扩展任务:给 api() 封装加"重试一次"(仅对 GET、仅对网络错误)——并说明为什么 POST 不应该自动重试(幂等性,《Spring MVC 请求映射与参数绑定》 的 5.4)✓
fetch 在服务器返回 500 时会抛异常。(对 / 错)await r.______()。fetch(url).then(r => r.json()).then(render) 在 500 时会发生什么?api() 封装(5.2),并处理"204 无体"的情况。Content-Type: application/json;json() ✓r.json() 解析出的是"错误响应体" → render 拿到错误数据去渲染 ✗✗(表面不报错、实际全错,比报错更危险)✓r.status === 204 再决定是否 r.json() ✓fetch 发 GET/POST 并处理 JSON ★★;fetch 不对 4xx/5xx 抛异常,会检查 r.ok ★★★;Content-Type 与 415 的关系(《Spring MVC 请求映射与参数绑定》);r.json() 的坑(6.2)。《Vue 基础与组件开发》:把这些"命令式 DOM 操作"升级成"声明式渲染"。
暂无