Axios 高频面试题

Axios 高频面试题

1. Axios 是什么?有哪些核心特点?

Axios 是一个基于 Promise 的 HTTP 客户端,可以在浏览器和 Node.js 中使用。

特性 说明
Promise API 支持 async/await,链式调用
浏览器 + Node.js 浏览器用 XMLHttpRequest,Node.js 用 http 模块
自动转换数据 请求时自动序列化 JSON,响应时自动解析 JSON
拦截器 请求/响应拦截,可统一处理 token、错误
取消请求 支持 AbortController 取消未完成的请求
防 XSRF 自动读取 cookie 中的 XSRF-TOKEN
超时控制 可设置请求超时时间
进度监控 支持上传/下载进度回调
javascript 复制代码
import axios from 'axios'

// ✅ 基本 GET 请求
const getUser = async (id) => {
  const { data } = await axios.get(`/api/users/${id}`)
  return data
}

// ✅ POST 请求
const createUser = async (userData) => {
  const { data } = await axios.post('/api/users', userData)
  return data
}

// ✅ 带查询参数的 GET
const getUsers = async (params) => {
  const { data } = await axios.get('/api/users', { params })
  return data
}
// 等价于:GET /api/users?page=1&size=10
getUsers({ page: 1, size: 10 })

// ✅ 其他请求方法
axios.put('/api/users/1', { name: '新名字' })     // 完整更新
axios.patch('/api/users/1', { name: '新名字' })   // 部分更新
axios.delete('/api/users/1')                        // 删除
axios.head('/api/users')                            // 获取响应头
axios.options('/api/users')                         // 预检请求

Axios vs Fetch 对比

特性 Axios Fetch
浏览器兼容 需安装,兼容 IE11 原生支持,IE 不支持
自动 JSON 转换 ✅ 自动 ❌ 需手动 res.json()
拦截器 ✅ 内置 ❌ 需自行封装
超时设置 timeout 配置 ❌ 需 AbortController + setTimeout
取消请求 ✅ AbortController ✅ AbortController
错误处理 非 2xx 自动 reject 只在网络错误时 reject
Node.js 支持 ❌(需 node-fetch/undici)
上传进度 onUploadProgress ❌ 不支持
包大小 ~14KB(gzipped) 0(原生)

💡 面试加分点 :能说出 Axios 底层在浏览器中使用 XMLHttpRequest,在 Node.js 中使用 http/https 模块(适配器模式)。Fetch 是浏览器原生 API,基于 ReadableStream


2. 如何创建 Axios 实例?为什么推荐?

为什么要创建实例:不同业务模块可能对接不同后端服务,需要不同的 baseURL、超时时间、请求头等配置。创建独立实例可以互不干扰。

javascript 复制代码
import axios from 'axios'

// ✅ 创建实例(推荐:不同业务使用不同实例)
const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
  withCredentials: true, // 跨域携带 cookie
})

// ✅ 多实例场景:对接不同服务
const userService = axios.create({
  baseURL: 'https://user.api.com',
  timeout: 5000,
})

const orderService = axios.create({
  baseURL: 'https://order.api.com',
  timeout: 15000, // 订单服务可能较慢
})

const uploadService = axios.create({
  baseURL: 'https://upload.api.com',
  timeout: 60000, // 上传文件超时更长
  headers: { 'Content-Type': 'multipart/form-data' },
})

// 使用实例发送请求
const getUser = (id) => userService.get(`/users/${id}`)
const createOrder = (data) => orderService.post('/orders', data)
javascript 复制代码
// ❌ 错误做法:直接修改全局默认配置(会影响所有请求)
axios.defaults.baseURL = 'https://api.example.com'
axios.defaults.timeout = 10000
// 这样所有使用 axios 的地方都会受影响

// ✅ 正确做法:创建独立实例
const request = axios.create({ baseURL: '/api', timeout: 10000 })

💡 面试加分点 :实例拥有独立的拦截器和配置,修改实例配置不会影响全局 axios 或其他实例。配置优先级:请求配置 > 实例配置 > 全局默认


3. 请求拦截器和响应拦截器的执行顺序是什么?

拦截器是 Axios 最核心的功能之一,可以在请求发送前和响应到达后进行统一处理。

执行顺序

复制代码
请求拦截器2 → 请求拦截器1 → 发送请求 → 响应拦截器1 → 响应拦截器2

⚠️ 注意 :请求拦截器是后添加的先执行 (类似栈),响应拦截器是先添加的先执行(类似队列)。

javascript 复制代码
import axios from 'axios'

const request = axios.create({ baseURL: '/api', timeout: 10000 })

