Skip to content

Ajax 与前后端交互 ​

S3-038必学进阶约 60 分钟JDK 21企业开发基础 · 前端基础与 Vue

1. 本节定位 ​

页面"不刷新就更新数据"的技术,统称 Ajax。今天的主流写法是 fetch ✓

text
传统表单:   提交 → 整页刷新 → 服务器返回新 HTML
Ajax:      JS 发请求 → 拿 JSON → JS 更新页面局部        ([S3-021](../web/S3-021) 的"分离")✓

本节同时是前后端协作的"桥" ——你将真正站在前端的视角,看你的接口是怎么被调用的 ✓

2. 学习目标 ​

  • 会用 fetch 发 GET / POST 请求 ★★;
  • 知道 fetch 不会对 4xx/5xx 抛异常 ★★★(联调第一大坑);
  • 会处理 JSON(r.json())与错误(try/catch + r.ok)★★;
  • 说出同源策略与 CORS(《Filter、Listener 与会话管理》)★★;
  • 说出"前后端交互的完整链路"(含 token、401 处理)★★;
  • 会做"联调失败"的定位(F12 Network,《HTML 与 CSS 基础》)✓。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

联调时一半的"扯皮"源于对交互层理解不一致:

扯皮现场实际原因
"你们接口返回了错误,但页面没反应"前端没处理非 2xx(fetch 不抛异常)✗
"CORS 报错,是你们的问题"后端没配 Access-Control-Allow-Origin ⚠️(双方都要懂 ✓)
"token 传了为什么还 401"Bearer 前缀丢了 / 放在了错误头里 ⚠️

本节让双方说同一种语言 ✓

5. 核心概念 ​

5.1 fetch 的基本用法 ★★ ​

javascript
// 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 节)✓

5.2 ★★★ fetch 不会对 4xx/5xx 抛异常 ​

这是联调第一大坑 ✗✗

javascript
// ✗ 直觉写法(错误)
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 抛异常 ⚠️ ——换库时行为不同,要记得 ✓

★ 更工程化的封装(把"检查 + 解析 + 错误"封装一次):

javascript
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/错误响应在客户端的"对端" ✓

5.3 同源策略与 CORS ★★ ​

同源 = 协议 + 域名 + 端口都相同 ✓

text
https://admin.example.com  vs  https://api.example.com
    → 域名相同? example.com 相同,但**完整域名不同** → 跨域 ⚠️
http://localhost:3000 vs http://localhost:8080
    → 端口不同 → 跨域 ⚠️(**开发环境最常见**)

跨域时浏览器会:先发一个 OPTIONS 预检请求(带自定义头或非简单方法时)→ 服务端必须响应允许 → 否则 JS 拿不到响应 ✗

后端的 CORS 配置(Spring MVC):

java
@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 是"浏览器的策略",不是"服务器的限制" ✓

text
curl 直连后端 → 完全没有 CORS 概念,能拿到数据 ✓
浏览器里 fetch → 检查响应头 → 不允许就不给 JS

所以排查 CORS 时:用 curl 能拿到 ≠ 浏览器能用 ✗ ——看 F12 Network 里 OPTIONS 请求的响应 ✓

5.4 前后端交互的完整链路 ★★ ​

text
【登录】
前端 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)

★ 逐字段错误的"消费端":

javascript
catch (e) {
    if (e.details) {                                   // VALIDATION_FAILED
        e.details.forEach(d => showFieldError(d.field, d.message));   // ← 精确标红 ✓
    } else {
        alert(e.message);
    }
}

前后端把"错误契约"设计好了,联调就是"填空题" ✓

5.5 前端"拿不到数据"的排查清单 ★★★ ​

F12 → Network 看那个失败的请求(《HTML 与 CSS 基础》)✓

Network 里看到结论谁修
红色(连接失败/CORS)网络/服务/配置问题先后端看服务起没起 ✓
404路径不对(前端拼错 or 后端没这个路径)对照 《Spring MVC 请求映射与参数绑定》 的映射 ✓
415Content-Type 没设 JSON ✗前端 ✓
400/422校验失败 → 看响应体 details ✓通常前端改参数 ✓
401token 缺失/过期前端(或后端失效时间)⚠️
403权限不够(《Spring Security 认证与授权》)后端改规则 or 前端改账号 ✓
500后端异常 → 拿 traceId 去后端日志查 ★★后端 ✓(《日志框架与日志规范》)
200 但页面空前端渲染问题(《HTML 与 CSS 基础》)前端 ✓

★ 这张表的本质:"状态码"就是前后端的"分工坐标" ✓(S3-REST 的 5.4)✓

6. 递进示例 ​

6.1 最小示例 ​

javascript
const r = await fetch('/api/orders/ORD-1');
const order = await r.json();
console.log(order.orderNo);

6.2 业务示例:列表页加载 + 删除 ​

javascript
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() 会报错 ✗

修:先判断状态:

javascript
if (r.status === 204) return null;               // ✓
return r.json();

(又一个"约定了状态码就要在客户端处理"的例子——S3-REST 的状态码约定在这里"变现")✓

