Skip to content

网络请求踩坑

前端网络请求中高频遇到的跨域、Cookie、Token 和请求行为问题。


预防措施

  1. 开发环境配置代理:Vite 的 server.proxy 或 Webpack 的 devServer.proxy 可避免开发时跨域问题
  2. 统一请求拦截器:在 axios 拦截器中统一处理 Token、错误码、重定向
  3. 接口响应格式规范:与后端约定统一的响应结构(如 { code, data, message }
  4. 超时时间分层设置:普通接口 15 秒,文件上传 1 分钟,导出 2 分钟
  5. 请求取消机制:页面切换时取消未完成的请求,避免数据错乱

1. CORS 跨域错误

什么是 CORS(Cross-Origin Resource Sharing,跨域资源共享)? CORS 是浏览器的安全机制,用于限制网页向不同源(协议、域名、端口任一不同)的服务器发起请求。 当检测到跨域请求时,浏览器会先检查服务器是否允许该跨域访问,不允许则拦截响应。

Access to XMLHttpRequest at 'http://api.example.com' from origin
'http://localhost:5173' has been blocked by CORS policy

原因:后端没有设置允许跨域的响应头。

快速排查

  1. 打开 Network 面板,看请求是否发出
  2. 看 Console 报错是 CORS 还是其他网络错误
  3. 确认后端是否返回了 Access-Control-Allow-Origin

解决方案

  • 后端添加 CORS 头(Access-Control-Allow-Origin 等)
  • 开发环境用 Vite 代理
  • 生产环境用 Nginx 反向代理

📖 完整代理配置和跨域原理见 接口联调与调试 → 跨域处理


2. 预检请求(OPTIONS)

什么是预检请求(Preflight Request)? 当浏览器发起"非简单请求"时,会先自动发送一个 OPTIONS 请求给服务器,询问是否允许该跨域请求。 只有服务器确认允许后,浏览器才会发送真正的请求。这个 OPTIONS 请求就是"预检请求"。

# 浏览器自动发送 OPTIONS 请求检查是否允许跨域
# 以下情况会触发预检:
# - 使用了 PUT/DELETE/PATCH 等非简单方法
# - 请求头包含自定义字段(如 Authorization)
# - Content-Type 不是 application/x-www-form-urlencoded / multipart/form-data / text/plain

# 常见问题:后端没有处理 OPTIONS 请求,返回 404/405
bash
# 后端需要处理 OPTIONS 请求并返回正确的 CORS 头
# 或使用 nginx / 网关统一处理

js
// 跨域请求默认不携带 Cookie(浏览器安全策略)
axios.get('http://api.example.com/user')  // ❌ 不带 Cookie

// ✅ 设置 withCredentials: true,允许跨域请求携带 Cookie
axios.get('http://api.example.com/user', {
  withCredentials: true                    // 跨域时携带 Cookie
})

// fetch 也一样,credentials 选项控制 Cookie 行为
fetch('http://api.example.com/user', {
  credentials: 'include'   // 'include' 始终携带,'same-origin' 仅同源携带
})
bash
# 后端也需要配合,返回以下响应头:
Access-Control-Allow-Credentials: true
# 注意:Allow-Credentials: true 时,Allow-Origin 不能用 *,必须指定具体域名
# 例如:
Access-Control-Allow-Origin: https://example.com
# 如果需要支持多个域名,后端需要根据请求的 Origin 动态返回对应的值

4. Token 过期处理

js
// 问题:token 过期后,多个请求同时失败,刷新多次

let isRefreshing = false    // 是否正在刷新
let pendingQueue = []       // 等待队列

axios.interceptors.response.use(null, async error => {
  if (error.response?.status !== 401) return Promise.reject(error)

  if (!isRefreshing) {
    isRefreshing = true
    try {
      const newToken = await refreshToken()
      setToken(newToken)
      // 重试队列中的请求
      pendingQueue.forEach(cb => cb(newToken))
      pendingQueue = []
      // 重试当前请求
      error.config.headers.Authorization = `Bearer ${newToken}`
      return axios(error.config)
    } catch {
      removeToken()
      router.push('/login')
    } finally {
      isRefreshing = false
    }
  }

  // 正在刷新,将请求加入队列
  return new Promise(resolve => {
    pendingQueue.push(token => {
      error.config.headers.Authorization = `Bearer ${token}`
      resolve(axios(error.config))
    })
  })
})

5. 重复请求未取消

js
// 快速切换页面时,上一个请求还没返回,数据错乱

// ✅ 方式一:AbortController
const controller = new AbortController()

fetch('/api/data', { signal: controller.signal })

// 页面切换时取消
onUnmounted(() => controller.abort())

// ✅ 方式二:axios CancelToken(旧版)
const source = axios.CancelToken.source()
axios.get('/api/data', { cancelToken: source.token })
source.cancel('请求取消')

6. 文件下载拿不到文件名

js
// 后端返回文件流,前端需要从响应头获取文件名
const res = await fetch('/api/export')
const disposition = res.headers.get('Content-Disposition')

// 解析文件名
let filename = 'download.xlsx'
if (disposition) {
  // 优先取 filename*(RFC 5987 标准,支持中文编码)
  // 格式:filename*=UTF-8''%E6%96%87%E4%BB%B6.xlsx
  const match = disposition.match(/filename\*?=(?:UTF-8'')?["']?([^"';\n]+)/)
  if (match) filename = decodeURIComponent(match[1])
}

// 创建下载链接
const blob = await res.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)

7. 请求超时设置

为什么要设置超时?为什么不都用同一个超时时间?

超时是为了防止请求"卡住"。如果服务器没有响应,浏览器会一直等待,用户体验很差。

不同接口需要不同的超时时间:

  • 普通查询接口:15 秒足够,服务器一般 1-2 秒就能返回
  • 文件上传:可能需要 1 分钟甚至更长,取决于文件大小和网速
  • 数据导出:大数据量导出可能需要 2-3 分钟
  • 超时太短:会误判为失败,用户看到"请求超时"但其实服务器还在处理
  • 超时太长:用户等待体验差,可能以为页面卡死了
js
// 超时时间要根据接口特点设置
const service = axios.create({
  timeout: 15000   // 默认 15 秒
})

// 文件上传/导出等长操作单独设置
request.post('/api/upload', formData, { timeout: 60000 })    // 1 分钟
request.get('/api/export', { timeout: 120000, responseType: 'blob' })  // 2 分钟

8. 接口返回 302 重定向

js
// 问题:axios 默认自动跟随 302,拿不到重定向的响应

// fetch 默认也会自动跟随

// 如果需要手动处理重定向:
fetch('/api/redirect', {
  redirect: 'manual'   // 不自动跟随
}).then(res => {
  console.log(res.status)           // 0(opaque redirect)
  console.log(res.headers.get('Location'))  // 重定向地址
})

9. content-type 不匹配

为什么不能手动设置 FormData 的 content-type?

FormData 发送文件时,浏览器会自动设置 Content-Type: multipart/form-data; boundary=xxx。这个 boundary 是一个随机字符串,用来分隔表单中的多个字段。

如果你手动设置 Content-Type: application/json,就会破坏这个机制,后端无法解析表单数据,导致上传失败。

简单记住:

  • 发送 JSON 数据:Content-Type: application/json(axios 默认)
  • 发送 FormData/文件:不要手动设置,让浏览器自动设置
js
// ❌ 发送 JSON 数据但 content-type 不对
axios.post('/api/user', { name: '张三' })  // 自动设置 application/json ✅

// ❌ 发送 FormData 但手动设了 content-type
const form = new FormData()
form.append('file', file)
axios.post('/api/upload', form, {
  headers: { 'Content-Type': 'application/json' }  // ❌ 会破坏 boundary
})

// ✅ FormData 不要手动设置 content-type,让浏览器自动设置
axios.post('/api/upload', form)  // 浏览器自动设置 multipart/form-data; boundary=xxx

10. HTTP 缓存不生效

nginx
# 问题:接口设置了缓存但浏览器不缓存

# Cache-Control 指令说明:
# - no-cache:每次使用缓存前必须向服务器验证(不是不缓存,而是"协商缓存")
# - no-store:完全不缓存,每次都从服务器获取
# - max-age=N:缓存 N 秒内有效,期间直接使用缓存不请求服务器
# - public:允许 CDN 等中间代理缓存
# - private:只允许浏览器缓存,不允许代理缓存
# - immutable:资源永远不变,浏览器不会发送验证请求

# 原因一:Cache-Control 设置了 no-cache
# no-cache = 每次都向服务器验证(不是不缓存)

# 原因二:请求头有 Cache-Control: no-cache
# 浏览器开发者工具勾选了 "Disable cache"

# 原因三:URL 有随机参数
# /api/data?_t=123456  →  每次 URL 不同,缓存失效
bash
# 推荐的缓存策略
# HTML(入口文件):不缓存或协商缓存
Cache-Control: no-cache

# 静态资源(JS/CSS/图片,文件名带 hash):强缓存 1 年
Cache-Control: public, max-age=31536000, immutable

# API 接口:根据业务需求设置
Cache-Control: max-age=60    # 缓存 60 秒

真实场景

场景 1:登录后请求接口仍返回 401

  • 问题:用户已登录,但调用接口时后端返回 401 未授权
  • 原因:跨域请求默认不携带 Cookie,未设置 withCredentials: true
  • 解决:前端设置 withCredentials: true,后端设置 Access-Control-Allow-Credentials: true

场景 2:Token 过期后页面反复跳转登录页

  • 问题:Token 过期时,页面同时发出 10 个请求,每个都触发刷新 Token,导致多次跳转登录
  • 原因:未做 Token 刷新的并发控制
  • 解决:使用标志位和队列,确保只刷新一次 Token,其他请求等待新 Token 后重试

场景 3:文件上传总是失败

  • 问题:使用 FormData 上传文件,后端接收不到数据
  • 原因:手动设置了 Content-Type: application/json,破坏了 multipart/form-data 的 boundary
  • 解决:删除手动设置的 Content-Type,让浏览器自动设置

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准