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 请求,也可以取消fetch、addEventListener等。一个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,用户无感知。
关键难点
- Token 过期后正在发送的请求如何处理?
- 多个请求同时 401,如何避免重复刷新?
- 刷新 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等对象类型会深度合并,对于baseURL、timeout等标量类型会直接覆盖。
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()检测运行环境,自动选择XHR或http适配器。你也可以通过自定义适配器实现 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实现更精确的超时控制。
17. Axios 如何处理 Cookie 和跨域凭证?
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 的核心设计非常值得学习:
- 拦截器链 本质是 Promise 链,理解这一点就能理解拦截器的执行顺序
- axios 既是函数又有方法 ------ 通过
bind+ 属性复制实现,这是一个巧妙的设计- 适配器模式 让核心逻辑与运行环境解耦,是跨平台的关键