← 返回场景决策

🌐 浏览器与网络场景决策

什么时候用 / 什么时候不用 / 边界在哪里 — 9 大核心场景全解析

HTTP/2 缓存策略 CORS 跨域方案 WebSocket vs SSE 请求方法 状态码 Cookie vs Token 重定向

1. HTTP/2 vs HTTP/1.1 — 协议版本决策

HTTP/1.1 = 单车道公路 一辆车跑完下一辆才能跑(队头阻塞)
HTTP/2 = 多车道高速 多辆车并行跑,还压缩了路牌(头部压缩)
HTTP/2 的核心优势
  • 多路复用:一个 TCP 连接并行多个请求 — 不需要开 6 个连接
  • 头部压缩:HPACK 算法压缩重复 Header — Cookie 等不再每次全传
  • 服务器推送:服务器主动推资源 — HTML + CSS + JS 一起发
  • 流优先级:重要资源先传 — CSS 比 JS 优先级高
⚠️ HTTP/2 的坑
  • TCP 队头阻塞仍在:丢包时所有流都卡住 — HTTP/3 (QUIC) 才解决
  • 域名分片不再需要:HTTP/1.1 时代开多个域名绕 6 连接限制,HTTP/2 反而有害
  • 服务器推送难用:浏览器缓存状态不好判断,容易推送已有资源

📊 HTTP/1.1 vs HTTP/2 vs HTTP/3 决策表

维度HTTP/1.1HTTP/2HTTP/3
传输层TCPTCPQUIC (UDP)
并发连接6个/域名无限(多路复用)无限
队头阻塞应用层TCP层(丢包全卡)❌ 无
头部压缩✅ HPACK✅ QPACK
0-RTT 连接
兼容性100%98%93%
推荐度老旧项目✅ 当前首选前沿项目

2. HTTP 缓存策略决策

强缓存 = 冰箱里的饮料 没过期就直接喝,不用问超市(不请求服务器)
协商缓存 = 看看保质期 不确定,问一句"还新鲜吗?"(304还是200)
强缓存策略
  • Cache-Control: max-age=31536000:带 hash 的静态资源 — 1年强缓存
  • immutable:文件名含 hash 永远不会变 — Cache-Control: max-age=31536000, immutable
  • public vs private:CDN 能缓存用 public,用户私有用 private
协商缓存策略
  • ETag:文件内容hash — 精确判断内容是否变化
  • Last-Modified:最后修改时间 — 精度只到秒,可能不准确
  • HTML 文件:不用强缓存 — 每次都要协商缓存,确保拿到最新

📊 缓存策略决策表

资源类型缓存策略Cache-Control原因
JS/CSS(带hash)强缓存max-age=31536000, immutablehash变了文件名就变了
HTML协商缓存no-cache保证拿到最新入口
API 响应协商缓存no-cache 或 max-age=0数据可能变化
用户头像协商缓存max-age=3005分钟缓存+协商
不缓存不缓存no-store敏感数据/实时数据

💻 Nginx 缓存配置

📀 nginx
📁 nginx/
conf.d/
cache.conf
proxy.conf
ssl.conf
# 带hash的静态资源 — 强缓存1年 location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; } # HTML — 协商缓存 location / { add_header Cache-Control "no-cache"; etag on; } # API — 不缓存 location /api/ { add_header Cache-Control "no-store"; }
面试金句 强缓存 vs 协商缓存:强缓存(Cache-Control: max-age)期间浏览器不发任何请求,直接用本地副本;协商缓存(no-cache + ETag)每次发请求但只传 ETag/Last-Modified,服务器对比后返回 304(无 body)或 200(新内容)。immutable 告诉浏览器即使用户 F5 刷新也不发请求,适用于 hash 文件名资源。

3. CORS — 跨域资源共享决策

同源策略 = 小区的门禁 只让本小区的人进,外面的人不让
CORS = 访客登记 登记过的访客可以进来,服务器发通行证
什么时候需要 CORS
  • 前后端分离:前端 localhost:5173 → 后端 localhost:8080 — 端口不同=跨域
  • 多域名部署:app.example.com → api.example.com — 子域名不同=跨域
  • 第三方API:调用高德地图、微信登录 — 必须服务端支持 CORS
  • CDN 资源:cdn.example.com 的字体跨域 — 需要 Access-Control-Allow-Origin
什么时候不跨域
  • 同源部署:Nginx 反向代理,前端和API同一个域名 — 不存在跨域
  • 子路径部署:/ 是前端,/api 代理到后端 — 同源

📊 简单请求 vs 预检请求