// === 请求拦截器1:添加 token ===
request.interceptors.request.use(
  (config) => {
    console.log('请求拦截器1')
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => Promise.reject(error)
)

// === 请求拦截器2:添加 loading ===
request.interceptors.request.use(
  (config) => {
    console.log('请求拦截器2') // 后添加,但先执行!
    if (config.showLoading !== false) {
      showGlobalLoading()
    }
    return config
  },
  (error) => Promise.reject(error)
)

// === 响应拦截器1:处理业务状态码 ===
request.interceptors.response.use(
  (response) => {
    console.log('响应拦截器1') // 先添加,先执行
    hideGlobalLoading()
    const { data } = response
    if (data.code !== 0) {
      message.error(data.message || '请求失败')
      return Promise.reject(new Error(data.message))
    }
    return data.data // 直接返回业务数据
  },
  (error) => {
    hideGlobalLoading()
    return Promise.reject(error)
  }
)

// === 响应拦截器2:统一错误处理 ===
request.interceptors.response.use(null, (error) => {
  console.log('响应拦截器2')
  if (error.response) {
    const { status } = error.response
    const errorMap = {
      400: '请求参数错误',
      401: '登录已过期,请重新登录',
      403: '没有访问权限',
      404: '请求的资源不存在',
      429: '请求过于频繁,请稍后再试',
      500: '服务器内部错误',
      502: '网关错误',
      503: '服务暂时不可用',
    }
    message.error(errorMap[status] || `未知错误(${status})`)

    if (status === 401) {
      localStorage.removeItem('token')
      window.location.href = '/login'
    }
  } else if (error.code === 'ECONNABORTED') {
    message.error('请求超时,请检查网络')
  } else if (error.message === 'Network Error') {
    message.error('网络连接失败')
  }
  return Promise.reject(error)
})

export default request
javascript 复制代码
// ✅ 移除拦截器
const interceptorId = request.interceptors.request.use(config => config)
request.interceptors.request.eject(interceptorId) // 通过 ID 移除

💡 面试加分点 :Axios 内部将请求拦截器和响应拦截器与请求方法组成 Promise 链(chain 数组),用 unshift 添加请求拦截器、push 添加响应拦截器,最终通过 Promise.then 链式执行。


4. 如何取消请求?有哪些应用场景?

两种取消方式

方式 版本 状态
AbortController v0.22.0+ ✅ 推荐(原生 Web API)
CancelToken 旧版 ⚠️ 已废弃,但仍可用
javascript 复制代码
import axios from 'axios'

// ✅ 方式1:AbortController(推荐)
const controller = new AbortController()

axios.get('/api/users', {
  signal: controller.signal,
}).then(res => {
  console.log(res.data)
}).catch(err => {
  if (axios.isCancel(err)) {
    console.log('请求已取消:', err.message)
  }
})

// 取消请求
controller.abort('用户取消了操作')

// ⚠️ 方式2:CancelToken(已废弃,了解即可)
const CancelToken = axios.CancelToken
const source = CancelToken.source()
axios.get('/api/users', { cancelToken: source.token })
source.cancel('取消原因')

应用场景

javascript 复制代码
// ✅ 场景1:React 组件卸载时取消请求(防止内存泄漏)
import { useEffect, useState } from 'react'

function UserProfile({ userId }) {
  const [user, setUser] = useState(null)

  useEffect(() => {
    const controller = new AbortController()

    axios.get(`/api/users/${userId}`, { signal: controller.signal })
      .then(res => setUser(res.data))
      .catch(err => {
        if (!axios.isCancel(err)) console.error(err)
      })

    return () => controller.abort() // 组件卸载时取消
  }, [userId])

  return user ? <div>{user.name}</div> : <div>Loading...</div>
}
javascript 复制代码
// ✅ 场景2:搜索防抖 ------ 新请求发出前取消旧请求
function useSearch() {
  const controllerRef = useRef(null)

  const search = async (keyword) => {
    // 取消上一次请求
    controllerRef.current?.abort()

    const controller = new AbortController()
    controllerRef.current = controller

    try {
      const { data } = await axios.get('/api/search', {
        params: { keyword },
        signal: controller.signal,
      })
      return data
    } catch (err) {
      if (!axios.isCancel(err)) throw err
    }
  }

  return { search }
}
javascript 复制代码
// ✅ 场景3:Tab 切换时取消上一个 Tab 的请求
function TabContent({ activeTab }) {
  const [data, setData] = useState(null)

  useEffect(() => {
    const controller = new AbortController()

    axios.get(`/api/tab/${activeTab}`, { signal: controller.signal })
      .then(res => setData(res.data))
      .catch(err => {
        if (!axios.isCancel(err)) console.error(err)
      })

    return () => controller.abort()
  }, [activeTab])

  return <div>{JSON.stringify(data)}</div>
}

💡 面试加分点AbortController 是 Web 标准 API,不仅可以取消 Axios 请求,也可以取消 fetchaddEventListener 等。一个 AbortController 可以通过 signal 同时关联多个请求,调用一次 abort() 即可取消所有关联请求。


5. 如何实现请求重试?

网络不稳定时,自动重试可以提高请求成功率。通常结合指数退避策略(每次重试间隔逐步增大)。

javascript 复制代码
import axios from 'axios'

const request = axios.create({ baseURL: '/api', timeout: 5000 })

// ✅ 方式1:拦截器实现重试(简洁)
request.interceptors.response.use(null, async (error) => {
  const config = error.config
  if (!config || !config.retry) return Promise.reject(error)

  config.__retryCount = config.__retryCount || 0

  if (config.__retryCount >= config.retry) {
    return Promise.reject(error)
  }

  config.__retryCount++

  // 指数退避:1s → 2s → 4s → 8s...
  const delay = config.retryDelay
    ? config.retryDelay * Math.pow(2, config.__retryCount - 1)
    : 1000

  console.log(`第 ${config.__retryCount} 次重试,延迟 ${delay}ms`)

  await new Promise(resolve => setTimeout(resolve, delay))
  return request(config)
})

// 使用:设置重试 3 次,初始间隔 1 秒
request.get('/api/data', {
  retry: 3,
  retryDelay: 1000,
})
javascript 复制代码
// ✅ 方式2:axios-retry 插件(生产推荐)
import axiosRetry from 'axios-retry'

axiosRetry(request, {
  retries: 3,
  retryDelay: axiosRetry.exponentialDelay, // 指数退避
  retryCondition: (error) => {
    // 只对网络错误和 5xx 错误重试
    return axiosRetry.isNetworkOrIdempotentRequestError(error)
      || error.response?.status >= 500
  },
  onRetry: (retryCount, error) => {
    console.log(`重试第 ${retryCount} 次:`, error.message)
  },
})
javascript 复制代码
// ✅ 方式3:独立工具函数(灵活)
async function retryRequest(fn, retries = 3, delay = 1000) {
  for (let i = 0; i <= retries; i++) {
    try {
      return await fn()
    } catch (error) {
      if (i === retries) throw error
      console.log(`第 ${i + 1} 次重试...`)
      await new Promise(r => setTimeout(r, delay * Math.pow(2, i)))
    }
  }
}

// 使用
const data = await retryRequest(
  () => axios.get('/api/data'),
  3,
  1000
)

💡 面试加分点 :重试时注意幂等性 ------ GET/PUT/DELETE 天然幂等可安全重试,POST 不幂等需谨慎(可能导致重复创建资源)。生产环境推荐使用 axios-retry 插件。


6. 如何实现并发请求?如何控制并发数?

javascript 复制代码
import axios from 'axios'

// ✅ Promise.all:全部成功才成功
const fetchDashboard = async () => {
  const [users, orders, stats] = await Promise.all([
    axios.get('/api/users'),
    axios.get('/api/orders'),
    axios.get('/api/stats'),
  ])
  return {
    users: users.data,
    orders: orders.data,
    stats: stats.data,
  }
}

// ✅ Promise.allSettled:不管成功失败都返回
const fetchSafe = async () => {
  const results = await Promise.allSettled([
    axios.get('/api/users'),
    axios.get('/api/orders'),
    axios.get('/api/stats'),
  ])

  return results.map(result => {
    if (result.status === 'fulfilled') return result.value.data
    console.error('请求失败:', result.reason)
    return null
  })
}
javascript 复制代码
// ✅ 控制并发数量(核心面试题)
async function asyncPool(limit, tasks) {
  const results = []
  const executing = new Set()

  for (const [index, task] of tasks.entries()) {
    const promise = Promise.resolve().then(() => task())

    results[index] = promise
    executing.add(promise)

    const clean = () => executing.delete(promise)
    promise.then(clean, clean)

    // 当并发数达到上限时,等待最快完成的一个
    if (executing.size >= limit) {
      await Promise.race(executing)
    }
  }

  return Promise.all(results)
}

// 使用示例:同时最多 3 个并发请求
const urls = ['/api/1', '/api/2', '/api/3', '/api/4', '/api/5', '/api/6']
const tasks = urls.map(url => () => axios.get(url))
const results = await asyncPool(3, tasks)
javascript 复制代码
// ✅ 简化版:按批次并发
async function batchRequest(urls, batchSize = 3) {
  const results = []
  for (let i = 0; i < urls.length; i += batchSize) {
    const batch = urls.slice(i, i + batchSize)
    const batchResults = await Promise.all(
      batch.map(url => axios.get(url).catch(err => ({ error: err })))
    )
    results.push(...batchResults)
  }
  return results
}

💡 面试加分点Promise.all 全部成功才成功(短路失败),Promise.allSettled 全部完成才返回(无论成败)。asyncPool 是一个高频手写题,核心思想是维护一个执行池,达到上限时用 Promise.race 等待最快完成的任务。


7. 如何上传文件?如何实现进度监控?

javascript 复制代码
import axios from 'axios'

// ✅ 单文件上传 + 进度监控
const uploadFile = async (file, onProgress) => {
  const formData = new FormData()
  formData.append('file', file)
  formData.append('type', 'avatar')

  const { data } = await axios.post('/api/upload', formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
    onUploadProgress: (event) => {
      if (event.total) {
        const percent = Math.round((event.loaded * 100) / event.total)
        onProgress?.(percent)
        console.log(`上传进度: ${percent}%`)
      }
    },
  })
  return data
}