6.3 反例:用 GET 传复杂条件 ​

bash
GET /api/orders/search?keyword=索引&tags=java&tags=mysql&from=2026-01-01&to=...

问题:《Spring MVC 请求映射与参数绑定》 的 6.3 节分析过——URL 长度、嵌套表达不了 ✗

修:POST /search + JSON body ✓

7. 完整可运行演示 ​

⚠️ 说明:fetch 需要"浏览器环境"或 Node 18+。本机 Node v24 支持 fetch ✓ 但访问真实接口需要先启动后端(spring-boot-web)——建议把 《Spring MVC 请求映射与参数绑定》 的九场景与本节互相印证 ✓

javascript
// 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 后运行):

text
GET 列表: 200 true
GET 不存在: 404 r.ok = false          ← ★ fetch 不会在这里抛异常
POST 校验失败: 422
  响应体: {"code":"VALIDATION_FAILED","message":"参数校验失败",...,"details":[...]}

★ 这个演示证明了 5.2 的"坑":404 也会走"正常路径" ✗ ——必须自己检查 r.ok ✓

8. Debug 观察点 ​

  1. 在 Network 里看请求的 Request Headers:确认 Content-Type 与 Authorization 真的发了吗 ✓("以为发了"是常见错觉)⚠️;
  2. 看 Response 标签页:后端到底返回了什么(而不是前端展示的)✓;
  3. 看 OPTIONS 请求(跨域时):它的响应头就是 CORS 的判定依据 ✓;
  4. 故意把 token 改错一位:观察 401 与统一错误体 ✓。

9. 常见错误 ​

#错误说明
1不检查 r.ok ✗✗4xx/5xx 走正常路径 ✗(5.2)
2POST 忘设 Content-Type415 ✗(《Spring MVC 请求映射与参数绑定》)
3JSON.stringify 忘了写body: obj → 发出去的是 [object Object] ✗✗
4跨域只改了后端,前端没带 credentialsCookie 带不上(《Filter、Listener 与会话管理》)✗
5204 还去 r.json()解析空体报错 ✗(6.2)
6把 GET 的参数拼在 body 里GET 没有有效 body ✗ ——用查询参数 ✓

10. 动手实验 ​

  1. 启动 web-basics,跑第 7 节的 fetch-demo.js ✓;
  2. 验证"不抛异常":在 404 的响应上写 try/catch,观察 catch 不会触发 ✗ ——再用 r.ok 修 ✓;
  3. 造一个 500:临时在服务里 throw new RuntimeException → 观察 500 的响应体是"统一错误体"(《参数校验与全局异常处理》)→ 拿 traceId 去日志里查 ★✓;
  4. 跨域实验:用 file:// 打开一个本地页面去 fetch localhost——观察 CORS 报错 ✓(然后按 5.3 配后端);
  5. 对照 《Spring MVC 请求映射与参数绑定》 的九场景:用 fetch 把九个场景全部请求一遍(状态码一一对应)✓。

扩展任务:给 api() 封装加"重试一次"(仅对 GET、仅对网络错误)——并说明为什么 POST 不应该自动重试(幂等性,《Spring MVC 请求映射与参数绑定》 的 5.4)✓

11. 练习题 ​

  1. 判断:fetch 在服务器返回 500 时会抛异常。(对 / 错)
  2. 填空:POST JSON 必须设置请求头 ______;读取 JSON 响应用 await r.______()。
  3. 简答:为什么"CORS 是浏览器行为"?
  4. (代码阅读)fetch(url).then(r => r.json()).then(render) 在 500 时会发生什么?
  5. (编程)实现 api() 封装(5.2),并处理"204 无体"的情况。
参考答案(要点)
  1. 错。只对"网络层失败"(DNS、断网)抛异常;4xx/5xx 走正常路径 ✗✗(5.2)
  2. Content-Type: application/json;json() ✓
  3. 因为"允许哪些来源的页面读取我的响应"是"浏览器为了保护用户"做的判断 ✓ 服务端只是"声明允许" ✓;curl 没有这个保护,所以能拿到数据 ✗(别用 curl 验证 CORS)✗
  4. 会正常执行——r.json() 解析出的是"错误响应体" → render 拿到错误数据去渲染 ✗✗(表面不报错、实际全错,比报错更危险)✓
  5. 见 6.2:先判断 r.status === 204 再决定是否 r.json() ✓

12. 掌握标准 ​

  • [ ] 会用 fetch 发 GET/POST 并处理 JSON ★★;
  • [ ] 知道 fetch 不对 4xx/5xx 抛异常,会检查 r.ok ★★★;
  • [ ] 知道 Content-Type 与 415 的关系(《Spring MVC 请求映射与参数绑定》);
  • [ ] 理解同源策略与 CORS,知道"三条件缺一不可" ★★;
  • [ ] 会做"前端拿不到数据"的定位(F12 Network + 状态码分工)★★;
  • [ ] 知道 204 与 r.json() 的坑(6.2)。

13. 扩展资源 ​

14. 下一步学习 ​

《Vue 基础与组件开发》:把这些"命令式 DOM 操作"升级成"声明式渲染"。

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引