在前后端分离项目中,前端最常见的工作之一就是向后端发送 HTTP 请求。

例如:

1
2
3
4
5
6
7
8
9
Vue / React
↓
Axios
↓
HTTP Request
↓
FastAPI / Spring Boot / Express
↓
Database

浏览器本身提供了 fetch,但在实际 Vue、React 项目中,Axios 依然很常见,因为它提供了比较完整的请求配置、拦截器、超时控制、请求实例、统一错误处理等能力。

本文从最基础的 Axios 请求讲到项目中常用的 Axios 二次封装。

一、Axios 基础入门

1.1 Axios 是什么

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

简单来说:

1
Axios = 帮助 JavaScript 向服务器发送 HTTP 请求的工具

例如前端请求后端用户数据:

1
2
3
4
5
import axios from 'axios'

const response = await axios.get('/api/users')

console.log(response.data)

假设后端返回:

1
2
3
4
5
6
7
8
9
10
{
"code": 200,
"message": "success",
"data": [
{
"id": 1,
"name": "Tom"
}
]
}

那么:

1
response.data

就是服务器返回的数据。

Axios 最常见的使用场景包括:

  1. GET 查询数据
  2. POST 提交数据
  3. PUT 修改数据
  4. DELETE 删除数据
  5. Token 身份认证
  6. 文件上传
  7. 文件下载
  8. 请求拦截
  9. 响应拦截
  10. 请求错误统一处理

1.2 安装 Axios

Vue、React、Vite 等现代前端项目通常直接通过 npm、pnpm 或 yarn 安装。

npm

1
npm install axios

pnpm

1
pnpm add axios

yarn

1
yarn add axios

安装完成后:

1
import axios from 'axios'

即可使用。

1.3 第一个 Axios 请求

最简单的 GET 请求:

1
2
3
4
5
6
7
8
9
import axios from 'axios'

axios.get('https://example.com/api/users')
.then(response => {
console.log(response.data)
})
.catch(error => {
console.error(error)
})

Axios 返回的是一个 Promise,因此也可以使用 async/await。

实际项目中更加推荐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import axios from 'axios'

async function getUsers() {
try {
const response = await axios.get(
'https://example.com/api/users'
)

console.log(response.data)
} catch (error) {
console.error(error)
}
}

getUsers()

相比 .then():

1
2
3
axios.get('/api/users')
.then(...)
.catch(...)

async/await 在业务逻辑比较复杂的时候通常更加容易阅读。

1.4 Axios 返回的数据是什么

Axios 请求成功之后,并不是直接返回后端的数据,而是返回一个 response 对象。

例如:

1
2
3
const response = await axios.get('/api/users')

console.log(response)

response 常见结构:

1
2
3
4
5
6
7
8
{
data: {},
status: 200,
statusText: 'OK',
headers: {},
config: {},
request: {}
}

最常使用的是:

1
response.data

例如:

1
2
3
4
5
const response = await axios.get('/api/users')

const users = response.data

console.log(users)

如果后端统一返回:

1
2
3
4
5
6
7
{
"code": 200,
"message": "success",
"data": {
"name": "Tom"
}
}

那么真正需要的业务数据可能是:

1
response.data.data

也就是:

1
const user = response.data.data

这也是为什么真实项目中通常会使用响应拦截器:

1
2
3
4
5
response
↓
response.data
↓
业务代码

把多余的一层提前处理掉。

二、HTTP 请求基础

2.1 发送 GET 请求

GET 通常用于查询数据。

最简单的方式:

1
const response = await axios.get('/api/users')

如果需要携带查询参数:

1
2
3
4
5
6
const response = await axios.get('/api/users', {
params: {
page: 1,
pageSize: 10
}
})

最终发送:

1
GET /api/users?page=1&pageSize=10

例如:

1
2
3
4
5
6
7
8
9
10
11
async function getUsers() {
const response = await axios.get('/api/users', {
params: {
keyword: 'Tom',
page: 1,
pageSize: 10
}
})

console.log(response.data)
}

最终 URL 类似:

1
/api/users?keyword=Tom&page=1&pageSize=10

因此 GET 请求参数通常放在:

1
params

里面。

2.2 发送 POST 请求

POST 一般用于创建资源或者提交数据。

例如用户登录:

1
2
3
4
const response = await axios.post('/api/login', {
username: 'admin',
password: '123456'
})

这里第二个参数:

1
2
3
4
{
username: 'admin',
password: '123456'
}

就是请求体。

等价于:

1
2
3
4
5
6
POST /api/login

{
"username": "admin",
"password": "123456"
}

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
async function login() {
try {
const response = await axios.post('/api/login', {
username: 'admin',
password: '123456'
})

console.log(response.data)
} catch (error) {
console.error(error)
}
}