// ✅ 多文件上传
const uploadMultiple = async (files) => {
  const formData = new FormData()
  // 同一字段名追加多个文件
  Array.from(files).forEach(file => {
    formData.append('files', file)
  })
  return axios.post('/api/upload/batch', formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
  })
}

// ✅ 下载文件 + 进度监控
const downloadFile = async (url, filename, onProgress) => {
  const response = await axios.get(url, {
    responseType: 'blob', // 关键:指定响应类型为 blob
    onDownloadProgress: (event) => {
      if (event.total) {
        const percent = Math.round((event.loaded * 100) / event.total)
        onProgress?.(percent)
      }
    },
  })

  // 创建下载链接
  const blob = new Blob([response.data])
  const link = document.createElement('a')
  link.href = URL.createObjectURL(blob)
  link.download = filename
  link.click()
  URL.revokeObjectURL(link.href) // 释放内存
}
javascript 复制代码
// ✅ 大文件分片上传(核心考点)
async function uploadLargeFile(file, chunkSize = 5 * 1024 * 1024) {
  const totalChunks = Math.ceil(file.size / chunkSize)
  const fileHash = await calculateFileHash(file) // 计算文件 hash

  for (let i = 0; i < totalChunks; i++) {
    const start = i * chunkSize
    const end = Math.min(start + chunkSize, file.size)
    const chunk = file.slice(start, end)

    const formData = new FormData()
    formData.append('chunk', chunk)
    formData.append('hash', fileHash)
    formData.append('index', i)
    formData.append('total', totalChunks)
    formData.append('filename', file.name)

    await axios.post('/api/upload/chunk', formData)
    console.log(`分片 ${i + 1}/${totalChunks} 上传完成`)
  }

  // 通知服务器合并分片
  await axios.post('/api/upload/merge', {
    hash: fileHash,
    filename: file.name,
    totalChunks,
  })
}

💡 面试加分点 :大文件上传通常结合 断点续传 (上传前查询已上传的分片,跳过已传部分)和 秒传(通过文件 hash 判断服务器是否已有该文件)来优化用户体验。


8. 如何实现 Token 无感刷新?

