【AJAX-Day3】Axios深入与请求拦截

3 阅读4分钟

【AJAX-Day3】Axios深入与请求拦截

🎯 核心目标:掌握 Axios 实例配置、拦截器、请求取消、文件上传下载、Fetch API


一、Axios 实例与全局配置

1.1 为什么需要实例?

当项目中需要请求多个不同的服务(如业务接口、日志接口、第三方接口),每个服务的 baseURL、超时时间不同,就需要创建多个独立的 Axios 实例。

// 创建实例(推荐)
const http = axios.create({
  baseURL: 'https://api.example.com',  // 基础URL(所有请求都会拼接它)
  timeout: 5000,                        // 超时时间(毫秒)
  headers: {
    'Content-Type': 'application/json'
  }
})

// 使用实例(不影响全局 axios)
http.get('/users')  // 实际请求:https://api.example.com/users

// 另一个实例(不同服务器)
const logHttp = axios.create({
  baseURL: 'https://log.example.com',
  timeout: 3000
})

1.2 默认配置

// 全局默认配置(所有 axios 请求都受影响,谨慎使用)
axios.defaults.baseURL = 'https://api.example.com'
axios.defaults.timeout = 10000
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`

// 实例默认配置(推荐,只影响该实例)
const http = axios.create({ baseURL: '...' })
http.defaults.timeout = 8000

二、拦截器(Interceptors)

2.1 拦截器是什么?

拦截器可以在请求发出前响应返回后统一处理逻辑,是实际项目中最重要的机制。

请求拦截器                          响应拦截器
onFulfilled → 修改请求配置           onFulfilled → 处理成功响应
onRejected  → 处理请求配置错误       onRejected  → 处理错误响应

请求 → [请求拦截器] → 网络 → [响应拦截器].then/.catch

2.2 请求拦截器

const http = axios.create({ baseURL: '/api' })

// 添加请求拦截器
http.interceptors.request.use(
  (config) => {
    // 在请求发出前做什么
    
    // 1. 自动注入 Token
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    
    // 2. 显示全局 Loading
    showLoading()
    
    // 3. 打印请求日志
    console.log(`[请求] ${config.method?.toUpperCase()} ${config.url}`)
    
    // 必须返回 config
    return config
  },
  (error) => {
    // 请求配置出错(很少发生)
    return Promise.reject(error)
  }
)

2.3 响应拦截器

http.interceptors.response.use(
  (response) => {
    // HTTP 状态码 2xx 时触发
    
    // 1. 关闭全局 Loading
    hideLoading()
    
    // 2. 统一处理业务状态码
    const { code, data, message } = response.data
    if (code === 200) {
      return data  // 直接返回业务数据,外层不需要 .data.data
    } else if (code === 401) {
      // Token 失效,跳转登录
      localStorage.removeItem('token')
      router.push('/login')
      return Promise.reject(new Error(message))
    } else {
      // 其他业务错误
      ElMessage.error(message || '操作失败')
      return Promise.reject(new Error(message))
    }
  },
  (error) => {
    // HTTP 状态码非 2xx 时触发
    hideLoading()
    
    if (error.response) {
      const { status } = error.response
      
      switch (status) {
        case 401:
          ElMessage.error('登录已过期,请重新登录')
          router.push('/login')
          break
        case 403:
          ElMessage.error('您没有权限执行此操作')
          break
        case 404:
          ElMessage.error('请求的资源不存在')
          break
        case 500:
          ElMessage.error('服务器繁忙,请稍后再试')
          break
        default:
          ElMessage.error(`请求失败:${status}`)
      }
    } else if (error.code === 'ECONNABORTED') {
      ElMessage.error('请求超时,请检查网络')
    } else if (!error.response) {
      ElMessage.error('网络错误,请检查网络连接')
    }
    
    return Promise.reject(error)
  }
)

2.4 完整的 request.js 封装

// src/utils/request.js
import axios from 'axios'
import router from '@/router'

const http = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  timeout: 10000,
  headers: { 'Content-Type': 'application/json' }
})

// 请求拦截
http.interceptors.request.use(config => {
  const token = localStorage.getItem('access_token')
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
})

// 响应拦截
http.interceptors.response.use(
  response => response.data,  // 直接返回 data
  error => {
    if (error.response?.status === 401) {
      localStorage.clear()
      router.replace('/login')
    }
    return Promise.reject(error)
  }
)

export default http

三、请求取消(Cancel Token)

3.1 为什么需要取消请求?

  • 用户快速切换页面,上一页的请求还未完成
  • 搜索框快速输入,旧请求结果覆盖新结果
  • 重复点击按钮,避免重复提交

3.2 AbortController(现代方式,推荐)

// 创建控制器
const controller = new AbortController()

// 发送请求时传入 signal
axios.get('/api/data', {
  signal: controller.signal
}).then(data => {
  console.log(data)
}).catch(error => {
  if (axios.isCancel(error)) {
    console.log('请求被取消:', error.message)
  }
})

// 取消请求(如:用户离开页面)
controller.abort()  // 传入原因:controller.abort('用户取消')

3.3 搜索防取消实战

let controller = null

async function search(keyword) {
  // 取消上一次未完成的请求
  if (controller) {
    controller.abort()
  }
  controller = new AbortController()
  
  try {
    const { data } = await axios.get('/api/search', {
      params: { keyword },
      signal: controller.signal
    })
    renderResults(data)
  } catch (error) {
    if (!axios.isCancel(error)) {
      console.error('搜索失败:', error)
    }
  } finally {
    controller = null
  }
}

// 输入框监听
input.addEventListener('input', debounce(e => {
  search(e.target.value)
}, 300))

四、文件上传与下载

4.1 文件上传

// HTML
// <input type="file" id="fileInput" multiple>
// <button onclick="upload()">上传</button>

async function upload() {
  const fileInput = document.querySelector('#fileInput')
  const files = fileInput.files
  
  if (!files.length) {
    alert('请选择文件')
    return
  }
  
  const formData = new FormData()
  // 添加文件(支持多文件)
  for (const file of files) {
    formData.append('files', file)
  }
  // 添加额外参数
  formData.append('userId', '123')
  
  try {
    const { data } = await http.post('/api/upload', formData, {
      headers: {
        'Content-Type': 'multipart/form-data'  // 必须!
      },
      onUploadProgress: (progressEvent) => {
        // 上传进度
        const percent = Math.round(
          (progressEvent.loaded / progressEvent.total) * 100
        )
        updateProgress(percent)  // 更新进度条
        console.log(`上传进度:${percent}%`)
      }
    })
    console.log('上传成功:', data)
  } catch (error) {
    console.error('上传失败:', error)
  }
}

4.2 文件下载

async function download(fileId, fileName) {
  try {
    const response = await http.get(`/api/files/${fileId}`, {
      responseType: 'blob',  // 关键!告诉 axios 响应是二进制数据
      onDownloadProgress: (e) => {
        const percent = Math.round((e.loaded / e.total) * 100)
        console.log(`下载进度:${percent}%`)
      }
    })
    
    // 创建下载链接
    const url = URL.createObjectURL(response.data)
    const a = document.createElement('a')
    a.href = url
    a.download = fileName || 'download'
    a.click()
    URL.revokeObjectURL(url)  // 释放内存
  } catch (error) {
    console.error('下载失败:', error)
  }
}

五、Fetch API

5.1 Fetch 简介

Fetch 是浏览器原生提供的 HTTP 请求 API(无需安装),基于 Promise,是 XHR 的现代替代品。

// 基本用法
const response = await fetch('https://api.example.com/users')
// ⚠️ fetch 只有在网络错误时才 reject,HTTP 4xx/5xx 不会 reject!
if (!response.ok) {
  throw new Error(`HTTP Error: ${response.status}`)
}
const data = await response.json()  // 解析 JSON
console.log(data)

5.2 Fetch vs Axios 对比

特性FetchAxios
内置/第三方浏览器内置第三方库
Node.js 支持v18+ 支持支持(任意版本)
默认 JSON❌ 需手动 .json()✅ 自动解析
错误处理❌ 4xx/5xx 不会 reject✅ 4xx/5xx 自动 reject
超时控制需手动实现✅ timeout 参数
拦截器❌ 无✅ 有
请求取消AbortControllerAbortController
上传进度❌ 不支持✅ onUploadProgress

5.3 Fetch 封装

async function request(url, options = {}) {
  const { method = 'GET', data, params, headers = {}, timeout = 10000 } = options
  
  // 处理查询参数
  if (params) {
    url += '?' + new URLSearchParams(params)
  }
  
  // 超时控制
  const controller = new AbortController()
  const timerId = setTimeout(() => controller.abort(), timeout)
  
  // Token
  const token = localStorage.getItem('token')
  if (token) headers.Authorization = `Bearer ${token}`
  
  try {
    const response = await fetch(url, {
      method,
      headers: {
        'Content-Type': 'application/json',
        ...headers
      },
      body: data ? JSON.stringify(data) : undefined,
      signal: controller.signal
    })
    
    clearTimeout(timerId)
    
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`)
    }
    
    return await response.json()
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error('请求超时')
    }
    throw error
  }
}

