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, immutable | hash变了文件名就变了 |
| HTML | 协商缓存 | no-cache | 保证拿到最新入口 |
| API 响应 | 协商缓存 | no-cache 或 max-age=0 | 数据可能变化 |
| 用户头像 | 协商缓存 | max-age=300 | 5分钟缓存+协商 |
| 不缓存 | 不缓存 | no-store | 敏感数据/实时数据 |
💻 Nginx 缓存配置
# 带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/HEAD | PUT/DELETE/PATCH |
| Content-Type | text/plain, multipart/form-data, application/x-www-form-urlencoded | application/json 等 |
| 自定义Header | ❌ 不允许 | ✅ 允许 |
| 请求次数 | 1次 | 2次(OPTIONS + 实际请求) |
| 典型场景 | 表单提交 | AJAX + JSON + Token |
💻 Spring Boot CORS 配置
// 方式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 | 开发时代理 | ❌ 仅开发 | ⭐⭐⭐ | 开发调试 |
| JSONP | script 标签不受限 | ✅ 能改 | ⭐ 旧方案 | 只支持GET,已过时 |
| postMessage | 窗口间通信 | - | ⭐⭐ | iframe 通信 |
💻 Vite 开发代理
// 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 轮询决策表
| 维度 | WebSocket | SSE | 长轮询 |
| 方向 | 双向 | 单向(服务器→客户端) | 双向(模拟) |
| 协议 | ws:// / wss:// | HTTP | HTTP |
| 断线重连 | 需手动实现 | 自动重连 | 每次新请求 |
| 浏览器支持 | 99% | 98% | 100% |
| 代理/防火墙 | 可能被拦截 | 普通HTTP | 普通HTTP |
| 复杂度 | 高 | 低 | 低 |
| 适合场景 | 聊天/游戏 | 通知/AI流式 | 简单兼容 |
💻 Vue 3 SSE 流式接收(ChatGPT 式)
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
// 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 实战对比
// 用户原始数据
{ "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。
8. Cookie vs Token — 认证方案决策
Cookie = 身份证(政府发)
→
浏览器自动携带,服务端验证
Token = 通行证(自己保管)
→
手动放请求头,跨域友好
📊 Cookie vs Token 决策表
| 维度 | Cookie + Session | JWT Token |
| 存储位置 | 浏览器自动存 | LocalStorage / 内存 |
| 携带方式 | 浏览器自动带 | 手动放 Authorization Header |
| 跨域 | 麻烦(SameSite限制) | 天然支持 |
| CSRF | 易受攻击 | 天然免疫 |
| 服务端存储 | 需要(Session) | 不需要(无状态) |
| 撤销 | 立即使Session失效 | 难(直到过期) |
| 移动端 | 不便 | 友好 |
| 推荐场景 | 传统Web应用 | SPA + 移动端 + API |
💻 JWT 认证完整流程
// 1. 登录获取 Token
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
const { token } = await res.json();
localStorage.setItem('token', token);
// 2. Axios 拦截器自动带 Token
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 3. 响应拦截器处理 401
axios.interceptors.response.use(
res => res,
err => {
if (err.response?.status === 401) {
localStorage.removeItem('token');
router.push('/login'); // 跳转登录
}
return Promise.reject(err);
}
);
面试金句
Axios 拦截器(Interceptor)是前端统一处理请求/响应的标准方案:请求拦截器在每个请求发出前自动注入 Authorization Header;响应拦截器统一处理 401(Token 过期→清除→跳转登录)。安全注意:JWT 存 localStorage 有 XSS 风险,高安全场景改用 HttpOnly Cookie + CSRF Token 方案。