当 Access Token 过期时,自动使用 Refresh Token 获取新 Token,用户无感知。

关键难点

  1. Token 过期后正在发送的请求如何处理?
  2. 多个请求同时 401,如何避免重复刷新?
  3. 刷新 Token 时的新请求怎么办?
javascript 复制代码
import axios from 'axios'

let isRefreshing = false       // 是否正在刷新 token
let pendingRequests = []       // 等待 token 刷新的请求队列

const request = axios.create({ baseURL: '/api', timeout: 10000 })

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

// 响应拦截器:处理 token 过期
request.interceptors.response.use(
  (response) => response.data,
  async (error) => {
    const { config, response } = error

    // 非 401 或已重试过,直接抛出
    if (response?.status !== 401 || config._retry) {
      return Promise.reject(error)
    }

    // 如果正在刷新 token,将请求加入等待队列
    if (isRefreshing) {
      return new Promise((resolve, reject) => {
        pendingRequests.push({ resolve, reject, config })
      })
    }

    config._retry = true // 标记已重试,防止死循环
    isRefreshing = true

    try {
      const refreshToken = localStorage.getItem('refresh_token')

      // ⚠️ 注意:刷新请求不能使用 request 实例(会被拦截),要用原始 axios
      const { data } = await axios.post('/api/auth/refresh', {
        refreshToken,
      })

      // 更新 token
      localStorage.setItem('access_token', data.accessToken)
      localStorage.setItem('refresh_token', data.refreshToken)

      // 用新 token 重试所有等待中的请求
      pendingRequests.forEach(({ resolve, config: pendingConfig }) => {
        pendingConfig.headers.Authorization = `Bearer ${data.accessToken}`
        resolve(request(pendingConfig))
      })
      pendingRequests = []

      // 重试当前请求
      config.headers.Authorization = `Bearer ${data.accessToken}`
      return request(config)

    } catch (refreshError) {
      // 刷新 token 也失败,强制退出登录
      pendingRequests.forEach(({ reject: rejectFn }) => rejectFn(refreshError))
      pendingRequests = []
      localStorage.clear()
      window.location.href = '/login'
      return Promise.reject(refreshError)

    } finally {
      isRefreshing = false
    }
  }
)

export default request

💡 面试加分点

  • 刷新 token 的请求必须使用原始 axios 而不是封装的实例,否则会触发拦截器导致死循环
  • Refresh Token 也有过期时间,通常比 Access Token 长得多(如 7 天 vs 2 小时)
  • 为了更安全,Refresh Token 应存储在 HttpOnly Cookie 中,而不是 localStorage

9. Axios 的配置优先级是什么?

Axios 配置按以下优先级生效(从高到低):

复制代码
请求级配置 > 实例级配置 > 全局默认配置
javascript 复制代码
import axios from 'axios'

// 第1层:全局默认配置(最低优先级)
axios.defaults.baseURL = 'https://global.api.com'
axios.defaults.timeout = 30000
axios.defaults.headers.common['X-Custom'] = 'global'

// 第2层:实例级配置(中等优先级)
const request = axios.create({
  baseURL: 'https://instance.api.com',  // 覆盖全局
  timeout: 10000,                        // 覆盖全局
})

// 第3层:请求级配置(最高优先级)
request.get('/users', {
  timeout: 5000,  // 覆盖实例配置
  headers: { 'X-Request-Id': 'abc123' },
})

// 最终生效的配置:
// baseURL: 'https://instance.api.com' (实例级)
// timeout: 5000                       (请求级)
// headers.X-Custom: 'global'          (全局级,未被覆盖)
// headers.X-Request-Id: 'abc123'      (请求级)

💡 面试加分点 :Axios 源码中使用 mergeConfig 方法合并配置,对于 headers 等对象类型会深度合并,对于 baseURLtimeout 等标量类型会直接覆盖。


10. Axios 的适配器模式是什么?

Axios 之所以能同时运行在浏览器和 Node.js 中,是因为使用了适配器模式(Adapter Pattern)

scss 复制代码
┌──────────────────────────┐
│        Axios 核心         │
│  (Promise 链 + 拦截器)    │
└────────────┬─────────────┘
             │
    ┌────────┴────────┐
    │                 │
┌───▼───┐      ┌─────▼─────┐
│ 浏览器 │      │  Node.js  │
│  适配器 │      │   适配器   │
│ (XHR)  │      │  (http)   │
└────────┘      └───────────┘
javascript 复制代码
import axios from 'axios'

// Axios 根据运行环境自动选择适配器
// 浏览器 → XMLHttpRequest
// Node.js → http/https 模块

// ✅ 自定义适配器(高级用法)
const mockAdapter = (config) => {
  return new Promise((resolve) => {
    // 模拟接口返回,常用于测试
    if (config.url === '/api/users') {
      resolve({
        data: [{ id: 1, name: '张三' }],
        status: 200,
        statusText: 'OK',
        headers: {},
        config,
      })
    }
  })
}

const testRequest = axios.create({
  adapter: mockAdapter, // 使用自定义适配器
})

// 不会发送真实请求,直接返回 mock 数据
const { data } = await testRequest.get('/api/users')
console.log(data) // [{ id: 1, name: '张三' }]

💡 面试加分点 :适配器模式是一种结构型设计模式,将不兼容的接口转换为统一的接口。Axios 通过 getDefaultAdapter() 检测运行环境,自动选择 XHRhttp 适配器。你也可以通过自定义适配器实现 mock、日志记录等功能。


11. 如何封装一个完整的请求工具类?

javascript 复制代码
// utils/request.js ------ 企业级请求封装
import axios from 'axios'
import { message } from 'antd' // 或 ElementPlus 的 ElMessage