六、知识图谱

Axios深入与请求拦截
├── Axios 实例
│   ├── axios.create({ baseURL, timeout, headers })
│   └── 多实例:不同服务用不同实例
├── 拦截器
│   ├── 请求拦截:注入 Token、显示 Loading
│   ├── 响应拦截:统一处理业务码、错误提示、跳登录
│   └── 封装 request.js(项目标配)
├── 请求取消
│   ├── AbortController(现代,推荐)
│   └── 应用:搜索防闪、页面切换取消
├── 文件操作
│   ├── 上传:FormData + multipart/form-data + onUploadProgress
│   └── 下载:responseType: 'blob' + createObjectURL
└── Fetch API
    ├── 浏览器原生,基于 Promise
    ├── 坑:4xx/5xx 不自动 reject,需判断 response.ok
    └── 对比 Axios:功能少但无需安装

七、高频面试题

Q1:Axios 拦截器的执行顺序?

多个请求拦截器:后添加的先执行(栈结构);多个响应拦截器:先添加的先执行(队列结构)。请求拦截器(后→前)→ 请求发出 → 响应拦截器(前→后)。

Q2:如何实现请求的 Loading 效果?

在请求拦截器中 showLoading(),在响应拦截器的 fulfilledrejected 中都 hideLoading()。注意并发请求时用计数器而非简单布尔值,避免第一个响应回来就把 loading 关掉。

Q3:Fetch 和 Axios 哪个更好?

各有优缺点。Fetch 是原生 API,无依赖,但缺少拦截器、不会自动处理错误状态码。Axios 功能更完善(拦截器、超时、进度、取消),适合生产项目。现代项目通常选 Axios。

Q4:如何防止重复提交?

// 方案一:按钮 disabled
button.disabled = true
await axios.post('/api/submit', data)
button.disabled = false

// 方案二:请求拦截器去重(相同 URL 正在请求中则取消新请求)
const pendingMap = new Map()
interceptors.request.use(config => {
  const key = `${config.method}:${config.url}`
  if (pendingMap.has(key)) {
    config.cancelToken = new axios.CancelToken(c => c('重复请求'))
  } else {
    pendingMap.set(key, true)
  }
  return config
})

⬅️ 上一篇Day2 - Promise与回调地狱 ➡️ 下一篇Day4 - Node.js、Express与跨域