维度简单请求预检请求 (Preflight)
方法GET/POST/HEADPUT/DELETE/PATCH
Content-Typetext/plain, multipart/form-data, application/x-www-form-urlencodedapplication/json 等
自定义Header❌ 不允许✅ 允许
请求次数1次2次(OPTIONS + 实际请求)
典型场景表单提交AJAX + JSON + Token

💻 Spring Boot CORS 配置

📀 backend-service
📁 src/main/java/.../config/
CorsConfig.java
WebSecurityConfig.java
📁 nginx/conf.d/
proxy.conf
// 方式1:全局配置(推荐) @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("https://app.example.com") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); // 预检缓存1小时 } } // 方式2:Nginx 反向代理(更推荐,彻底避免跨域) // location /api/ { // proxy_pass http://backend:8080/; // }
面试金句 CORS 是浏览器行为,不是服务端行为:浏览器发现跨域 → 先发 OPTIONS 预检请求 → 服务器返回 CORS 头 → 浏览器检查通过后才发真实请求。allowedOrigins("*") + allowCredentials(true) 是非法组合,W3C 规范禁止通配符配 Cookie。maxAge(3600) 让浏览器缓存预检结果 1 小时,减少 OPTIONS 请求。
⚠️ CORS 常见坑
  • allowedOrigins("*") + allowCredentials(true):浏览器不允许!必须指定具体域名
  • 响应式 CORS Filter 顺序:CORS Filter 必须在 Security Filter 之前,否则 OPTIONS 请求被拦截
  • Cookie 跨域:需要 SameSite=None; Secure + 前端 credentials: 'include'

4. 跨域解决方案决策

📊 跨域方案决策表

方案原理能否改后端推荐度场景
CORS(服务端配)后端加 Header✅ 能改⭐⭐⭐标准方案
Nginx 反向代理同源化✅ 能改Nginx⭐⭐⭐部署同域
Vite proxy开发时代理❌ 仅开发⭐⭐⭐开发调试
JSONPscript 标签不受限✅ 能改⭐ 旧方案只支持GET,已过时
postMessage窗口间通信-⭐⭐iframe 通信

💻 Vite 开发代理

📀 frontend-app
📁 frontend/
vite.config.js
package.json
src/
// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } }) // 前端请求 /api/users → 实际请求 http://localhost:8080/users // 开发环境不跨域,生产环境用 Nginx 代理
面试金句 Vite/Webpack Dev Server 的 proxy 在 Node.js 中间层做请求转发,浏览器以为自己在请求 localhost:5173/api/users,实际被代理到 localhost:8080/users。changeOrigin: true 修改 Host 头为目标地址。只在开发环境生效,生产环境必须用 Nginx 反向代理实现同样的同源化效果。

5. WebSocket vs SSE — 实时通信决策

WebSocket = 电话通话 双向通话,双方随时说
SSE = 收音机广播 只能服务器往客户端推,单向
轮询 = 每隔5分钟打一次电话 你:"有新消息吗?" 对方:"没有" → 浪费
什么时候用 WebSocket
  • 聊天应用:IM、在线客服 — 双向高频通信
  • 协作编辑:多人在线编辑文档 — 双方都频繁发数据
  • 游戏:实时对战 — 低延迟双向通信
  • 股票行情:实时推送 + 客户端下单 — 双向
什么时候用 SSE
  • 消息通知:系统通知、新评论提醒 — 只需服务端推
  • 实时日志:构建日志流、部署进度 — 单向推送
  • 数据大屏:实时数据展示 — 只推不收
  • AI 流式回复:ChatGPT 式逐字输出 — 简单高效

📊 WebSocket vs SSE vs 轮询决策表

维度WebSocketSSE长轮询
方向双向单向(服务器→客户端)双向(模拟)
协议ws:// / wss://HTTPHTTP
断线重连需手动实现自动重连每次新请求
浏览器支持99%98%100%
代理/防火墙可能被拦截普通HTTP普通HTTP
复杂度
适合场景聊天/游戏通知/AI流式简单兼容

💻 Vue 3 SSE 流式接收(ChatGPT 式)

📀 frontend-app
📁 src/composables/
useChat.js
useAuth.js
📁 src/components/
ChatPanel.vue
async function streamChat(message) { const res = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message }) }); const reader = res.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); // 逐字追加到回复内容 replyText.value += chunk; } }
面试金句 使用 Fetch API + ReadableStream 替代 EventSource 实现 SSE:EventSource 只支持 GET 且不能设自定义 Header,而 Fetch streaming 支持 POST + 任意 Header。getReader().read() 返回 Uint8Array,需用 TextDecoder 转字符串。这是 ChatGPT/Claude 等 AI 流式回复的标准实现。

💻 Spring Boot WebSocket