// ========== 创建实例 ==========
const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 15000,
  headers: { 'Content-Type': 'application/json' },
})

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

    // 2. 防重复提交:POST/PUT/PATCH 添加时间戳
    if (['post', 'put', 'patch'].includes(config.method)) {
      config.data = {
        ...config.data,
        _t: Date.now(),
      }
    }

    return config
  },
  (error) => Promise.reject(error)
)

// ========== 响应拦截器 ==========
request.interceptors.response.use(
  (response) => {
    const { code, data, message: msg } = response.data

    // 统一处理业务状态码
    switch (code) {
      case 0:
      case 200:
        return data
      case 401:
        handleLogout()
        return Promise.reject(new Error('登录已过期'))
      default:
        message.error(msg || '请求失败')
        return Promise.reject(new Error(msg))
    }
  },
  (error) => {
    handleHttpError(error)
    return Promise.reject(error)
  }
)

// ========== HTTP 错误处理 ==========
function handleHttpError(error) {
  if (error.response) {
    const { status } = error.response
    const errorMap = {
      400: '请求参数错误',
      401: '登录已过期',
      403: '没有权限访问',
      404: '资源不存在',
      405: '请求方法不允许',
      408: '请求超时',
      429: '请求过于频繁',
      500: '服务器错误',
      502: '网关错误',
      503: '服务暂时不可用',
    }
    message.error(errorMap[status] || `请求错误(${status})`)
    if (status === 401) handleLogout()
  } else if (error.code === 'ECONNABORTED') {
    message.error('请求超时,请检查网络')
  } else if (error.message?.includes('Network Error')) {
    message.error('网络连接失败,请检查网络')
  }
}

function handleLogout() {
  localStorage.removeItem('access_token')
  localStorage.removeItem('refresh_token')
  window.location.href = '/login'
}

// ========== 导出便捷方法 ==========
export const get = (url, params, config) =>
  request.get(url, { params, ...config })

export const post = (url, data, config) =>
  request.post(url, data, config)

export const put = (url, data, config) =>
  request.put(url, data, config)

export const del = (url, config) =>
  request.delete(url, config)

export default request
javascript 复制代码
// api/user.js ------ 按模块组织接口
import { get, post, put, del } from '@/utils/request'

export const userApi = {
  getList: (params) => get('/users', params),
  getById: (id) => get(`/users/${id}`),
  create: (data) => post('/users', data),
  update: (id, data) => put(`/users/${id}`, data),
  remove: (id) => del(`/users/${id}`),
}

// 使用
const users = await userApi.getList({ page: 1, size: 10 })

💡 面试加分点:完整封装应包含:实例创建、请求/响应拦截、token 管理、错误处理、loading 控制、接口模块化。进阶可加入:请求缓存、请求去重、请求重试、取消请求等能力。


12. 如何实现请求去重(防重复提交)?

同一个接口短时间内被重复调用时,只保留最后一次请求或拒绝重复请求。

javascript 复制代码
import axios from 'axios'

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

// ========== 方案1:取消重复请求(保留最新的) ==========
const pendingMap = new Map()

// 生成请求的唯一 key
function getRequestKey(config) {
  const { method, url, params, data } = config
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}

// 添加请求到 pending 中
function addPending(config) {
  const key = getRequestKey(config)
  if (pendingMap.has(key)) {
    // 取消之前的重复请求
    pendingMap.get(key).abort()
  }
  const controller = new AbortController()
  config.signal = controller.signal
  pendingMap.set(key, controller)
}

// 移除 pending 请求
function removePending(config) {
  const key = getRequestKey(config)
  pendingMap.delete(key)
}

request.interceptors.request.use((config) => {
  addPending(config)
  return config
})

request.interceptors.response.use(
  (response) => {
    removePending(response.config)
    return response.data
  },
  (error) => {
    if (!axios.isCancel(error)) {
      removePending(error.config || {})
    }
    return Promise.reject(error)
  }
)
javascript 复制代码
// ========== 方案2:节流式防重复(相同请求间隔内拒绝) ==========
const throttleMap = new Map()

request.interceptors.request.use((config) => {
  // 只对 POST/PUT/PATCH 做防重复
  if (!['post', 'put', 'patch'].includes(config.method)) return config

  const key = getRequestKey(config)
  const now = Date.now()

  if (throttleMap.has(key) && now - throttleMap.get(key) < 2000) {
    // 2 秒内拒绝重复请求
    return Promise.reject(new Error('请勿重复提交'))
  }

  throttleMap.set(key, now)
  return config
})

💡 面试加分点 :防重复提交还可以通过 按钮 loading 状态 在 UI 层解决(简单场景),也可以通过后端 幂等性设计(如幂等 Key)从根本上解决。前端方案是辅助手段,不能完全依赖。


13. 如何实现请求缓存?

对于不常变化的数据(如省市区列表、字典数据),缓存请求结果可以避免重复请求。

javascript 复制代码
import axios from 'axios'

// ✅ 方案1:简单的内存缓存
class RequestCache {
  constructor(maxAge = 5 * 60 * 1000) { // 默认 5 分钟
    this.cache = new Map()
    this.maxAge = maxAge
  }

  generateKey(config) {
    const { method, url, params } = config
    return `${method}:${url}:${JSON.stringify(params || {})}`
  }

  get(key) {
    const entry = this.cache.get(key)
    if (!entry) return null
    if (Date.now() - entry.timestamp > this.maxAge) {
      this.cache.delete(key)
      return null
    }
    return entry.data
  }

  set(key, data) {
    this.cache.set(key, { data, timestamp: Date.now() })
  }

  clear() {
    this.cache.clear()
  }
}

const cache = new RequestCache()

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

