【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 对比
| 特性 | Fetch | Axios |
|---|---|---|
| 内置/第三方 | 浏览器内置 | 第三方库 |
| Node.js 支持 | v18+ 支持 | 支持(任意版本) |
| 默认 JSON | ❌ 需手动 .json() | ✅ 自动解析 |
| 错误处理 | ❌ 4xx/5xx 不会 reject | ✅ 4xx/5xx 自动 reject |
| 超时控制 | 需手动实现 | ✅ timeout 参数 |
| 拦截器 | ❌ 无 | ✅ 有 |
| 请求取消 | AbortController | AbortController |
| 上传进度 | ❌ 不支持 | ✅ 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(),在响应拦截器的fulfilled和rejected中都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与跨域