📀 backend-service
📁 src/main/java/.../
WsConfig.java
ChatHandler.java
WebSocketConfig.java
model/
// 1. 依赖:spring-boot-starter-websocket // 2. WebSocket 配置 @Configuration @EnableWebSocket public class WsConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers( WebSocketHandlerRegistry registry) { registry.addHandler(new ChatHandler(), "/ws/chat") .setAllowedOrigins("*"); } } // 3. Handler public class ChatHandler extends TextWebSocketHandler { @Override protected void handleTextMessage( WebSocketSession session, TextMessage message) { String payload = message.getPayload(); // 广播给所有连接 sessions.forEach(s -> s.sendMessage( new TextMessage("收到: " + payload))); } }
面试金句 Spring WebSocket 基于 JSR-356 标准,TextWebSocketHandler 处理文本消息,BinaryWebSocketHandler 处理二进制。生产环境需维护 sessions 集合(CopyOnWriteArrayList 线程安全),实现连接/断开事件(afterConnectionEstablished/afterConnectionClosed)。大规模场景推荐 STOMP + 消息代理(RabbitMQ/ActiveMQ)替代原生 WebSocket。

6. HTTP 请求方法选择

📊 请求方法决策表

方法语义幂等安全典型场景
GET获取资源查询列表、获取详情
POST创建资源提交表单、创建订单
PUT全量更新替换整个资源
PATCH部分更新修改某个字段
DELETE删除资源删除记录
OPTIONS询问支持的方法CORS 预检请求
⚠️ 常见误区
  • 用 GET 做删除:❌ GET 应该是安全的,不应有副作用
  • 用 POST 做查询:❌ 查询用 GET,除非查询参数太长(如复杂搜索条件)
  • PUT vs PATCH:PUT 传完整对象替换,PATCH 只传修改的字段
  • 幂等性:调用1次和调用N次效果相同 = 幂等。GET/PUT/DELETE 幂等,POST/PATCH 不幂等

💻 PUT vs PATCH 实战对比

📀 api-docs
📁 docs/
api-examples.json
user-api.md
// 用户原始数据 { "id": 1, "name": "张三", "email": "zhangsan@test.com", "age": 25 } // ✅ PUT /users/1 — 全量替换(必须传完整数据) { "name": "张三", "email": "new@test.com", "age": 25 } // 如果不传 age,age 会被清空! // ✅ PATCH /users/1 — 部分更新(只传要改的) { "email": "new@test.com" } // 只改 email,其他字段不动 // 实际开发中 90% 用 PATCH,因为前端通常只改一两个字段
面试金句 PUT 是幂等的(Idempotent):多次调用效果相同——用完整的资源状态替换旧状态,缺失字段=null。PATCH 不保证幂等:只修改指定字段,其他字段不变。RESTful 最佳实践中,PUT 用于整体替换,PATCH 用于局部更新,实际开发中 PATCH 使用频率远高于 PUT。

7. HTTP 状态码决策

📊 常用状态码速查

范围状态码含义前端处理
2xx 成功200OK正常处理数据
201Created创建成功,跳转详情
204No Content删除成功,无返回体
206Partial Content分片下载/断点续传
3xx 重定向301永久重定向更新书签,缓存新地址
302临时重定向登录跳转、支付跳转
4xx 客户端错误400Bad Request参数校验失败,提示用户
401Unauthorized跳转登录页
403Forbidden无权限,提示无权访问
404Not Found显示404页面
5xx 服务端错误500Internal Server Error显示错误页面,上报
502/503Bad Gateway/Service Unavailable服务不可用,稍后重试
⚠️ 状态码常见误用
  • 所有错误都返回200 + 业务code:❌ HTTP 语义被破坏,监控/缓存/重试机制全失效
  • 401 vs 403:401 = 没登录(需要认证),403 = 登录了但没权限
  • 400 vs 422:400 = 请求格式错误,422 = 格式正确但语义错误(如邮箱格式对但已被注册)

9. 重定向方案决策

📊 重定向决策表

方案状态码适用场景SEO 影响
301 永久重定向301域名迁移、HTTP→HTTPS权重转移
302 临时重定向302登录跳转、维护页面权重不转移
307 临时重定向307同302,但保证方法和body不变-
前端路由跳转-SPA 内部页面切换无请求
HTML meta 刷新-纯静态页面跳转搜索引擎可能不跟
⚠️ 301 vs 302 关键区别:301 是永久的,浏览器会缓存重定向结果,下次直接跳。如果你临时用了 301,想改回来就很麻烦(用户浏览器缓存了旧的重定向)。所以拿不准就用 302。

🌐 浏览器与网络场景决策 · 全栈资料库

← 返回场景决策