request.interceptors.request.use((config) => {
  // 只缓存 GET 请求
  if (config.method === 'get' && config.cache !== false) {
    const key = cache.generateKey(config)
    const cached = cache.get(key)
    if (cached) {
      // 返回缓存结果,通过适配器跳过实际请求
      config.adapter = () => Promise.resolve({
        data: cached,
        status: 200,
        statusText: 'OK (cached)',
        headers: {},
        config,
      })
    }
  }
  return config
})

request.interceptors.response.use((response) => {
  if (response.config.method === 'get' && response.config.cache !== false) {
    const key = cache.generateKey(response.config)
    cache.set(key, response.data)
  }
  return response.data
})

// 使用
const data1 = await request.get('/cities')      // 发送请求
const data2 = await request.get('/cities')      // 命中缓存,不发请求
const data3 = await request.get('/users', {
  cache: false,  // 禁用缓存
})

💡 面试加分点 :生产环境推荐使用专业的数据请求库如 TanStack Query (React Query)SWR,它们内置了缓存管理、过期策略、后台更新、乐观更新等能力,比手动实现更完善。


14. Axios 如何处理 Content-Type?

不同类型的请求数据需要设置不同的 Content-Type

Content-Type 用途 Axios 行为
application/json JSON 数据(默认) 自动序列化对象为 JSON
multipart/form-data 文件上传 传 FormData 时自动设置
application/x-www-form-urlencoded 表单提交 需手动处理
javascript 复制代码
import axios from 'axios'
import qs from 'qs'

// ✅ 1. JSON 格式(默认,最常用)
axios.post('/api/users', {
  name: '张三',
  age: 25,
})
// 自动设置 Content-Type: application/json
// 请求体: {"name":"张三","age":25}

// ✅ 2. 表单格式(URL 编码)
axios.post('/api/login', qs.stringify({
  username: 'admin',
  password: '123456',
}), {
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
// 请求体: username=admin&password=123456

// ✅ 3. FormData 格式(文件上传)
const formData = new FormData()
formData.append('file', fileObj)
formData.append('name', '头像')

axios.post('/api/upload', formData)
// Axios 检测到 FormData,自动设置 Content-Type: multipart/form-data
// ⚠️ 不要手动设置 Content-Type,Axios 会自动添加 boundary

// ❌ 错误:手动设置会丢失 boundary 参数
axios.post('/api/upload', formData, {
  headers: { 'Content-Type': 'multipart/form-data' }, // 缺少 boundary
})

// ✅ 正确:让 Axios 自动处理
axios.post('/api/upload', formData)
// Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...

💡 面试加分点 :传 FormData 时不要手动设置 Content-Type,Axios 会自动添加正确的 boundary 分隔符。手动设置反而会导致 boundary 缺失,服务端无法解析。


15. Axios 的错误处理有哪些类型?

Axios 的错误分为三大类:

javascript 复制代码
import axios from 'axios'

axios.get('/api/data').catch((error) => {
  if (error.response) {
    // ✅ 类型1:服务器响应了,但状态码不在 2xx 范围
    // error.response.data    → 响应体数据
    // error.response.status  → HTTP 状态码(如 404, 500)
    // error.response.headers → 响应头
    console.log('服务器错误:', error.response.status)

  } else if (error.request) {
    // ✅ 类型2:请求已发出,但没有收到响应(网络错误、超时等)
    // error.request → XMLHttpRequest 实例(浏览器)或 http.ClientRequest 实例(Node.js)
    console.log('无响应:', error.request)

  } else {
    // ✅ 类型3:在设置请求时发生错误(代码错误)
    console.log('请求配置错误:', error.message)
  }

  // 通用属性
  console.log('错误配置:', error.config)     // 请求配置
  console.log('错误代码:', error.code)        // 如 'ECONNABORTED'
  console.log('是否取消:', axios.isCancel(error))

  // ✅ 使用 error.toJSON() 获取结构化信息
  console.log(error.toJSON())
})
javascript 复制代码
// ✅ 完善的错误处理工具函数
function getErrorMessage(error) {
  if (axios.isCancel(error)) {
    return '请求已取消'
  }
  if (error.code === 'ECONNABORTED') {
    return '请求超时'
  }
  if (error.code === 'ERR_NETWORK') {
    return '网络连接失败'
  }
  if (error.response) {
    const statusMessages = {
      400: '请求参数错误',
      401: '未授权,请重新登录',
      403: '拒绝访问',
      404: '请求地址不存在',
      408: '请求超时',
      500: '服务器内部错误',
      501: '服务未实现',
      502: '网关错误',
      503: '服务不可用',
      504: '网关超时',
    }
    return statusMessages[error.response.status]
      || error.response.data?.message
      || `请求错误 (${error.response.status})`
  }
  return error.message || '未知错误'
}

💡 面试加分点 :注意 Fetch API 与 Axios 的错误处理差异 ------ Fetch 只在网络故障 时 reject,服务器返回 404/500 等状态码时仍然 resolve(需手动检查 response.ok),而 Axios 会自动将非 2xx 状态码作为错误处理。


16. 如何在 Axios 中处理请求超时?

javascript 复制代码
import axios from 'axios'

const request = axios.create({
  baseURL: '/api',
  timeout: 10000, // 全局超时 10 秒
})

// ✅ 针对不同接口设置不同超时
const quickApi = (url, params) => request.get(url, {
  params,
  timeout: 3000, // 快速接口 3 秒
})

const slowApi = (url, data) => request.post(url, data, {
  timeout: 60000, // 慢接口 60 秒(如导出报表)
})

// ✅ 超时重试 + 提示用户
request.interceptors.response.use(null, async (error) => {
  if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
    const config = error.config

    // 超时自动重试 1 次
    if (!config._timeoutRetried) {
      config._timeoutRetried = true
      return request(config)
    }

    // 重试后仍超时,提示用户
    const shouldRetry = await confirmRetry('请求超时,是否重试?')
    if (shouldRetry) {
      config._timeoutRetried = false
      return request(config)
    }
  }
  return Promise.reject(error)
})
javascript 复制代码
// ✅ 使用 AbortController 实现自定义超时(更灵活)
async function requestWithTimeout(config, timeout = 10000) {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), timeout)

  try {
    const response = await axios({
      ...config,
      signal: controller.signal,
    })
    return response
  } finally {
    clearTimeout(timer)
  }
}