2.3 PUT 请求

PUT 通常用于修改资源。

例如修改用户:

1
2
3
4
await axios.put('/api/users/1', {
name: 'Jerry',
age: 20
})

发送:

1
PUT /api/users/1

请求体:

1
2
3
4
{
"name": "Jerry",
"age": 20
}

2.4 PATCH 请求

PATCH 同样用于修改资源。

例如:

1
2
3
await axios.patch('/api/users/1', {
name: 'Jerry'
})

PUT 和 PATCH 在 RESTful API 中通常存在语义上的区别。

PUT 更倾向于:

1
整体更新资源

PATCH 更倾向于:

1
部分修改资源

例如原始用户:

1
2
3
4
5
{
"name": "Tom",
"age": 18,
"email": "tom@example.com"
}

只修改用户名:

1
2
3
await axios.patch('/api/users/1', {
name: 'Jerry'
})

2.5 DELETE 请求

删除用户:

1
await axios.delete('/api/users/1')

对应:

1
DELETE /api/users/1

如果需要传递查询参数:

1
2
3
4
5
await axios.delete('/api/users/1', {
params: {
force: true
}
})

最终:

1
DELETE /api/users/1?force=true

2.6 常见请求方式对比

方法 作用 Axios
GET 查询 axios.get()
POST 创建 axios.post()
PUT 完整更新 axios.put()
PATCH 部分更新 axios.patch()
DELETE 删除 axios.delete()

最常见的 CRUD 对应关系:

1
2
3
4
5
6
7
Create → POST

Read → GET

Update → PUT / PATCH

Delete → DELETE

2.7 Axios 通用配置方式

除了:

1
axios.get()

还可以直接传入完整配置对象:

1
2
3
4
5
6
7
const response = await axios({
method: 'get',
url: '/api/users',
params: {
page: 1
}
})

POST:

1
2
3
4
5
6
7
8
const response = await axios({
method: 'post',
url: '/api/users',
data: {
name: 'Tom',
age: 20
}
})

常见配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
axios({
method: 'post',

url: '/api/users',

baseURL: 'https://api.example.com',

params: {
page: 1
},

data: {
name: 'Tom'
},

headers: {
Authorization: 'Bearer token'
},

timeout: 5000
})

其中需要重点区分:

1
params

和:

1
data

params 一般表示 URL 查询参数:

1
/users?page=1

data 一般表示 HTTP Request Body。

2.8 设置请求头 Headers

某些接口需要指定请求头。

例如:

1
2
3
4
5
await axios.get('/api/users', {
headers: {
Authorization: 'Bearer abc123'
}
})

发送的请求头:

1
Authorization: Bearer abc123

POST:

1
2
3
4
5
6
7
8
9
10
11
await axios.post(
'/api/users',
{
name: 'Tom'
},
{
headers: {
Authorization: 'Bearer abc123'
}
}
)

注意 Axios POST 的参数顺序:

1
2
3
4
5
axios.post(
url,
data,
config
)

也就是:

1
2
3
4
5
第一个参数:URL

第二个参数:请求数据

第三个参数:请求配置

三、身份认证与拦截器

3.1 Token 身份认证

现在很多前后端分离项目都会使用 JWT。

用户登录后后端返回:

1
2
3
{
"token": "xxxxx"
}

前端保存:

1
localStorage.setItem('token', response.data.token)

请求其他接口时:

1
2
3
4
5
6
7
const token = localStorage.getItem('token')

await axios.get('/api/user/profile', {
headers: {
Authorization: `Bearer ${token}`
}
})

但是问题来了。

如果项目里面有 100 个 API:

1
2
3
4
axios.get(...)
axios.post(...)
axios.put(...)
axios.delete(...)

难道每次都手动写:

1
Authorization: `Bearer ${token}`

当然不会。这种情况交给请求拦截器处理。

3.2 请求拦截器

请求拦截器可以在 HTTP 请求真正发送之前执行代码。

流程:

1
2
3
4
5
6
7
8
9
调用 API
↓
请求拦截器
↓
添加 Token
↓
发送 HTTP 请求
↓
后端服务器

例如:

1
2
3
4
5
6
7
8
9
10
axios.interceptors.request.use(
config => {
console.log('准备发送请求')

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

注意一定要:

1
return config

如果不返回,请求就无法正常继续。

3.3 请求拦截器自动携带 Token

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('token')

if (token) {
config.headers.Authorization = `Bearer ${token}`
}

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

之后业务代码只需要:

1
axios.get('/api/users')

Axios 会自动变成:

1
2
3
GET /api/users

Authorization: Bearer xxxxx

无需每个接口重复添加 Token。

3.4 响应拦截器

响应拦截器与请求拦截器相反。

它会在:

1
服务器返回响应

之后执行。

流程:

1
2
3
4
5
6
7
后端服务器
↓
HTTP Response
↓
响应拦截器
↓
业务代码

例如:

1
2
3
4
5
6
7
8
9
10
11
12
axios.interceptors.response.use(
response => {
console.log('请求成功')

return response
},
error => {
console.error('请求失败')

return Promise.reject(error)
}
)

3.5 为什么实际项目要使用响应拦截器

假设每个接口都返回:

1
2
3
4
5
6
7
{
"code": 200,
"message": "success",
"data": {
"name": "Tom"
}
}

正常 Axios:

1
2
3
const response = await axios.get('/api/user')

console.log(response.data.data)

每次都写:

1
response.data

非常麻烦。

所以可以:

1
2
3
4
5
axios.interceptors.response.use(
response => {
return response.data
}
)

这时候:

1
2
3
const response = await axios.get('/api/user')

console.log(response)

得到的就是:

1
2
3
4
5
6
7
{
"code": 200,
"message": "success",
"data": {
"name": "Tom"
}
}

还可以继续处理:

1
2
3
4
5
axios.interceptors.response.use(
response => {
return response.data.data
}
)

这时业务代码:

1
2
3
const user = await axios.get('/api/user')

console.log(user.name)

会更加简洁。

不过真实项目中是否直接返回 data.data,要根据后端统一响应格式决定。

四、Axios 工程化封装

4.1 创建 Axios 实例

真实项目一般不建议直接修改全局 axios。

更加推荐:

1
axios.create()

创建属于当前项目的 Axios 实例。

例如:

1
2
3
4
5
6
import axios from 'axios'

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

之后:

1
request.get('/users')

真正发送:

1
http://localhost:8000/api/users

这里:

1
baseURL

可以理解为公共 API 前缀。

4.2 baseURL 的作用

假设后端地址:

1
http://localhost:8000

接口分别为:

1
2
3
4
5
6
7
http://localhost:8000/api/users

http://localhost:8000/api/login

http://localhost:8000/api/articles

http://localhost:8000/api/comments

如果不用 baseURL:

1
2
3
4
5
axios.get('http://localhost:8000/api/users')

axios.post('http://localhost:8000/api/login')

axios.get('http://localhost:8000/api/articles')

大量重复。

设置:

1
2
3
const request = axios.create({
baseURL: 'http://localhost:8000/api'
})

之后:

1
2
3
4
5
request.get('/users')

request.post('/login')

request.get('/articles')

代码会清晰很多。

4.3 timeout 请求超时

可以设置:

1
2
3
const request = axios.create({
timeout: 10000
})

表示:

1
10000 ms = 10 秒

如果请求超过指定时间仍未完成,Axios 会终止请求并抛出错误。

也可以单独设置:

1
2
3
axios.get('/api/users', {
timeout: 5000
})

表示最多等待 5 秒。

4.4 项目中常见 Axios 封装

一般项目目录会类似:

1
2
3
4
5
6
7
8
9
10
11
12
src
├── api
│ ├── user.ts
│ ├── article.ts
│ └── auth.ts
│
├── utils
│ └── request.ts
│
├── views
├── components
└── main.ts

其中:

1
request.ts

负责 Axios 配置。

1
api/

负责具体接口。

4.5 封装 request.ts

JavaScript 项目可以写:

1
2
3
4
5
6
7
8
// src/utils/request.js

import axios from 'axios'

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

添加请求拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
request.interceptors.request.use(
config => {
const token = localStorage.getItem('token')

if (token) {
config.headers.Authorization = `Bearer ${token}`
}

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

添加响应拦截器:

1
2
3
4
5
6
7
8
request.interceptors.response.use(
response => {
return response.data
},
error => {
return Promise.reject(error)
}
)

最终:

1
export default request

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import axios from 'axios'

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

request.interceptors.request.use(
config => {
const token = localStorage.getItem('token')

if (token) {
config.headers.Authorization = `Bearer ${token}`
}

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

request.interceptors.response.use(
response => {
return response.data
},
error => {
return Promise.reject(error)
}
)

export default request

这基本就是 Axios 封装的最小版本。

4.6 统一处理 HTTP 错误

Axios 请求失败后可以通过:

1
error.response

获取服务器返回的信息。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
request.interceptors.response.use(
response => {
return response.data
},

error => {
if (error.response) {
console.log(error.response.status)
console.log(error.response.data)
}

return Promise.reject(error)
}
)

HTTP 状态码常见情况:

状态码 含义
200 请求成功
201 创建成功
400 请求参数错误
401 未登录或 Token 失效
403 没有权限
404 资源不存在
500 服务器错误
502 网关错误
503 服务暂时不可用

例如统一处理 401:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
request.interceptors.response.use(
response => {
return response.data
},

error => {
if (error.response?.status === 401) {
localStorage.removeItem('token')

window.location.href = '/login'
}

return Promise.reject(error)
}
)

这样 Token 失效之后,就可以自动退出登录。

4.7 区分不同类型的错误

一个 Axios 请求失败,不一定意味着后端报错。

一般可以分为三种情况。

第一种:

1
error.response

存在。

说明服务器已经响应,但是状态码不是 Axios 认为的成功状态。

例如:

1
2
3
4
5
400
401
403
404
500

第二种:

1
error.request

存在,但是:

1
error.response

不存在。

说明请求已经发送出去,但是没有收到服务器响应。

可能的原因包括:

1
2
3
4
5
6
7
8
9
网络断开

服务器没有启动

请求超时

跨域配置问题

网关故障

第三种情况:

请求本身配置过程中就出现了问题。

因此可以写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
request.interceptors.response.use(
response => response.data,

error => {
if (error.response) {
console.error(
'服务器返回错误:',
error.response.status
)
} else if (error.request) {
console.error('服务器没有响应')
} else {
console.error(
'请求配置错误:',
error.message
)
}

return Promise.reject(error)
}
)

4.8 业务状态码处理

实际项目中经常出现:

HTTP 状态:

1
200 OK

但是业务状态:

1
2
3
4
5
{
"code": 40001,
"message": "用户名不存在",
"data": null
}

这时候不能只判断 HTTP 状态码。

可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
request.interceptors.response.use(
response => {
const result = response.data

if (result.code !== 200) {
console.error(result.message)

return Promise.reject(
new Error(result.message)
)
}

return result.data
},

error => {
return Promise.reject(error)
}
)

这样业务层只会收到真正的数据:

1
2
3
const user = await getUser()

console.log(user)

而不是:

1
2
3
4
5
{
code: 200,
message: 'success',
data: user
}

不过这部分一定要根据你自己的后端接口协议进行调整。

4.9 封装 API

有了:

1
src/utils/request.js

之后,不建议在 Vue 组件中到处写:

1
request.get('/users')

更加推荐继续封装 API。

例如:

1
src/api/user.js

代码:

1
2
3
4
5
6
7
8
9
import request from '@/utils/request'

export function getUsers(params) {
return request({
url: '/users',
method: 'get',
params
})
}

新增用户:

1
2
3
4
5
6
7
export function createUser(data) {
return request({
url: '/users',
method: 'post',
data
})
}

修改用户:

1
2
3
4
5
6
7
export function updateUser(id, data) {
return request({
url: `/users/${id}`,
method: 'put',
data
})
}

删除用户:

1
2
3
4
5
6
export function deleteUser(id) {
return request({
url: `/users/${id}`,
method: 'delete'
})
}

最终:

1
2
3
4
5
6
import {
getUsers,
createUser,
updateUser,
deleteUser
} from '@/api/user'

业务代码就不需要关注底层 HTTP 请求细节。

4.10 完整 CRUD API 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// src/api/user.js

import request from '@/utils/request'

export function getUsers(params) {
return request({
url: '/users',
method: 'get',
params
})
}

export function getUser(id) {
return request({
url: `/users/${id}`,
method: 'get'
})
}

export function createUser(data) {
return request({
url: '/users',
method: 'post',
data
})
}

export function updateUser(id, data) {
return request({
url: `/users/${id}`,
method: 'put',
data
})
}

export function deleteUser(id) {
return request({
url: `/users/${id}`,
method: 'delete'
})
}

页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
import {
getUsers,
createUser
} from '@/api/user'

async function loadUsers() {
const data = await getUsers({
page: 1,
pageSize: 10
})

console.log(data)
}

整个请求链路就变成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Vue Component

↓

user.js

↓

request.js

↓

Axios

↓

HTTP

↓

Backend

这是实际项目中比较常见的 Axios 分层方式。

4.11 Axios + Vue 3 示例

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<script setup>
import { ref, onMounted } from 'vue'
import { getUsers } from '@/api/user'

const users = ref([])
const loading = ref(false)

async function loadUsers() {
loading.value = true

try {
users.value = await getUsers({
page: 1,
pageSize: 10
})
} catch (error) {
console.error('获取用户失败', error)
} finally {
loading.value = false
}
}

onMounted(() => {
loadUsers()
})
</script>

<template>
<div>
<div v-if="loading">
加载中...
</div>

<div
v-for="user in users"
:key="user.id"
>
{{ user.name }}
</div>
</div>
</template>

这是一个典型的 loading 处理流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
loading = true

↓

发送 Axios 请求

↓

成功 / 失败

↓

finally

↓

loading = false

五、Axios 与 TypeScript

5.1 Axios + TypeScript

Axios 本身已经包含 TypeScript 类型定义,因此 TS 项目可以直接使用。

假设后端返回:

1
2
3
4
5
{
"id": 1,
"name": "Tom",
"email": "tom@example.com"
}

定义:

1
2
3
4
5
interface User {
id: number
name: string
email: string
}

请求:

1
2
3
4
5
6
7
import axios from 'axios'

const response = await axios.get<User>(
'/api/users/1'
)

console.log(response.data.name)

这时:

1
response.data

会被 TypeScript 推断为:

1
User

从而获得代码提示。

5.2 统一后端响应类型

假设后端所有接口统一:

1
2
3
4
5
{
"code": 200,
"message": "success",
"data": {}
}

可以定义:

1
2
3
4
5
interface ApiResponse<T> {
code: number
message: string
data: T
}

用户:

1
2
3
4
interface User {
id: number
name: string
}

请求:

1
2
3
const response = await axios.get<
ApiResponse<User>
>('/api/users/1')

此时:

1
response.data.data

类型就是:

1
User

这是 Axios + TypeScript 项目中常见的写法。

5.3 环境变量配置 baseURL

真实项目通常不会直接:

1
baseURL: 'http://localhost:8000'

因为开发环境可能是:

1
http://localhost:8000

测试环境可能是:

1
https://test-api.example.com

生产环境可能是:

1
https://api.example.com

Vite 项目可以使用环境变量。

例如:

1
.env.development

写:

1
VITE_API_BASE_URL=http://localhost:8000/api

生产:

1
.env.production

写:

1
VITE_API_BASE_URL=https://api.example.com/api

Axios:

1
2
3
4
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})

这样:

1
2
3
开发环境 → 本地 API

生产环境 → 正式 API

不用修改代码。

六、文件、请求控制与高级功能

6.1 文件上传

文件上传通常使用:

1
FormData

例如:

1
2
3
const formData = new FormData()

formData.append('file', file)

然后:

1
2
3
4
await axios.post(
'/api/upload',
formData
)

例如 Vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function uploadFile(event) {
const file = event.target.files[0]

const formData = new FormData()

formData.append('file', file)

const response = await axios.post(
'/api/upload',
formData
)

console.log(response.data)
}

HTML:

1
2
3
4
<input
type="file"
@change="uploadFile"
/>

如果后端还要求其他参数:

1
2
3
4
5
formData.append('file', file)

formData.append('userId', '1001')

formData.append('description', '头像')

都可以一起传递。

6.2 上传进度

Axios 支持监听上传进度。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
await axios.post(
'/api/upload',
formData,
{
onUploadProgress(event) {
if (!event.total) {
return
}

const percent = Math.round(
event.loaded * 100 / event.total
)

console.log(`上传进度:${percent}%`)
}
}
)

可以用于实现:

1
2
3
4
5
6
7
上传进度:10%

上传进度:30%

上传进度:72%

上传进度:100%

然后配合进度条组件展示上传进度。

6.3 文件下载

例如后端提供:

1
GET /api/files/report

返回 Excel、PDF、ZIP 等文件。

Axios:

1
2
3
4
5
6
const response = await axios.get(
'/api/files/report',
{
responseType: 'blob'
}
)

这里:

1
responseType: 'blob'

不能省略。

然后:

1
2
3
4
5
6
7
8
9
10
11
12
13
const blob = new Blob([response.data])

const url = URL.createObjectURL(blob)

const a = document.createElement('a')

a.href = url

a.download = 'report.xlsx'

a.click()

URL.revokeObjectURL(url)

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async function downloadFile() {
const response = await axios.get(
'/api/files/report',
{
responseType: 'blob'
}
)

const blob = new Blob([
response.data
])

const url = URL.createObjectURL(blob)

const link = document.createElement('a')

link.href = url
link.download = 'report.xlsx'

document.body.appendChild(link)

link.click()

document.body.removeChild(link)

URL.revokeObjectURL(url)
}

6.4 取消 Axios 请求

某些请求可能非常慢。

例如:

1
2
3
4
5
6
7
AI 对话

搜索建议

文件处理

复杂统计查询

用户切换页面以后,旧请求可能已经不需要了。

现代 Axios 可以配合:

1
AbortController

取消请求。

例如:

1
2
3
4
5
const controller = new AbortController()

axios.get('/api/users', {
signal: controller.signal
})

取消:

1
controller.abort()

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const controller = new AbortController()

async function loadData() {
try {
const response = await axios.get(
'/api/data',
{
signal: controller.signal
}
)

console.log(response.data)
} catch (error) {
if (axios.isCancel(error)) {
console.log('请求已取消')
return
}

console.error(error)
}
}

loadData()

需要取消:

1
controller.abort()

6.5 搜索场景为什么需要取消请求

假设用户连续输入:

1
2
3
4
5
6
7
a

ax

axi

axios

程序可能连续发送:

1
2
3
4
5
6
7
/api/search?keyword=a

/api/search?keyword=ax

/api/search?keyword=axi

/api/search?keyword=axios

如果:

1
axios

请求先返回,

然后:

1
a

这个旧请求才返回,

页面可能反而显示旧搜索结果。

因此可以在新请求开始前取消旧请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let controller

async function search(keyword) {
if (controller) {
controller.abort()
}

controller = new AbortController()

const response = await axios.get(
'/api/search',
{
params: {
keyword
},

signal: controller.signal
}
)

return response.data
}

搜索框、自动补全等功能常用这种做法。

6.6 并发请求

有时一个页面同时需要多个接口:

1
2
3
4
5
用户信息

消息列表

通知数量

可以:

1
2
3
4
5
6
7
8
9
const [
userResponse,
messagesResponse,
notificationsResponse
] = await Promise.all([
axios.get('/api/user'),
axios.get('/api/messages'),
axios.get('/api/notifications')
])

相比:

1
2
3
4
5
await axios.get('/api/user')

await axios.get('/api/messages')

await axios.get('/api/notifications')

后者会一个接一个执行:

1
2
3
4
5
6
7
请求1完成
↓
请求2开始
↓
请求2完成
↓
请求3开始

而:

1
Promise.all()

可以让多个请求并发执行:

1
2
3
4
5
请求1 ─────→

请求2 ─────→

请求3 ─────→

如果接口之间没有依赖关系,这通常更加高效。

6.7 请求之间存在依赖怎么办

例如:

先登录:

1
2
3
4
5
6
7
const loginResult = await axios.post(
'/api/login',
{
username: 'admin',
password: '123456'
}
)

然后取得 Token:

1
const token = loginResult.data.token

再查询用户:

1
2
3
4
5
6
7
8
const user = await axios.get(
'/api/user',
{
headers: {
Authorization: `Bearer ${token}`
}
}
)

这两个接口存在依赖,所以不能使用:

1
Promise.all()

必须按照顺序执行。

如果后端使用 Cookie、Session 等方式认证,跨域场景可能需要:

1
withCredentials: true

例如:

1
2
3
4
const request = axios.create({
baseURL: 'http://localhost:8000',
withCredentials: true
})

或者:

1
2
3
axios.get('/api/user', {
withCredentials: true
})

不过这里需要后端的 CORS 同时正确配置,否则浏览器仍然会阻止请求。

七、开发环境与项目配置

7.1 Axios 和跨域 CORS

很多刚学 Axios 的开发者都会遇到:

1
CORS policy

例如:

1
Access to XMLHttpRequest has been blocked by CORS policy

这通常不是 Axios 本身的问题。

浏览器发现:

1
2
3
前端:

http://localhost:5173

请求:

1
2
3
后端:

http://localhost:8000

端口不同,因此属于不同 Origin。

跨域问题通常需要:

1
后端配置 CORS

或者:

1
开发服务器配置代理

解决。

单纯修改 Axios 代码:

1
axios.get(...)

通常无法直接绕过浏览器 CORS 限制。

7.2 Vite 开发代理

例如 Vue + Vite 项目。

vite.config.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {
defineConfig
} from 'vite'

import vue from '@vitejs/plugin-vue'

export default defineConfig({
plugins: [
vue()
],

server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
})

Axios:

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

然后:

1
request.get('/users')

浏览器访问:

1
http://localhost:5173/api/users

Vite 代理到:

1
http://localhost:8000/api/users

开发阶段常用这种方式。

7.3 Axios 默认配置

也可以直接修改 axios 全局配置:

1
2
3
4
axios.defaults.baseURL =
'https://api.example.com'

axios.defaults.timeout = 10000

例如:

1
2
3
axios.defaults.headers.common[
'Authorization'
] = 'Bearer token'

不过大型项目更加推荐:

1
axios.create()

因为全局配置会影响所有 Axios 请求,而实例配置更加容易隔离。

7.4 多个后端怎么办

假设项目需要:

1
2
3
4
5
业务 API

AI API

文件服务 API

可以创建多个 Axios 实例。

业务:

1
2
3
export const apiRequest = axios.create({
baseURL: '/api'
})

AI:

1
2
3
export const aiRequest = axios.create({
baseURL: '/ai'
})

文件:

1
2
3
export const fileRequest = axios.create({
baseURL: '/file'
})

分别设置自己的:

1
2
3
4
5
6
7
baseURL

timeout

headers

interceptors

这种方式比修改全局 Axios 更适合复杂项目。

7.5 AI 接口为什么可能需要更长 timeout

普通 CRUD API:

1
timeout: 10000

可能足够。

但是:

1
2
3
4
5
6
7
LLM 推理

RAG 检索

图片生成

复杂数据分析

可能需要几十秒甚至更久。

例如:

1
2
3
4
const aiRequest = axios.create({
baseURL: '/api/ai',
timeout: 120000
})

即:

1
120000ms = 120 秒

因此 timeout 不应该无脑整个项目统一设置,而应该根据 API 类型决定。

7.6 Axios 错误类型判断

TypeScript 中尤其推荐:

1
2
3
4
5
6
7
8
9
10
11
import axios from 'axios'

try {
await axios.get('/api/users')
} catch (error) {
if (axios.isAxiosError(error)) {
console.log(
error.response?.status
)
}
}

相比:

1
catch (error: any)

这种写法类型更加安全。

7.7 一个更加完整的 request.ts

把前面的内容合起来,可以写一个更接近真实项目的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import axios from 'axios'

const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})

request.interceptors.request.use(
config => {
const token =
localStorage.getItem('token')

if (token) {
config.headers.Authorization =
`Bearer ${token}`
}

return config
},

error => {
return Promise.reject(error)
}
)

request.interceptors.response.use(
response => {
const result = response.data

if (
result.code !== undefined &&
result.code !== 200
) {
return Promise.reject(
new Error(
result.message ||
'请求失败'
)
)
}

return result
},

error => {
if (!axios.isAxiosError(error)) {
return Promise.reject(error)
}

const status =
error.response?.status

switch (status) {
case 400:
console.error('请求参数错误')
break

case 401:
console.error(
'登录状态已失效'
)

localStorage.removeItem(
'token'
)

break

case 403:
console.error('没有访问权限')
break

case 404:
console.error('接口不存在')
break

case 500:
console.error(
'服务器内部错误'
)

break

default:
console.error(
error.message ||
'网络请求失败'
)
}

return Promise.reject(error)
}
)

export default request

这已经可以覆盖大部分普通前端项目。

7.8 项目推荐目录结构

一个 Vue 项目可以这样组织:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
src
│
├── api
│ ├── auth.ts
│ ├── user.ts
│ ├── article.ts
│ └── file.ts
│
├── utils
│ └── request.ts
│
├── stores
│
├── router
│
├── views
│
├── components
│
└── main.ts

职责:

1
api/

负责定义:

1
有哪些接口

例如:

1
2
3
4
5
getUsers()

createUser()

deleteUser()

而:

1
utils/request.ts

负责:

1
2
3
4
5
6
7
8
9
10
11
12
13
Axios 实例

baseURL

timeout

Token

请求拦截器

响应拦截器

统一错误处理

页面只负责:

1
2
3
调用 API

展示数据

八、最佳实践与总结

8.1 不要直接在页面写大量 Axios 请求

不推荐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup>
import axios from 'axios'

async function getUsers() {
const token =
localStorage.getItem('token')

const response =
await axios.get(
'http://localhost:8000/api/users',
{
headers: {
Authorization:
`Bearer ${token}`
}
}
)

// ...
}
</script>

如果几十个页面都这么写:

1
2
3
4
5
6
7
8
9
URL 重复

Token 重复

错误处理重复

baseURL 重复

接口难维护

推荐:

1
const users = await getUsers()

然后把:

1
HTTP 请求细节

交给:

1
2
3
api/

request.ts

处理。

8.2 不要在每个 API 里面重复 try/catch

例如:

1
2
3
4
5
6
7
8
export async function getUsers() {
try {
return await request.get('/users')
} catch (error) {
console.error(error)
throw error
}
}

如果只是:

1
2
3
4
5
捕获错误

打印错误

再次抛出

那么意义并不大。

因为统一错误处理已经可以放到:

1
response interceptor

里面。

接口层可以直接:

1
2
3
export function getUsers() {
return request.get('/users')
}

页面真正需要针对业务做特殊处理的时候再:

1
2
3
4
5
try {
await getUsers()
} catch (error) {
// 当前页面专属逻辑
}

8.3 不要把所有 HTTP 200 都当作业务成功

这是常见的误区。

HTTP:

1
200

只能表示:

1
HTTP 请求成功完成

并不一定代表:

1
业务执行成功

例如:

1
2
3
4
5
6
7
HTTP 200

{
"code": 10003,
"message": "余额不足",
"data": null
}

因此真实项目通常存在两层判断:

1
2
3
4
5
HTTP 状态码

+

业务状态码

需要根据公司的接口规范统一处理。

8.4 Axios 与 Fetch 的区别

很多人会问:

1
2
3
既然浏览器已经有 fetch,

为什么还要 Axios?

两者都可以完成 HTTP 请求。

Fetch:

1
2
3
4
5
6
7
8
9
10
11
12
const response = await fetch(
'/api/users'
)

if (!response.ok) {
throw new Error(
`HTTP Error: ${response.status}`
)
}

const data =
await response.json()

Axios:

1
2
3
4
5
const response =
await axios.get('/api/users')

const data =
response.data

Axios 比较吸引人的地方通常包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
请求/响应拦截器

统一 baseURL

timeout 配置

自动 JSON 处理

统一实例

上传下载进度

同样支持 AbortController 取消(signal 写法与 Fetch 一致)

方便的请求配置

而 Fetch 的优势是:

1
2
3
4
5
浏览器原生

不需要额外依赖

Web 标准 API

因此并不存在:

1
Axios 一定比 Fetch 好

或者:

1
Fetch 一定应该替代 Axios

更加合理的理解应该是:

1
2
3
Fetch 更接近浏览器原生底层 API

Axios 更接近开箱即用的 HTTP 客户端

8.5 Axios 常见面试题

Axios 是什么?

Axios 是一个基于 Promise 的 HTTP 客户端,可以在浏览器和 Node.js 环境中使用,常用于发送 HTTP 请求。

Axios 请求拦截器有什么作用?

可以在请求发送之前统一修改请求配置,例如:

1
2
3
4
5
6
7
添加 Token

添加公共 Header

添加 Trace ID

统一参数处理

Axios 响应拦截器有什么作用?

可以统一处理服务器响应,例如:

1
2
3
4
5
6
7
提取 data

处理业务状态码

处理 401

统一错误提示

params 和 data 有什么区别?

一般:

1
params

用于 URL Query:

1
/users?page=1

而:

1
data

用于 Request Body:

1
2
3
{
"name": "Tom"
}

Axios 如何取消请求?

使用:

1
AbortController

例如:

1
2
3
4
5
6
7
8
const controller =
new AbortController()

axios.get('/api/users', {
signal: controller.signal
})

controller.abort()

Axios 如何统一添加 Token?

通常使用请求拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
request.interceptors.request.use(
config => {
const token =
localStorage.getItem('token')

if (token) {
config.headers.Authorization =
`Bearer ${token}`
}

return config
}
)

Axios 如何处理 Token 失效?

通常通过响应拦截器监听:

1
401 Unauthorized

然后:

1
2
3
4
5
删除本地 Token

清除用户状态

跳转登录页

8.6 Axios 学习重点

如果只是为了正常进行 Vue 或 React 项目开发,不需要把 Axios 每个配置都背下来。

首先需要熟练掌握:

1
2
3
4
5
6
7
axios.get()

axios.post()

axios.put()

axios.delete()

然后掌握:

1
2
3
4
5
6
7
8
9
params

data

headers

timeout

baseURL

再掌握工程化核心:

1
2
3
4
5
6
7
8
9
10
11
axios.create()

请求拦截器

响应拦截器

Token

统一错误处理

API 模块封装

最后再学习:

1
2
3
4
5
6
7
8
9
10
11
文件上传

文件下载

AbortController

并发请求

TypeScript 泛型

多 Axios 实例

做到这里,普通前端项目中的 Axios 基本就够用了。

8.7 一个完整请求到底经历了什么

最后,把 Axios 整个工作流程串起来。

用户打开页面:

1
Vue Component

调用:

1
getUsers()

进入:

1
src/api/user.ts

执行:

1
request.get('/users')

进入 Axios:

1
request.ts

请求拦截器执行:

1
2
3
4
5
读取 Token

↓

添加 Authorization

然后:

1
HTTP Request

发送到:

1
Backend API

后端查询数据库:

1
Database

返回:

1
HTTP Response

Axios 响应拦截器执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
判断 HTTP 状态

↓

判断业务状态

↓

处理 401 / 403 / 500

↓

提取数据

最后:

1
Vue Component

得到真正需要的数据。

整个过程可以表示为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Vue 页面

↓

API 模块

↓

Axios Instance

↓

Request Interceptor

↓

HTTP Request

↓

Backend

↓

Database

↓

HTTP Response

↓

Response Interceptor

↓

API 模块

↓

Vue 页面

8.8 总结

Axios 最基础的用法很简单:

1
2
const response =
await axios.get('/api/users')

项目开发中更要紧的是把 Axios 组织成一套可维护的请求体系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
axios.create()

↓

统一 baseURL

↓

统一 timeout

↓

请求拦截器

↓

自动携带 Token

↓

发送 HTTP 请求

↓

响应拦截器

↓

统一业务状态判断

↓

统一错误处理

↓

API 模块

↓

Vue / React 页面

因此学习 Axios 时,建议重点掌握:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Axios 实例

拦截器

Token

错误处理

API 模块化

TypeScript 类型

文件上传下载

请求取消

这些是进入企业项目以后使用频率最高的部分。