💡 面试加分点 :Axios 的 timeout 只计算从请求发出到收到响应 的时间。如果需要包含 DNS 解析、TCP 连接等时间,可以使用 AbortController + setTimeout 实现更精确的超时控制。


javascript 复制代码
import axios from 'axios'

// ✅ 允许跨域携带 Cookie
const request = axios.create({
  baseURL: 'https://api.other-domain.com',
  withCredentials: true, // 关键配置:跨域携带 Cookie
})

// 服务端也需要配置:
// Access-Control-Allow-Origin: https://your-domain.com  ← 不能是 *
// Access-Control-Allow-Credentials: true
场景 withCredentials 服务端 Allow-Origin
同域请求 不需要设置 不需要 CORS 头
跨域不携带 Cookie false(默认) 可以是 *
跨域携带 Cookie true 必须指定具体域名
javascript 复制代码
// ✅ 通过拦截器设置自定义 Cookie 字段(某些场景需要手动传递)
request.interceptors.request.use((config) => {
  // 方式1:通过 header 传递(非标准 Cookie,需后端配合)
  const sessionId = getCookie('SESSION_ID')
  if (sessionId) {
    config.headers['X-Session-Id'] = sessionId
  }

  return config
})

// ✅ 获取 Cookie 的工具函数
function getCookie(name) {
  const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`))
  return match ? decodeURIComponent(match[2]) : null
}

💡 面试加分点 :当 withCredentials: true 时,服务端 Access-Control-Allow-Origin 不能设为 *,必须指定具体域名。同时需要设置 Access-Control-Allow-Credentials: true。现代方案推荐使用 Token(如 JWT) 替代 Cookie 进行身份认证,可以避免 CSRF 问题。


18. 如何使用 Axios 实现请求队列(串行执行)?

某些场景下请求之间有依赖关系,需要按顺序执行。

javascript 复制代码
// ✅ 方案1:async/await 天然串行
async function serialRequests() {
  const user = await axios.get('/api/user/1')
  const orders = await axios.get(`/api/orders?userId=${user.data.id}`)
  const detail = await axios.get(`/api/orders/${orders.data[0].id}`)
  return { user: user.data, orders: orders.data, detail: detail.data }
}
javascript 复制代码
// ✅ 方案2:通用请求队列类
class RequestQueue {
  constructor(concurrency = 1) {
    this.concurrency = concurrency
    this.queue = []
    this.running = 0
  }

  add(requestFn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ requestFn, resolve, reject })
      this.run()
    })
  }

  async run() {
    while (this.running < this.concurrency && this.queue.length > 0) {
      const { requestFn, resolve, reject } = this.queue.shift()
      this.running++

      try {
        const result = await requestFn()
        resolve(result)
      } catch (error) {
        reject(error)
      } finally {
        this.running--
        this.run() // 继续处理队列
      }
    }
  }
}

// 使用:串行执行
const queue = new RequestQueue(1) // 并发数 = 1,即串行

queue.add(() => axios.get('/api/step1'))
queue.add(() => axios.post('/api/step2', { data: 'xxx' }))
queue.add(() => axios.get('/api/step3'))

// 使用:限制并发数为 3
const pool = new RequestQueue(3)
urls.forEach(url => pool.add(() => axios.get(url)))
javascript 复制代码
// ✅ 方案3:利用 Array.reduce 实现串行 Promise
function serialRequests(requestFns) {
  return requestFns.reduce(
    (chain, fn) => chain.then(results =>
      fn().then(result => [...results, result])
    ),
    Promise.resolve([])
  )
}

// 使用
const results = await serialRequests([
  () => axios.get('/api/a'),
  () => axios.get('/api/b'),
  () => axios.get('/api/c'),
])

💡 面试加分点:请求队列在以下场景非常有用:接口有调用频率限制(如 100次/秒)、请求之间有依赖关系、需要控制并发防止服务器压力过大。


19. Axios 与 TypeScript 如何配合使用?

typescript 复制代码
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'

// ✅ 定义通用响应结构
interface ApiResponse<T = any> {
  code: number
  message: string
  data: T
}

// ✅ 定义业务类型
interface User {
  id: number
  name: string
  email: string
  avatar: string
}

interface UserListParams {
  page: number
  size: number
  keyword?: string
}

interface PaginatedList<T> {
  list: T[]
  total: number
  page: number
  size: number
}

// ✅ 封装带类型的请求方法
class HttpClient {
  private instance: AxiosInstance

  constructor(config?: AxiosRequestConfig) {
    this.instance = axios.create(config)
    this.setupInterceptors()
  }

  private setupInterceptors() {
    this.instance.interceptors.response.use(
      (response: AxiosResponse<ApiResponse>) => {
        const { code, data, message } = response.data
        if (code === 0) return data
        return Promise.reject(new Error(message))
      },
      (error) => Promise.reject(error)
    )
  }

  // 带泛型的请求方法
  get<T>(url: string, params?: any): Promise<T> {
    return this.instance.get(url, { params })
  }

  post<T>(url: string, data?: any): Promise<T> {
    return this.instance.post(url, data)
  }

  put<T>(url: string, data?: any): Promise<T> {
    return this.instance.put(url, data)
  }

  delete<T>(url: string): Promise<T> {
    return this.instance.delete(url)
  }
}

const http = new HttpClient({
  baseURL: '/api',
  timeout: 10000,
})

// ✅ 使用:获得完整的类型推导
const user = await http.get<User>('/users/1')
// user 类型自动推导为 User
console.log(user.name) // ✅ 有智能提示

const list = await http.get<PaginatedList<User>>('/users', { page: 1, size: 10 })
// list.list 类型自动推导为 User[]
console.log(list.total) // ✅ 有智能提示
typescript 复制代码
// ✅ 扩展 AxiosRequestConfig,支持自定义配置
declare module 'axios' {
  interface AxiosRequestConfig {
    retry?: number
    retryDelay?: number
    cache?: boolean
    showLoading?: boolean
  }
}

// 现在可以在请求中使用自定义配置
axios.get('/api/data', {
  retry: 3,
  retryDelay: 1000,
  cache: true,
  showLoading: true,
})

💡 面试加分点 :通过泛型 get<T> 可以让返回值自动推导类型,提升开发体验。扩展 AxiosRequestConfig 接口可以让自定义配置(如 retry、cache)也获得类型支持。


20. Axios 源码核心流程是怎样的?

理解 Axios 源码有助于更好地使用和排查问题。

核心架构

scss 复制代码
请求发起 → 合并配置 → 请求拦截器链 → 适配器(发送请求) → 响应拦截器链 → 返回结果

关键源码解析

javascript 复制代码
// ✅ 1. 拦截器链(Promise 链)机制
class Axios {
  request(config) {
    // 合并配置
    config = mergeConfig(this.defaults, config)

    // 构建 Promise 链
    // chain = [请求拦截器2, 请求拦截器1, dispatchRequest, undefined, 响应拦截器1, 响应拦截器2]
    const chain = [dispatchRequest, undefined]

    // 请求拦截器 unshift 到链前面(所以后添加的先执行)
    this.interceptors.request.forEach((interceptor) => {
      chain.unshift(interceptor.fulfilled, interceptor.rejected)
    })

    // 响应拦截器 push 到链后面(先添加的先执行)
    this.interceptors.response.forEach((interceptor) => {
      chain.push(interceptor.fulfilled, interceptor.rejected)
    })

    // 按顺序执行 Promise 链
    let promise = Promise.resolve(config)
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift())
    }
    return promise
  }
}

// ✅ 2. dispatchRequest ------ 实际发送请求
function dispatchRequest(config) {
  // 转换请求数据
  config.data = transformData(config.data, config.headers, config.transformRequest)

  // 选择适配器(浏览器 XHR / Node.js http)
  const adapter = config.adapter || getDefaultAdapter()

  return adapter(config).then(
    (response) => {
      // 转换响应数据
      response.data = transformData(response.data, response.headers, config.transformResponse)
      return response
    },
    (reason) => {
      return Promise.reject(reason)
    }
  )
}

// ✅ 3. axios 既是函数又有方法的原因
function createInstance(defaultConfig) {
  const context = new Axios(defaultConfig)

  // 创建一个函数,绑定 Axios.prototype.request
  const instance = Axios.prototype.request.bind(context)

  // 将 Axios.prototype 上的方法(get/post/put 等)复制到 instance 上
  Object.keys(Axios.prototype).forEach(method => {
    instance[method] = Axios.prototype[method].bind(context)
  })

  // 将 context 上的属性(defaults/interceptors)复制到 instance 上
  Object.keys(context).forEach(key => {
    instance[key] = context[key]
  })

  return instance
}

// 这就是为什么 axios 可以这样使用:
// axios('/api/users')                 → 当作函数调用
// axios.get('/api/users')             → 调用方法
// axios.interceptors.request.use(...) → 访问属性

核心设计模式总结

设计模式 在 Axios 中的应用
适配器模式 浏览器 XHR / Node.js http 适配器
拦截器/责任链模式 请求/响应拦截器链
工厂模式 axios.create() 创建实例
代理/委托模式 createInstance 让 axios 既是函数又有方法

💡 面试加分点:Axios 的核心设计非常值得学习:

  1. 拦截器链 本质是 Promise 链,理解这一点就能理解拦截器的执行顺序
  2. axios 既是函数又有方法 ------ 通过 bind + 属性复制实现,这是一个巧妙的设计
  3. 适配器模式 让核心逻辑与运行环境解耦,是跨平台的关键
相关推荐
何时梦醒1 小时前
TypeScript 工具类型一篇讲透:Pick、Omit、Partial、Exclude、Record、ReturnType、keyof
前端·面试·typescript
用户2181697049301 小时前
Flutter (十九) Tabbar
前端
默_笙1 小时前
🍳 受控组件和非受控组件,我纠结了一整天,最后用"房东和租客"讲明白了
前端·javascript
烬羽1 小时前
CSS 三栏布局:`margin-left: -100%` 凭什么把侧栏「拽」回上一行?彻底搞懂负 margin
css·面试·前端框架
缓冲中请稍后1 小时前
前端HTTP请求完全指南:从基础到LLM接口调用实战
前端·面试
半个落月1 小时前
React 受控组件与非受控组件详解:从输入框到表单校验
前端·react.js
BreezeJiang1 小时前
写了 display:flex,为什么三栏布局还没完成?
前端·css
Hilaku1 小时前
高级前端如何优雅地拒绝不合理的产品需求?
前端·javascript·程序员
独立开阀者_FwtCoder1 小时前
这次更新:换新主题,同时把好计划分享出去
前端·javascript·vue.js