JavaScript
JavaScript 是一种基于脚本的编程语言,主要用于在 Web 页面上实现交互式的效果和动态的内容。JavaScript 是一种解释性语言,不需要编译就可以直接在浏览器中运行。
JavaScript 的主要特点包括:
轻量级:JavaScript 代码通常比较短小,可以快速加载和执行。
可移植性:JavaScript 可以在各种不同的浏览器和操作系统上运行。
面向对象编程:JavaScript 支持面向对象编程,包括对象、继承、封装等特性,可以用于构建复杂的软件系统。
客户端脚本语言:JavaScript 主要用于在 Web 页面上实现交互式的效果和动态的内容,可以与 HTML 和 CSS 一起使用。
异步编程:JavaScript 支持异步编程,可以利用回调函数、Promise、async/await 等方式实现异步操作,提高程序的性能和响应能力。
JavaScript 在 Web 开发中扮演着非常重要的角色,它可以用于实现各种交互式的效果和动态的内容,如表单验证、动画效果、AJAX 等。同时,JavaScript 也可以用于开发各种类型的应用程序,如桌面应用程序、移动应用程序等。
如果您想成为一名 Web 开发人员,JavaScript 是必不可少的编程语言之一,需要掌握 JavaScript 的基本语法和常用的开发框架和库,如 React、Angular、Vue 等。掌握 JavaScript 可以帮助您更加高效和灵活地实现 Web 开发中的各种功能和效果,为自己的职业发展和个人成长打下坚实的基础。

查看更多相关内容
axios 中如何实现并发请求和取消请求?请提供代码示例## Axios 并发请求
Axios 提供了 `axios.all()` 和 `axios.spread()` 方法来处理并发请求,同时也支持使用原生的 `Promise.all()`。
### 1. 使用 Promise.all()(推荐)
```javascript
// 同时发送多个请求
async function fetchMultipleData() {
try {
const [users, posts, comments] = await Promise.all([
axios.get('/api/users'),
axios.get('/api/posts'),
axios.get('/api/comments')
]);
console.log('Users:', users.data);
console.log('Posts:', posts.data);
console.log('Comments:', comments.data);
return {
users: users.data,
posts: posts.data,
comments: comments.data
};
} catch (error) {
console.error('至少一个请求失败:', error);
throw error;
}
}
```
### 2. 使用 axios.all()(传统方式)
```javascript
axios.all([
axios.get('/api/users'),
axios.get('/api/posts'),
axios.get('/api/comments')
])
.then(axios.spread((users, posts, comments) => {
// 所有请求都成功时执行
console.log('Users:', users.data);
console.log('Posts:', posts.data);
console.log('Comments:', comments.data);
}))
.catch(error => {
// 任一请求失败时执行
console.error('请求失败:', error);
});
```
### 3. 并发请求的错误处理
```javascript
async function fetchWithErrorHandling() {
const requests = [
axios.get('/api/users'),
axios.get('/api/posts'),
axios.get('/api/comments') // 可能失败的请求
];
// 使用 Promise.allSettled 等待所有请求完成
const results = await Promise.allSettled(requests);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`请求 ${index} 成功:`, result.value.data);
} else {
console.error(`请求 ${index} 失败:`, result.reason.message);
}
});
// 过滤出成功的结果
const successfulResults = results
.filter(result => result.status === 'fulfilled')
.map(result => result.value.data);
return successfulResults;
}
```
### 4. 限制并发数量
```javascript
// 使用 p-limit 或自定义实现限制并发
async function fetchWithConcurrencyLimit(urls, limit = 3) {
const results = [];
const executing = [];
for (const [index, url] of urls.entries()) {
const promise = axios.get(url).then(res => ({
index,
data: res.data
}));
results.push(promise);
if (urls.length >= limit) {
executing.push(promise);
if (executing.length >= limit) {
await Promise.race(executing);
executing.splice(
executing.findIndex(p => p === promise),
1
);
}
}
}
return Promise.all(results);
}
// 使用
const urls = ['/api/data/1', '/api/data/2', '/api/data/3', '/api/data/4'];
fetchWithConcurrencyLimit(urls, 2);
```
## Axios 取消请求
### 1. 使用 AbortController(推荐,v0.22.0+)
```javascript
// 创建 AbortController
const controller = new AbortController();
// 发送请求时传入 signal
axios.get('/api/data', {
signal: controller.signal
})
.then(response => {
console.log(response.data);
})
.catch(error => {
if (axios.isCancel(error)) {
console.log('请求已取消:', error.message);
} else {
console.error('请求失败:', error);
}
});
// 取消请求
controller.abort('用户取消操作');
// 5秒后自动取消
setTimeout(() => {
controller.abort('请求超时');
}, 5000);
```
### 2. 在 React 组件中使用
```javascript
import { useEffect } from 'react';
function UserList() {
useEffect(() => {
const controller = new AbortController();
const fetchUsers = async () => {
try {
const response = await axios.get('/api/users', {
signal: controller.signal
});
// 处理数据
} catch (error) {
if (axios.isCancel(error)) {
console.log('组件卸载,请求已取消');
} else {
console.error('获取用户失败:', error);
}
}
};
fetchUsers();
// 组件卸载时取消请求
return () => {
controller.abort('组件卸载');
};
}, []);
return <div>User List</div>;
}
```
### 3. 在 Vue 组件中使用
```javascript
<script setup>
import { onMounted, onUnmounted } from 'vue';
let controller;
onMounted(() => {
controller = new AbortController();
axios.get('/api/data', {
signal: controller.signal
})
.then(response => {
console.log(response.data);
})
.catch(error => {
if (axios.isCancel(error)) {
console.log('请求已取消');
}
});
});
onUnmounted(() => {
controller?.abort('组件卸载');
});
</script>
```
### 4. 取消多个请求
```javascript
const controllers = new Map();
// 发送请求时保存 controller
function fetchWithCancel(key, url) {
// 取消之前的同名请求
if (controllers.has(key)) {
controllers.get(key).abort('重复请求,取消前一个');
}
const controller = new AbortController();
controllers.set(key, controller);
return axios.get(url, {
signal: controller.signal
})
.finally(() => {
controllers.delete(key);
});
}
// 使用:搜索框防抖场景
fetchWithCancel('search', '/api/search?q=keyword');
```
### 5. 请求超时自动取消
```javascript
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort(`请求超时 (${timeout}ms)`);
}, timeout);
try {
const response = await axios.get(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
return response.data;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
```
### 6. 取消请求的工具函数封装
```javascript
class RequestManager {
constructor() {
this.controllers = new Map();
}
// 发送请求
async request(key, config) {
// 取消之前的同名请求
this.cancel(key);
const controller = new AbortController();
this.controllers.set(key, controller);
try {
const response = await axios({
...config,
signal: controller.signal
});
return response;
} finally {
this.controllers.delete(key);
}
}
// 取消指定请求
cancel(key, message = '请求被取消') {
if (this.controllers.has(key)) {
this.controllers.get(key).abort(message);
this.controllers.delete(key);
}
}
// 取消所有请求
cancelAll(message = '所有请求被取消') {
this.controllers.forEach(controller => {
controller.abort(message);
});
this.controllers.clear();
}
}
// 使用
const requestManager = new RequestManager();
// 发送请求
requestManager.request('userList', {
method: 'GET',
url: '/api/users'
});
// 取消指定请求
requestManager.cancel('userList');
// 取消所有请求(如页面切换时)
requestManager.cancelAll();
```
## 最佳实践
1. **组件卸载时取消请求**:避免内存泄漏和状态更新错误
2. **重复请求时取消前一个**:搜索框、表单提交等场景
3. **设置合理的超时时间**:防止请求挂起
4. **正确处理取消错误**:区分取消错误和业务错误
5. **使用 AbortController**:现代浏览器标准 API,兼容性好
服务端 · 3月7日 12:10
如何在 axios 中实现请求和响应拦截器?请举例说明实际应用场景## Axios 拦截器概述
Axios 拦截器允许你在请求发送前或响应接收后统一处理数据,是实现全局配置、错误处理、权限验证等功能的重要机制。
## 请求拦截器(Request Interceptors)
### 基本用法
```javascript
// 添加请求拦截器
axios.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
return config;
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error);
}
);
```
### 实际应用场景
#### 1. 统一添加认证 Token
```javascript
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => {
return Promise.reject(error);
}
);
```
#### 2. 添加时间戳防止缓存
```javascript
axios.interceptors.request.use(
config => {
if (config.method === 'get') {
config.params = {
...config.params,
_t: Date.now()
};
}
return config;
}
);
```
#### 3. 显示加载状态
```javascript
let requestCount = 0;
axios.interceptors.request.use(
config => {
requestCount++;
if (requestCount === 1) {
// 显示全局 loading
showLoading();
}
return config;
}
);
```
## 响应拦截器(Response Interceptors)
### 基本用法
```javascript
// 添加响应拦截器
axios.interceptors.response.use(
function (response) {
// 对响应数据做点什么
return response;
},
function (error) {
// 对响应错误做点什么
return Promise.reject(error);
}
);
```
### 实际应用场景
#### 1. 统一错误处理
```javascript
axios.interceptors.response.use(
response => response,
error => {
const { response } = error;
if (response) {
switch (response.status) {
case 401:
// 未授权,清除 token 并跳转到登录页
localStorage.removeItem('token');
window.location.href = '/login';
break;
case 403:
message.error('没有权限访问该资源');
break;
case 404:
message.error('请求的资源不存在');
break;
case 500:
message.error('服务器内部错误');
break;
default:
message.error(response.data.message || '请求失败');
}
} else {
message.error('网络错误,请检查网络连接');
}
return Promise.reject(error);
}
);
```
#### 2. 响应数据格式化
```javascript
axios.interceptors.response.use(
response => {
// 假设后端统一返回格式:{ code: 0, data: {}, message: '' }
const res = response.data;
if (res.code !== 0) {
message.error(res.message);
return Promise.reject(new Error(res.message));
}
return res.data; // 直接返回 data,简化组件中的调用
}
);
```
#### 3. 隐藏加载状态
```javascript
axios.interceptors.response.use(
response => {
requestCount--;
if (requestCount === 0) {
hideLoading();
}
return response;
},
error => {
requestCount--;
if (requestCount === 0) {
hideLoading();
}
return Promise.reject(error);
}
);
```
## 移除拦截器
```javascript
const myInterceptor = axios.interceptors.request.use(() => {});
axios.interceptors.request.eject(myInterceptor);
```
## 为实例添加拦截器
```javascript
const instance = axios.create({
baseURL: 'https://api.example.com'
});
instance.interceptors.request.use(config => {
// 只对当前实例生效
return config;
});
```
## 多个拦截器的执行顺序
```javascript
axios.interceptors.request.use(config => {
console.log('请求拦截器 1');
return config;
});
axios.interceptors.request.use(config => {
console.log('请求拦截器 2');
return config;
});
// 执行顺序:请求拦截器 2 → 请求拦截器 1 → 发送请求
// 响应拦截器执行顺序与添加顺序相反
```
## 最佳实践
1. **错误处理要完整**:请求和响应拦截器都要处理错误情况
2. **记得返回 config/response**:否则请求不会继续
3. **使用实例隔离**:不同服务使用不同实例,避免相互影响
4. **避免副作用**:拦截器中不要修改原始参数对象
5. **添加请求标识**:方便调试和追踪
服务端 · 3月7日 12:10
axios 中如何进行错误处理?请详细说明错误类型和处理策略## Axios 错误类型
Axios 请求可能产生的错误主要分为以下几类:
### 1. 请求配置错误
- URL 格式错误
- 请求方法错误
- 配置参数错误
### 2. 网络错误
- 无网络连接
- 请求超时
- DNS 解析失败
- CORS 跨域错误
### 3. HTTP 错误状态码
- 4xx 客户端错误(400, 401, 403, 404 等)
- 5xx 服务器错误(500, 502, 503 等)
### 4. 请求取消错误
- 主动取消请求
- 组件卸载时取消
## 错误对象结构
当请求失败时,Axios 会返回一个包含以下属性的错误对象:
```javascript
try {
await axios.get('/api/data');
} catch (error) {
console.log(error.message); // 错误信息
console.log(error.response); // 服务器响应(如果有)
console.log(error.request); // 请求对象
console.log(error.config); // 请求配置
console.log(error.code); // 错误代码
}
```
## 错误处理策略
### 1. 基础错误处理
```javascript
axios.get('/api/data')
.then(response => {
console.log(response.data);
})
.catch(error => {
if (error.response) {
// 服务器返回了错误状态码
console.log('Status:', error.response.status);
console.log('Data:', error.response.data);
} else if (error.request) {
// 请求已发送但没有收到响应
console.log('No response received');
} else {
// 请求配置出错
console.log('Error:', error.message);
}
});
```
### 2. 使用 async/await 处理错误
```javascript
async function fetchData() {
try {
const response = await axios.get('/api/data');
return response.data;
} catch (error) {
handleAxiosError(error);
}
}
function handleAxiosError(error) {
if (error.response) {
// 服务器响应错误
const { status, data } = error.response;
switch (status) {
case 400:
throw new Error(`请求参数错误: ${data.message}`);
case 401:
// 清除登录状态并跳转
logout();
throw new Error('登录已过期,请重新登录');
case 403:
throw new Error('没有权限执行此操作');
case 404:
throw new Error('请求的资源不存在');
case 422:
throw new Error(`数据验证失败: ${data.message}`);
case 500:
throw new Error('服务器内部错误,请稍后重试');
case 502:
throw new Error('网关错误');
case 503:
throw new Error('服务暂时不可用');
default:
throw new Error(data.message || '请求失败');
}
} else if (error.request) {
// 网络错误
if (error.code === 'ECONNABORTED') {
throw new Error('请求超时,请检查网络连接');
}
if (error.code === 'ERR_NETWORK') {
throw new Error('网络错误,请检查网络连接');
}
throw new Error('无法连接到服务器');
} else {
// 其他错误
throw new Error(error.message);
}
}
```
### 3. 全局错误处理(通过拦截器)
```javascript
// 创建 axios 实例
const instance = axios.create({
timeout: 10000
});
// 响应拦截器统一处理错误
instance.interceptors.response.use(
response => response,
error => {
// 统一错误处理
const errorMessage = getErrorMessage(error);
// 显示错误提示
message.error(errorMessage);
// 记录错误日志
logError(error);
return Promise.reject(error);
}
);
function getErrorMessage(error) {
if (error.response) {
const { status, data } = error.response;
const messageMap = {
400: '请求参数错误',
401: '登录已过期',
403: '没有权限',
404: '资源不存在',
500: '服务器错误',
502: '网关错误',
503: '服务不可用'
};
return data.message || messageMap[status] || '请求失败';
}
if (error.request) {
return '网络连接失败,请检查网络';
}
return error.message || '未知错误';
}
```
### 4. 超时错误处理
```javascript
const instance = axios.create({
timeout: 5000, // 5秒超时
timeoutErrorMessage: '请求超时,请稍后重试'
});
// 或者自定义超时处理
instance.interceptors.response.use(
response => response,
error => {
if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
// 超时错误特殊处理
return Promise.reject(new Error('请求响应时间过长,请稍后重试'));
}
return Promise.reject(error);
}
);
```
### 5. 重试机制
```javascript
axios.interceptors.response.use(null, async (error) => {
const { config } = error;
// 设置重试次数
if (!config || !config.retry) {
return Promise.reject(error);
}
config.retryCount = config.retryCount || 0;
if (config.retryCount >= config.retry) {
return Promise.reject(error);
}
config.retryCount += 1;
// 延迟重试
const delayRetry = new Promise(resolve => {
setTimeout(resolve, config.retryDelay || 1000);
});
await delayRetry;
return axios(config);
});
// 使用
axios.get('/api/data', {
retry: 3,
retryDelay: 2000
});
```
### 6. 请求取消错误处理
```javascript
const controller = new AbortController();
try {
const response = await axios.get('/api/data', {
signal: controller.signal
});
} catch (error) {
if (axios.isCancel(error)) {
console.log('请求被取消:', error.message);
} else {
// 处理其他错误
console.error('请求失败:', error);
}
}
// 取消请求
controller.abort('用户取消操作');
```
## 最佳实践
1. **分层处理**:全局拦截器 + 业务层处理
2. **用户友好**:错误信息要清晰易懂
3. **错误分类**:区分可恢复和不可恢复错误
4. **日志记录**:记录错误便于排查问题
5. **降级策略**:网络错误时提供缓存数据或默认数据
## 错误处理流程图
```
请求失败
↓
检查 error.response
↓
存在 → HTTP 错误 → 根据状态码处理
↓
不存在 → 检查 error.request
↓
存在 → 网络错误 → 提示用户检查网络
↓
不存在 → 配置错误 → 检查代码
```
服务端 · 3月7日 12:10
axios 有哪些高级特性?如文件上传下载、进度监控、CSRF 防护等## Axios 高级特性概览
Axios 不仅支持基本的 HTTP 请求,还提供了许多高级特性,包括文件上传下载、进度监控、CSRF 防护、请求转换等。
## 1. 文件上传
### 基础文件上传
```javascript
// HTML
// <input type="file" id="fileInput" />
const uploadFile = async (file) => {
const formData = new FormData();
formData.append('file', file);
try {
const response = await axios.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
return response.data;
} catch (error) {
console.error('上传失败:', error);
throw error;
}
};
// 使用
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
uploadFile(file);
});
```
### 多文件上传
```javascript
const uploadMultipleFiles = async (files) => {
const formData = new FormData();
files.forEach((file, index) => {
formData.append(`file${index}`, file);
});
// 或者使用相同字段名
// files.forEach(file => {
// formData.append('files', file);
// });
const response = await axios.post('/api/upload-multiple', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
return response.data;
};
```
### 带进度条的文件上传
```javascript
const uploadWithProgress = (file, onProgress) => {
const formData = new FormData();
formData.append('file', file);
return axios.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (progressEvent) => {
if (progressEvent.lengthComputable) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
onProgress(percentCompleted);
}
}
});
};
// React 组件中使用
function FileUpload() {
const [progress, setProgress] = useState(0);
const handleUpload = async (file) => {
try {
const result = await uploadWithProgress(file, setProgress);
console.log('上传成功:', result);
} catch (error) {
console.error('上传失败:', error);
}
};
return (
<div>
<input type="file" onChange={(e) => handleUpload(e.target.files[0])} />
<progress value={progress} max="100" />
<span>{progress}%</span>
</div>
);
}
```
## 2. 文件下载
### 基础文件下载
```javascript
const downloadFile = async (url, filename) => {
try {
const response = await axios.get(url, {
responseType: 'blob' // 重要:设置响应类型为 blob
});
// 创建下载链接
const blob = new Blob([response.data]);
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(downloadUrl);
} catch (error) {
console.error('下载失败:', error);
}
};
// 使用
downloadFile('/api/download/report.pdf', 'report.pdf');
```
### 带进度条的文件下载
```javascript
const downloadWithProgress = async (url, filename, onProgress) => {
const response = await axios.get(url, {
responseType: 'blob',
onDownloadProgress: (progressEvent) => {
if (progressEvent.lengthComputable) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
onProgress(percentCompleted);
}
}
});
const blob = new Blob([response.data]);
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(downloadUrl);
};
```
## 3. CSRF 防护
### 自动 CSRF Token 处理
```javascript
const instance = axios.create({
// 从 cookie 中读取 CSRF token 的字段名
xsrfCookieName: 'XSRF-TOKEN',
// 请求头中发送 CSRF token 的字段名
xsrfHeaderName: 'X-XSRF-TOKEN',
// 允许携带 cookie
withCredentials: true
});
```
### 手动设置 CSRF Token
```javascript
// 从 meta 标签获取 CSRF token
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
const instance = axios.create({
headers: {
'X-CSRF-TOKEN': csrfToken
}
});
// 或者通过拦截器动态设置
instance.interceptors.request.use(config => {
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
if (token) {
config.headers['X-CSRF-TOKEN'] = token;
}
return config;
});
```
## 4. 请求和响应转换
### 请求数据转换
```javascript
const instance = axios.create({
// 在请求发送到服务器之前修改请求数据
transformRequest: [
function (data, headers) {
// 对 data 进行转换
if (data instanceof FormData) {
return data;
}
// 添加时间戳防止缓存
if (data && typeof data === 'object') {
data._timestamp = Date.now();
}
return JSON.stringify(data);
}
]
});
```
### 响应数据转换
```javascript
const instance = axios.create({
// 在传递给 then/catch 之前修改响应数据
transformResponse: [
function (data) {
// 解析 JSON
const parsed = JSON.parse(data);
// 统一处理响应格式
if (parsed.code !== 0) {
throw new Error(parsed.message);
}
return parsed.data;
}
]
});
```
## 5. 参数序列化
### 自定义参数序列化
```javascript
import qs from 'qs';
const instance = axios.create({
// 自定义 params 序列化
paramsSerializer: {
encode?: (param: string): string => encodeURIComponent(param), // 自定义编码函数
serialize?: (params: Record<string, any>, options?: ParamsSerializerOptions): string => {
// 使用 qs 库进行序列化
return qs.stringify(params, { arrayFormat: 'brackets' });
},
indexes: false // 数组参数不使用索引
}
});
// 使用
instance.get('/api/search', {
params: {
q: 'keyword',
tags: ['javascript', 'axios']
}
});
// 结果: /api/search?q=keyword&tags[]=javascript&tags[]=axios
```
## 6. 代理配置
```javascript
// Node.js 环境
const instance = axios.create({
proxy: {
protocol: 'https',
host: '127.0.0.1',
port: 9000,
auth: {
username: 'mikeymike',
password: 'rapunz3l'
}
}
});
// 或者使用环境变量
const instance = axios.create({
proxy: false // 禁用代理
});
```
## 7. 适配器
### 自定义适配器
```javascript
const instance = axios.create({
adapter: (config) => {
return new Promise((resolve, reject) => {
// 自定义请求实现
const xhr = new XMLHttpRequest();
xhr.open(config.method.toUpperCase(), config.url);
xhr.onload = () => {
resolve({
data: xhr.response,
status: xhr.status,
statusText: xhr.statusText,
headers: {},
config,
request: xhr
});
};
xhr.onerror = () => reject(new Error('Request failed'));
xhr.send(config.data);
});
}
});
```
## 8. 验证状态码
```javascript
const instance = axios.create({
// 自定义合法状态码
validateStatus: (status) => {
return status >= 200 && status < 300; // 默认值
// 或者接受所有状态码
// return true;
// 或者只接受特定状态码
// return [200, 201, 204].includes(status);
}
});
```
## 9. 最大内容长度和重定向
```javascript
const instance = axios.create({
// 最大响应内容长度(字节)
maxContentLength: 2000,
// 最大请求内容长度(字节)
maxBodyLength: 2000,
// 最大重定向次数
maxRedirects: 5,
// 在 Node.js 中遵循重定向
// 在浏览器中此配置无效(浏览器自动处理重定向)
});
```
## 10. 完整的高级配置示例
```javascript
import axios from 'axios';
import qs from 'qs';
const advancedApi = axios.create({
baseURL: 'https://api.example.com',
timeout: 30000,
// 请求头
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
// 携带 cookie
withCredentials: true,
// CSRF 防护
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
// 响应类型
responseType: 'json',
// 参数序列化
paramsSerializer: (params) => {
return qs.stringify(params, { arrayFormat: 'repeat' });
},
// 请求转换
transformRequest: [
(data, headers) => {
// 添加认证信息
const token = localStorage.getItem('token');
if (token) {
headers.Authorization = `Bearer ${token}`;
}
// 如果不是 FormData,转换为 JSON
if (data && !(data instanceof FormData)) {
return JSON.stringify(data);
}
return data;
}
],
// 响应转换
transformResponse: [
(data) => {
// 统一错误处理
if (data && data.code !== 0) {
throw new Error(data.message);
}
return data?.data || data;
}
],
// 状态码验证
validateStatus: (status) => {
return status >= 200 && status < 500;
},
// 最大内容长度
maxContentLength: 50 * 1024 * 1024, // 50MB
// 最大重定向次数
maxRedirects: 5
});
// 添加进度监控拦截器
advancedApi.interceptors.request.use(config => {
if (config.onUploadProgress || config.onDownloadProgress) {
console.log('请求包含进度监控');
}
return config;
});
export default advancedApi;
```
## 最佳实践
1. **文件上传**:始终使用 FormData,设置正确的 Content-Type
2. **文件下载**:设置 responseType 为 'blob' 或 'arraybuffer'
3. **CSRF 防护**:正确配置 xsrfCookieName 和 xsrfHeaderName
4. **进度监控**:在需要用户体验的场景中使用
5. **数据转换**:统一处理请求和响应数据格式
6. **错误处理**:在 transformResponse 中统一处理业务错误
服务端 · 3月7日 12:10
axios 和 fetch API 有什么区别?在什么场景下应该选择使用 axios?## Axios vs Fetch API 对比
Axios 和 Fetch API 都是用于发送 HTTP 请求的工具,但它们在功能、易用性和兼容性方面存在显著差异。
## 核心区别对比表
| 特性 | Axios | Fetch API |
|------|-------|-----------|
| **API 设计** | 基于 Promise,API 更友好 | 原生 Promise,API 较底层 |
| **JSON 处理** | 自动转换 JSON 数据 | 需要手动调用 `.json()` |
| **请求拦截器** | ✅ 原生支持 | ❌ 需要自行封装 |
| **响应拦截器** | ✅ 原生支持 | ❌ 需要自行封装 |
| **请求取消** | ✅ 支持 AbortController | ✅ 支持 AbortController |
| **超时设置** | ✅ 原生支持 timeout | ❌ 需要手动实现 |
| **进度监控** | ✅ 原生支持 onUploadProgress/onDownloadProgress | ❌ 需要手动实现 |
| **错误处理** | HTTP 错误自动 reject | 只有网络错误才 reject |
| **浏览器兼容** | IE11+ | Chrome 42+, Edge 14+, Firefox 39+ |
| **体积** | ~13KB (gzip) | 原生支持,无额外体积 |
| **Node.js 支持** | ✅ 支持 | ❌ 不支持(需 polyfill) |
## 详细对比分析
### 1. JSON 数据处理
**Axios(自动处理):**
```javascript
// 自动将响应转换为 JSON
const response = await axios.get('/api/users');
console.log(response.data); // 已经是 JavaScript 对象
// 自动将请求体转换为 JSON
await axios.post('/api/users', { name: 'John' });
```
**Fetch(手动处理):**
```javascript
// 需要手动调用 .json()
const response = await fetch('/api/users');
const data = await response.json(); // 额外的 await
console.log(data);
// 需要手动设置 headers 和 stringify
await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'John' })
});
```
### 2. 错误处理
**Axios(自动处理 HTTP 错误):**
```javascript
try {
const response = await axios.get('/api/not-found');
} catch (error) {
// 404 会进入 catch
console.log(error.response.status); // 404
}
```
**Fetch(需要手动检查状态):**
```javascript
const response = await fetch('/api/not-found');
// 需要手动检查状态码
if (!response.ok) {
// 404 不会自动进入 catch
throw new Error(`HTTP error! status: ${response.status}`);
}
```
### 3. 超时设置
**Axios(原生支持):**
```javascript
axios.get('/api/data', {
timeout: 5000 // 5秒超时
});
```
**Fetch(需要手动实现):**
```javascript
const fetchWithTimeout = (url, options = {}, timeout = 5000) => {
return Promise.race([
fetch(url, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), timeout)
)
]);
};
```
### 4. 拦截器
**Axios(原生支持):**
```javascript
// 请求拦截器
axios.interceptors.request.use(config => {
config.headers.Authorization = `Bearer ${token}`;
return config;
});
// 响应拦截器
axios.interceptors.response.use(
response => response.data,
error => {
if (error.response.status === 401) {
redirectToLogin();
}
return Promise.reject(error);
}
);
```
**Fetch(需要自行封装):**
```javascript
// 需要创建包装函数
const fetchWithAuth = (url, options = {}) => {
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
});
};
```
### 5. 进度监控
**Axios(原生支持):**
```javascript
axios.post('/api/upload', formData, {
onUploadProgress: (progressEvent) => {
const percent = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(`上传进度: ${percent}%`);
}
});
```
**Fetch(需要手动实现):**
```javascript
// Fetch 没有原生进度支持,需要使用 ReadableStream
const response = await fetch('/api/download');
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 手动计算进度
}
```
## 选择建议
### 使用 Axios 的场景
1. **需要拦截器功能**
- 统一添加认证 Token
- 统一错误处理
- 统一日志记录
2. **需要进度监控**
- 文件上传下载
- 大文件传输
3. **需要超时控制**
- 防止请求挂起
- 提升用户体验
4. **项目复杂度较高**
- 多个 API 服务
- 复杂的错误处理逻辑
- 需要请求重试机制
5. **需要 IE 支持**
- 需要兼容 IE11
6. **Node.js 环境**
- 服务端渲染
- Node.js 脚本
### 使用 Fetch 的场景
1. **追求最小体积**
- 对包体积敏感的项目
- 简单的单页面应用
2. **现代浏览器环境**
- 不需要 IE 支持
- 现代框架(Next.js, Remix 等)
3. **简单的 HTTP 请求**
- 不需要复杂的拦截器
- 简单的 GET/POST 请求
4. **学习目的**
- 理解底层 HTTP API
- 教学演示
## 现代框架中的选择
### React/Vue/Angular 项目
```javascript
// 推荐使用 Axios
import axios from 'axios';
const api = axios.create({
baseURL: process.env.VUE_APP_API_URL,
timeout: 10000
});
// 添加拦截器
api.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
```
### Next.js / Remix 项目
```javascript
// 可以使用 Fetch(配合框架的数据获取函数)
// app/page.js (Next.js App Router)
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // 缓存配置
});
if (!res.ok) {
throw new Error('Failed to fetch');
}
return res.json();
}
```
## 总结
| 场景 | 推荐工具 |
|------|----------|
| 企业级应用 | Axios |
| 需要拦截器 | Axios |
| 文件上传下载 | Axios |
| 需要超时控制 | Axios |
| 需要 IE 支持 | Axios |
| Node.js 环境 | Axios |
| 简单项目 | Fetch |
| 对体积敏感 | Fetch |
| 现代浏览器 | Fetch |
| 学习目的 | Fetch |
**一般建议**:
- 中大型项目、需要复杂 HTTP 处理 → 选择 **Axios**
- 小型项目、简单请求、追求轻量 → 选择 **Fetch**
服务端 · 3月6日 23:04
使用 axios 时需要注意哪些安全问题?如何防止 XSS、CSRF 等攻击?在使用 axios 进行 HTTP 请求时,需要关注多种安全问题,包括 XSS、CSRF、敏感信息泄露等。
## 1. XSS(跨站脚本攻击)防护
### 问题描述
XSS 攻击可能通过 axios 请求的响应数据注入恶意脚本。
### 防护措施
```javascript
// 1. 响应数据转义
import DOMPurify from 'dompurify';
axios.interceptors.response.use(
(response) => {
// 对响应数据进行 XSS 过滤
if (response.data && typeof response.data === 'object') {
response.data = sanitizeData(response.data);
}
return response;
}
);
function sanitizeData(data) {
if (typeof data === 'string') {
return DOMPurify.sanitize(data);
}
if (Array.isArray(data)) {
return data.map(sanitizeData);
}
if (typeof data === 'object' && data !== null) {
return Object.keys(data).reduce((acc, key) => {
acc[key] = sanitizeData(data[key]);
return acc;
}, {});
}
return data;
}
// 2. 设置安全的 Content-Type
axios.defaults.headers.common['Content-Type'] = 'application/json; charset=utf-8';
// 3. 防止 JSON 注入
axios.interceptors.request.use(
(config) => {
if (config.data && typeof config.data === 'object') {
// 确保发送的是 JSON,不是可执行的 JavaScript
config.data = JSON.stringify(config.data);
}
return config;
}
);
```
## 2. CSRF(跨站请求伪造)防护
### 问题描述
攻击者诱导用户在已认证的网站上执行非预期的操作。
### 防护措施
```javascript
// 1. 使用 CSRF Token
const api = axios.create({
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
withCredentials: true // 允许携带 cookie
});
// 2. 手动添加 CSRF Token
api.interceptors.request.use((config) => {
// 从 meta 标签获取
const token = document.querySelector('meta[name="csrf-token"]')?.content;
if (token) {
config.headers['X-CSRF-Token'] = token;
}
// 或从 cookie 获取
const csrfToken = getCookie('csrfToken');
if (csrfToken) {
config.headers['X-CSRF-Token'] = csrfToken;
}
return config;
});
// 3. 双重 Cookie 验证
api.interceptors.request.use((config) => {
const sessionId = getCookie('sessionId');
const csrfToken = getCookie('csrfToken');
if (sessionId && csrfToken) {
config.headers['X-CSRF-Token'] = csrfToken;
// 验证 token 和 session 的关联性
}
return config;
});
// 4. SameSite Cookie 设置
// 服务端设置:Set-Cookie: sessionId=xxx; SameSite=Strict; Secure; HttpOnly
```
## 3. 敏感信息保护
### Token 安全存储
```javascript
// ❌ 不要直接存储在 localStorage(XSS 风险)
localStorage.setItem('token', token);
// ✅ 使用 httpOnly cookie(推荐)
// 服务端设置:Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict
// ✅ 如果必须使用 localStorage,添加额外的安全措施
const secureStorage = {
set(key, value) {
// 添加时间戳和签名
const data = {
value,
timestamp: Date.now(),
signature: generateSignature(value)
};
localStorage.setItem(key, JSON.stringify(data));
},
get(key) {
const item = localStorage.getItem(key);
if (!item) return null;
try {
const data = JSON.parse(item);
// 验证签名
if (data.signature !== generateSignature(data.value)) {
this.remove(key);
return null;
}
// 检查过期时间(例如 24 小时)
if (Date.now() - data.timestamp > 24 * 60 * 60 * 1000) {
this.remove(key);
return null;
}
return data.value;
} catch {
return null;
}
},
remove(key) {
localStorage.removeItem(key);
}
};
function generateSignature(value) {
// 使用简单的哈希或 HMAC
return btoa(value + SECRET_KEY);
}
```
### 请求头中的敏感信息
```javascript
// 1. 避免在 URL 中传递敏感信息
// ❌ 错误
axios.get(`/api/user?password=${password}`);
// ✅ 正确
axios.post('/api/user', { password });
// 2. 请求头加密
import CryptoJS from 'crypto-js';
axios.interceptors.request.use((config) => {
// 对敏感头部进行加密
if (config.headers.Authorization) {
config.headers['X-Encrypted'] = '1';
// 或使用自定义加密
// config.headers.Authorization = encrypt(config.headers.Authorization);
}
return config;
});
// 3. 限制请求头暴露
// 服务端设置:Access-Control-Expose-Headers: limited-headers
```
## 4. HTTPS 和证书验证
```javascript
// 1. 强制使用 HTTPS
const api = axios.create({
baseURL: 'https://api.example.com', // 必须使用 HTTPS
});
// 2. Node.js 环境中的证书验证
const https = require('https');
const fs = require('fs');
const api = axios.create({
httpsAgent: new https.Agent({
// 生产环境不要设置为 false
rejectUnauthorized: true,
// 使用自定义 CA 证书
ca: fs.readFileSync('path/to/ca-cert.pem')
})
});
// 3. 证书固定(Certificate Pinning)
const httpsAgent = new https.Agent({
checkServerIdentity: (host, cert) => {
const expectedFingerprint = 'AA:BB:CC:DD:EE:FF:...';
const actualFingerprint = cert.fingerprint256;
if (actualFingerprint !== expectedFingerprint) {
throw new Error('Certificate fingerprint mismatch');
}
}
});
```
## 5. 请求参数验证
```javascript
import Joi from 'joi';
// 1. 请求参数校验
const requestSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).max(32).required(),
age: Joi.number().integer().min(0).max(150)
});
axios.interceptors.request.use((config) => {
if (config.data) {
const { error } = requestSchema.validate(config.data);
if (error) {
return Promise.reject(new Error(`Validation error: ${error.message}`));
}
}
return config;
});
// 2. URL 参数编码
axios.interceptors.request.use((config) => {
if (config.params) {
config.params = Object.keys(config.params).reduce((acc, key) => {
acc[key] = encodeURIComponent(config.params[key]);
return acc;
}, {});
}
return config;
});
// 3. 防止 SQL 注入(前端层面)
function sanitizeInput(input) {
if (typeof input !== 'string') return input;
// 移除或转义危险字符
return input
.replace(/[;\"']/g, '')
.replace(/--/g, '')
.replace(/\/\*/g, '')
.replace(/\*\//g, '');
}
axios.interceptors.request.use((config) => {
if (config.data) {
config.data = Object.keys(config.data).reduce((acc, key) => {
acc[key] = sanitizeInput(config.data[key]);
return acc;
}, {});
}
return config;
});
```
## 6. 响应安全验证
```javascript
// 1. 验证响应内容类型
axios.interceptors.response.use(
(response) => {
const contentType = response.headers['content-type'];
// 确保响应是 JSON
if (!contentType || !contentType.includes('application/json')) {
return Promise.reject(new Error('Invalid content type'));
}
return response;
}
);
// 2. 验证响应数据签名
axios.interceptors.response.use(
(response) => {
const signature = response.headers['x-response-signature'];
const data = JSON.stringify(response.data);
if (signature && !verifySignature(data, signature)) {
return Promise.reject(new Error('Invalid response signature'));
}
return response;
}
);
function verifySignature(data, signature) {
const expectedSignature = CryptoJS.HmacSHA256(data, SECRET_KEY).toString();
return signature === expectedSignature;
}
```
## 7. 安全头部设置
```javascript
// 发送安全相关的请求头
const secureApi = axios.create({
headers: {
// 防止 MIME 类型嗅探
'X-Content-Type-Options': 'nosniff',
// 启用 XSS 过滤器
'X-XSS-Protection': '1; mode=block',
// 点击劫持防护
'X-Frame-Options': 'DENY',
// 内容安全策略
'Content-Security-Policy': "default-src 'self'",
// 严格的传输安全
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
}
});
```
## 8. 日志和监控
```javascript
// 安全日志记录
axios.interceptors.request.use((config) => {
// 记录请求日志(不包含敏感信息)
securityLogger.info({
type: 'request',
url: config.url,
method: config.method,
timestamp: new Date().toISOString(),
// 不要记录 headers 或 data 中的敏感信息
});
return config;
});
axios.interceptors.response.use(
(response) => {
securityLogger.info({
type: 'response',
url: response.config.url,
status: response.status,
timestamp: new Date().toISOString()
});
return response;
},
(error) => {
securityLogger.error({
type: 'error',
url: error.config?.url,
status: error.response?.status,
message: error.message,
timestamp: new Date().toISOString()
});
return Promise.reject(error);
}
);
// 异常检测
function detectAnomalies(response) {
// 检测异常大的响应
const responseSize = JSON.stringify(response.data).length;
if (responseSize > 10 * 1024 * 1024) { // 10MB
securityLogger.warn('Unusually large response detected');
}
// 检测异常的响应时间
const duration = response.config.metadata?.duration;
if (duration > 30000) { // 30秒
securityLogger.warn('Slow response detected');
}
}
```
## 9. 完整的安全配置示例
```javascript
import axios from 'axios';
import DOMPurify from 'dompurify';
import CryptoJS from 'crypto-js';
const secureApi = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
withCredentials: true,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
headers: {
'Content-Type': 'application/json; charset=utf-8',
'X-Content-Type-Options': 'nosniff',
'X-XSS-Protection': '1; mode=block'
}
});
// 请求拦截器 - 安全处理
secureApi.interceptors.request.use(
(config) => {
// 1. 参数验证和清理
if (config.data) {
config.data = sanitizeData(config.data);
}
// 2. 添加请求签名
const timestamp = Date.now();
config.headers['X-Timestamp'] = timestamp;
config.headers['X-Signature'] = generateRequestSignature(config, timestamp);
// 3. 记录安全日志
logSecurityEvent('request', config);
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器 - 安全处理
secureApi.interceptors.response.use(
(response) => {
// 1. 验证响应签名
if (!verifyResponseSignature(response)) {
return Promise.reject(new Error('Invalid response signature'));
}
// 2. XSS 清理
if (response.data) {
response.data = sanitizeData(response.data);
}
// 3. 异常检测
detectAnomalies(response);
return response;
},
(error) => {
logSecurityEvent('error', null, error);
return Promise.reject(error);
}
);
export default secureApi;
```
## 安全检查清单
* [ ] 使用 HTTPS 传输所有数据
* [ ] 启用 CSRF 防护
* [ ] 敏感信息使用 httpOnly Cookie 存储
* [ ] 对输入数据进行验证和清理
* [ ] 对输出数据进行 XSS 过滤
* [ ] 设置适当的安全头部
* [ ] 实现请求/响应签名验证
* [ ] 记录安全日志并监控异常
* [ ] 定期更新依赖包
* [ ] 进行安全审计和渗透测试
服务端 · 3月6日 23:03
axios 的底层实现原理是什么?请说明其核心架构和请求流程Axios 是一个基于 Promise 的 HTTP 客户端,其核心架构设计优雅,支持浏览器和 Node.js 环境。
## 1. 核心架构概览
### 整体架构图
```
┌─────────────────────────────────────────────────────────────┐
│ Axios 入口 │
│ axios.create() / axios.get() / axios.post() │
└──────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Axios 实例 │
│ - defaults (默认配置) │
│ - interceptors (拦截器) │
│ - request / get / post 等方法 │
└──────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 请求处理流程 │
│ 1. 合并配置 (mergeConfig) │
│ 2. 请求拦截器 (request interceptors) │
│ 3. 转换请求数据 (transformRequest) │
│ 4. 适配器执行请求 (adapter) │
│ 5. 转换响应数据 (transformResponse) │
│ 6. 响应拦截器 (response interceptors) │
└──────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 适配器层 (Adapter) │
│ 浏览器: XMLHttpRequest │
│ Node.js: http / https 模块 │
└─────────────────────────────────────────────────────────────┘
```
## 2. 核心源码解析
### Axios 类定义
```javascript
// 简化版 Axios 类结构
class Axios {
constructor(instanceConfig) {
// 默认配置
this.defaults = instanceConfig;
// 拦截器管理器
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
// 核心请求方法
request(config) {
// 1. 配置处理
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
}
// 2. 合并配置
config = mergeConfig(this.defaults, config);
// 3. 设置请求方法
if (config.method) {
config.method = config.method.toLowerCase();
}
// 4. 构建拦截器链
const chain = [dispatchRequest, undefined];
let promise = Promise.resolve(config);
// 请求拦截器
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 响应拦截器
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
// 5. 执行链
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
// 便捷方法
get(url, config) {
return this.request(mergeConfig(config || {}, {
method: 'get',
url
}));
}
post(url, data, config) {
return this.request(mergeConfig(config || {}, {
method: 'post',
url,
data
}));
}
// ... delete, head, options, put, patch
}
```
### 拦截器管理器
```javascript
// 拦截器管理器实现
class InterceptorManager {
constructor() {
this.handlers = [];
}
// 添加拦截器
use(fulfilled, rejected, options) {
this.handlers.push({
fulfilled,
rejected,
synchronous: options?.synchronous || false,
runWhen: options?.runWhen || null
});
return this.handlers.length - 1;
}
// 移除拦截器
eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
}
// 遍历拦截器
forEach(fn) {
this.handlers.forEach((handler) => {
if (handler !== null) {
fn(handler);
}
});
}
}
```
### 请求分发器
```javascript
// 请求分发函数
function dispatchRequest(config) {
// 1. 检查请求是否已取消
throwIfCancellationRequested(config);
// 2. 转换请求数据
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
// 3. 合并 headers
config.headers = flattenHeaders(
config.headers,
config.method
);
// 4. 获取适配器
const adapter = config.adapter || defaults.adapter;
// 5. 执行适配器
return adapter(config).then(
function onAdapterResolution(response) {
// 检查请求是否已取消
throwIfCancellationRequested(config);
// 转换响应数据
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
return response;
},
function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
// 转换错误信息
if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
}
);
}
```
## 3. 适配器层实现
### 浏览器适配器 (XHR)
```javascript
// 浏览器端适配器 - 基于 XMLHttpRequest
function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
const requestData = config.data;
const requestHeaders = config.headers;
const responseType = config.responseType;
// 创建 XHR 对象
const request = new XMLHttpRequest();
// 配置请求
request.open(config.method.toUpperCase(), buildURL(config.url, config.params), true);
// 设置超时
request.timeout = config.timeout;
// 设置响应类型
if (responseType) {
request.responseType = responseType;
}
// 设置 withCredentials
if (config.withCredentials) {
request.withCredentials = true;
}
// 设置请求头
Object.keys(requestHeaders).forEach((key) => {
if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
delete requestHeaders[key];
} else {
request.setRequestHeader(key, requestHeaders[key]);
}
});
// 处理取消请求
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
request.abort();
reject(cancel);
});
}
// 监听状态变化
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// 准备响应对象
const response = {
data: responseType === 'text' ? request.responseText : request.response,
status: request.status,
statusText: request.statusText,
headers: parseHeaders(request.getAllResponseHeaders()),
config,
request
};
// 根据状态码处理响应
settle(resolve, reject, response);
};
// 处理错误
request.onerror = function handleError() {
reject(new Error('Network Error'));
};
// 处理超时
request.ontimeout = function handleTimeout() {
reject(new Error(`timeout of ${config.timeout}ms exceeded`));
};
// 处理下载进度
if (config.onDownloadProgress) {
request.onprogress = config.onDownloadProgress;
}
// 处理上传进度
if (config.onUploadProgress) {
request.upload.onprogress = config.onUploadProgress;
}
// 发送请求
request.send(requestData);
});
}
```
### Node.js 适配器 (HTTP)
```javascript
// Node.js 端适配器 - 基于 http/https 模块
function httpAdapter(config) {
return new Promise(function dispatchHttpRequest(resolve, reject) {
const {
url,
method,
data,
headers,
timeout,
httpAgent,
httpsAgent
} = config;
// 解析 URL
const parsed = new URL(url);
const isHttps = parsed.protocol === 'https:';
// 选择模块
const http = isHttps ? require('https') : require('http');
// 配置选项
const options = {
hostname: parsed.hostname,
port: parsed.port,
path: parsed.pathname + parsed.search,
method: method.toUpperCase(),
headers,
agent: isHttps ? httpsAgent : httpAgent,
timeout
};
// 创建请求
const req = http.request(options, (res) => {
let responseData = [];
res.on('data', (chunk) => {
responseData.push(chunk);
});
res.on('end', () => {
const response = {
data: Buffer.concat(responseData).toString(),
status: res.statusCode,
statusText: res.statusMessage,
headers: res.headers,
config
};
settle(resolve, reject, response);
});
});
// 处理错误
req.on('error', (err) => {
reject(err);
});
// 处理超时
req.on('timeout', () => {
req.destroy();
reject(new Error('timeout'));
});
// 发送数据
if (data) {
req.write(data);
}
req.end();
});
}
```
## 4. 请求流程详解
### 完整请求流程
```javascript
// 示例:axios.get('/user')
// Step 1: 调用 axios.get
axios.get('/user', { params: { id: 123 } });
// Step 2: 进入 Axios.request 方法
// - 合并配置
// - 构建拦截器链
// Step 3: 执行请求拦截器
// 拦截器 2 (fulfilled) → 拦截器 1 (fulfilled) → dispatchRequest
// Step 4: dispatchRequest
// - 转换请求数据
// - 获取适配器
// - 执行适配器
// Step 5: 适配器执行
// 浏览器: 创建 XHR → 配置 → 发送 → 监听回调
// Node.js: 创建 HTTP 请求 → 配置 → 发送 → 监听回调
// Step 6: 处理响应
// - 转换响应数据
// - 执行响应拦截器
// - 返回结果
// 完整流程代码示例
axios.get('/user', { params: { id: 123 } })
.then(response => {
// 响应拦截器处理后的结果
console.log(response.data);
})
.catch(error => {
// 错误处理
console.error(error);
});
```
### 拦截器执行顺序
```javascript
// 请求拦截器执行顺序:后添加的先执行
axios.interceptors.request.use(config => {
console.log('Request Interceptor 1');
return config;
});
axios.interceptors.request.use(config => {
console.log('Request Interceptor 2');
return config;
});
// 执行顺序: Interceptor 2 → Interceptor 1 → Request
// 响应拦截器执行顺序:先添加的先执行
axios.interceptors.response.use(response => {
console.log('Response Interceptor 1');
return response;
});
axios.interceptors.response.use(response => {
console.log('Response Interceptor 2');
return response;
});
// 执行顺序: Response → Interceptor 1 → Interceptor 2
```
## 5. 取消请求实现原理
### CancelToken 实现
```javascript
// CancelToken 类
class CancelToken {
constructor(executor) {
let resolvePromise;
// 创建 Promise
this.promise = new Promise((resolve) => {
resolvePromise = resolve;
});
// 执行器函数
executor((message) => {
if (this.reason) {
return; // 已经取消过
}
this.reason = new Cancel(message);
resolvePromise(this.reason);
});
}
// 静态方法:source
static source() {
let cancel;
const token = new CancelToken((c) => {
cancel = c;
});
return { token, cancel };
}
}
// Cancel 类
class Cancel {
constructor(message) {
this.message = message;
}
toString() {
return `Cancel${this.message ? ': ' + this.message : ''}`;
}
}
// 使用示例
const source = CancelToken.source();
axios.get('/user', {
cancelToken: source.token
});
// 取消请求
source.cancel('Operation canceled by the user.');
```
## 6. 数据转换流程
```javascript
// 请求数据转换
function transformRequest(data, headers) {
// 如果是对象,转换为 JSON 字符串
if (typeof data === 'object' && data !== null) {
headers['Content-Type'] = 'application/json';
return JSON.stringify(data);
}
return data;
}
// 响应数据转换
function transformResponse(data) {
// 尝试解析 JSON
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) {
// 不是 JSON,保持原样
}
}
return data;
}
// 在配置中使用
transformRequest: [
function(data, headers) {
// 自定义转换逻辑
return data;
}
],
transformResponse: [
function(data) {
// 自定义转换逻辑
return data;
}
]
```
## 7. 配置合并策略
```javascript
// 配置合并函数
function mergeConfig(config1, config2) {
const config = {};
// 默认配置
const defaultToConfig2 = [
'url', 'method', 'data', 'baseURL', 'transformRequest',
'transformResponse', 'paramsSerializer', 'timeout',
'withCredentials', 'adapter', 'responseType', 'xsrfCookieName',
'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress',
'maxContentLength', 'validateStatus', 'maxRedirects', 'httpAgent',
'httpsAgent', 'cancelToken', 'socketPath'
];
defaultToConfig2.forEach(prop => {
if (typeof config2[prop] !== 'undefined') {
config[prop] = config2[prop];
} else if (typeof config1[prop] !== 'undefined') {
config[prop] = config1[prop];
}
});
// Headers 需要深度合并
config.headers = mergeHeaders(config1.headers, config2.headers);
// Params 需要合并
config.params = { ...config1.params, ...config2.params };
return config;
}
```
## 8. 核心设计模式
### 1. 适配器模式 (Adapter)
```javascript
// 统一接口,不同实现
const adapter = isBrowser ? xhrAdapter : httpAdapter;
```
### 2. 责任链模式 (Chain of Responsibility)
```javascript
// 拦截器链
const chain = [
requestInterceptor2, undefined,
requestInterceptor1, undefined,
dispatchRequest, undefined,
responseInterceptor1, undefined,
responseInterceptor2, undefined
];
```
### 3. 工厂模式 (Factory)
```javascript
// 创建 Axios 实例
axios.create = function create(instanceConfig) {
return new Axios(instanceConfig);
};
```
### 4. 观察者模式 (Observer)
```javascript
// 取消令牌
const source = CancelToken.source();
source.token.promise.then(onCanceled);
```
## 总结
Axios 的核心设计亮点:
1. **统一的 Promise API**:简化异步处理
2. **拦截器机制**:灵活的请求/响应处理
3. **适配器模式**:支持多平台运行
4. **配置合并策略**:灵活的配置管理
5. **取消请求**:完善的请求控制
6. **数据转换**:自动化的数据处理
7. **错误处理**:统一的错误管理机制
服务端 · 3月6日 23:02
使用 axios 时有哪些性能优化技巧?如何减少不必要的网络请求?在使用 axios 进行 HTTP 请求时,可以通过多种方式优化性能,减少不必要的网络开销,提升用户体验。
## 1. 请求缓存
### 内存缓存
```javascript
class AxiosCache {
constructor() {
this.cache = new Map();
this.ttl = 5 * 60 * 1000; // 5分钟缓存
}
generateKey(config) {
return `${config.method}-${config.url}-${JSON.stringify(config.params)}`;
}
get(config) {
const key = this.generateKey(config);
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
this.cache.delete(key);
return null;
}
set(config, data) {
const key = this.generateKey(config);
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
clear() {
this.cache.clear();
}
}
const cache = new AxiosCache();
// 使用缓存的 axios 实例
const cachedApi = axios.create();
cachedApi.interceptors.request.use(config => {
// 检查缓存
const cached = cache.get(config);
if (cached) {
// 返回缓存数据,取消请求
config.adapter = () => Promise.resolve({
data: cached,
status: 200,
statusText: 'OK',
headers: {},
config
});
}
return config;
});
cachedApi.interceptors.response.use(response => {
// 缓存响应数据
if (response.config.method === 'get') {
cache.set(response.config, response.data);
}
return response;
});
```
### 使用 Cache API(Service Worker)
```javascript
// 在 Service Worker 中缓存请求
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/')) {
event.respondWith(
caches.match(event.request).then(response => {
if (response) {
return response;
}
return fetch(event.request).then(response => {
const clone = response.clone();
caches.open('api-cache').then(cache => {
cache.put(event.request, clone);
});
return response;
});
})
);
}
});
```
## 2. 请求去重(防抖)
```javascript
class RequestDeduper {
constructor() {
this.pendingRequests = new Map();
}
generateKey(config) {
return `${config.method}-${config.url}-${JSON.stringify(config.params)}-${JSON.stringify(config.data)}`;
}
async request(config) {
const key = this.generateKey(config);
// 如果有正在进行的相同请求,返回该 Promise
if (this.pendingRequests.has(key)) {
return this.pendingRequests.get(key);
}
// 创建新请求
const promise = axios(config).finally(() => {
this.pendingRequests.delete(key);
});
this.pendingRequests.set(key, promise);
return promise;
}
}
const deduper = new RequestDeduper();
// 使用
const fetchUser = (id) => deduper.request({
method: 'GET',
url: `/api/users/${id}`
});
// 同时调用多次,只会发送一次请求
fetchUser(1);
fetchUser(1);
fetchUser(1); // 三次调用,一次请求
```
## 3. 请求合并
```javascript
class RequestBatcher {
constructor() {
this.batch = [];
this.timeout = null;
this.delay = 50; // 50ms 内的请求合并
}
addRequest(request) {
return new Promise((resolve, reject) => {
this.batch.push({ request, resolve, reject });
clearTimeout(this.timeout);
this.timeout = setTimeout(() => this.flush(), this.delay);
});
}
async flush() {
if (this.batch.length === 0) return;
const currentBatch = this.batch;
this.batch = [];
// 合并请求
const ids = currentBatch.map(item => item.request.id);
try {
const response = await axios.post('/api/batch', { ids });
// 分发结果
currentBatch.forEach((item, index) => {
item.resolve(response.data[index]);
});
} catch (error) {
currentBatch.forEach(item => {
item.reject(error);
});
}
}
}
```
## 4. 懒加载和分页
```javascript
// 虚拟滚动 + 分页加载
class VirtualListLoader {
constructor(api, pageSize = 20) {
this.api = api;
this.pageSize = pageSize;
this.cache = new Map();
this.loadingPages = new Set();
}
async loadPage(page) {
// 检查缓存
if (this.cache.has(page)) {
return this.cache.get(page);
}
// 防止重复加载
if (this.loadingPages.has(page)) {
return new Promise(resolve => {
const check = setInterval(() => {
if (this.cache.has(page)) {
clearInterval(check);
resolve(this.cache.get(page));
}
}, 100);
});
}
this.loadingPages.add(page);
try {
const response = await this.api.get('/api/items', {
params: {
page,
pageSize: this.pageSize
}
});
this.cache.set(page, response.data);
return response.data;
} finally {
this.loadingPages.delete(page);
}
}
}
```
## 5. 请求优先级管理
```javascript
class PriorityRequestQueue {
constructor() {
this.queue = [];
this.maxConcurrent = 6; // 浏览器最大并发数
this.running = 0;
}
add(config, priority = 0) {
return new Promise((resolve, reject) => {
this.queue.push({
config,
priority,
resolve,
reject
});
this.queue.sort((a, b) => b.priority - a.priority);
this.process();
});
}
async process() {
if (this.running >= this.maxConcurrent || this.queue.length === 0) {
return;
}
this.running++;
const { config, resolve, reject } = this.queue.shift();
try {
const response = await axios(config);
resolve(response);
} catch (error) {
reject(error);
} finally {
this.running--;
this.process();
}
}
}
// 使用
const queue = new PriorityRequestQueue();
// 高优先级请求
queue.add({ url: '/api/critical-data' }, 10);
// 低优先级请求
queue.add({ url: '/api/background-data' }, 1);
```
## 6. 压缩和精简请求
```javascript
// 请求数据压缩
const compressRequest = (data) => {
// 移除 undefined 和 null 值
const cleaned = JSON.parse(JSON.stringify(data));
return cleaned;
};
// 字段精简
const minimizeFields = (fields) => {
// 只请求需要的字段
return fields.join(',');
};
axios.get('/api/users', {
params: {
fields: minimizeFields(['id', 'name', 'avatar']),
include: minimizeFields(['posts', 'comments'])
}
});
```
## 7. 使用 HTTP/2 Server Push
```javascript
// 服务端配置 HTTP/2 Push
// 在响应头中添加 Link 头
// Link: </api/related-data>; rel=preload; as=fetch
// 客户端预加载
const preloadResources = () => {
const links = document.querySelectorAll('link[rel=preload][as=fetch]');
links.forEach(link => {
axios.get(link.href, {
headers: { 'Purpose': 'prefetch' }
});
});
};
```
## 8. 连接复用和 Keep-Alive
```javascript
// 使用相同的 axios 实例以复用连接
const api = axios.create({
baseURL: 'https://api.example.com',
// 启用 keep-alive(在 Node.js 中)
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true })
});
// 浏览器端自动复用连接
```
## 9. 请求超时优化
```javascript
// 根据网络状况动态调整超时
const getTimeout = () => {
const connection = navigator.connection;
if (connection) {
switch (connection.effectiveType) {
case '4g': return 10000;
case '3g': return 20000;
case '2g': return 30000;
default: return 15000;
}
}
return 10000;
};
axios.get('/api/data', {
timeout: getTimeout()
});
```
## 10. 错误重试策略
```javascript
axios.interceptors.response.use(null, async (error) => {
const { config } = error;
if (!config || !config.retry) {
return Promise.reject(error);
}
config.retryCount = config.retryCount || 0;
if (config.retryCount >= config.retry) {
return Promise.reject(error);
}
config.retryCount += 1;
// 指数退避
const backoff = Math.pow(2, config.retryCount) * 1000;
await new Promise(resolve => setTimeout(resolve, backoff));
return axios(config);
});
// 使用
axios.get('/api/data', {
retry: 3,
retryDelay: 1000
});
```
## 11. 离线优先策略
```javascript
// 使用 IndexedDB 缓存
const offlineFirstRequest = async (config) => {
try {
// 先尝试网络请求
const response = await axios(config);
// 缓存到 IndexedDB
await saveToIndexedDB(config, response.data);
return response;
} catch (error) {
// 网络失败,尝试从缓存读取
const cached = await getFromIndexedDB(config);
if (cached) {
return { data: cached, fromCache: true };
}
throw error;
}
};
```
## 12. 监控和分析
```javascript
// 性能监控拦截器
axios.interceptors.request.use(config => {
config.metadata = { startTime: Date.now() };
return config;
});
axios.interceptors.response.use(response => {
const duration = Date.now() - response.config.metadata.startTime;
// 上报性能数据
analytics.track('api_request', {
url: response.config.url,
method: response.config.method,
duration,
status: response.status,
size: JSON.stringify(response.data).length
});
// 慢请求警告
if (duration > 3000) {
console.warn(`Slow request: ${response.config.url} took ${duration}ms`);
}
return response;
});
```
## 最佳实践总结
| 优化策略 | 适用场景 | 预期效果 |
| ----- | -------- | ------------ |
| 请求缓存 | 不频繁变化的数据 | 减少 50-90% 请求 |
| 请求去重 | 快速连续触发 | 减少重复请求 |
| 请求合并 | 批量操作 | 减少请求数量 |
| 分页加载 | 长列表 | 减少初始加载时间 |
| 优先级队列 | 关键/非关键请求 | 提升关键请求响应 |
| 数据压缩 | 大数据传输 | 减少传输体积 |
| 连接复用 | 频繁请求 | 减少连接开销 |
| 智能超时 | 不稳定网络 | 提升用户体验 |
| 错误重试 | 临时故障 | 提高成功率 |
| 离线优先 | 弱网环境 | 提升可用性 |
服务端 · 3月6日 23:02
如何对使用 axios 的代码进行单元测试和 Mock?请说明常用的测试方法对使用 axios 的代码进行测试时,需要掌握单元测试、集成测试和 Mock 技术。
## 1. 使用 Jest 和 axios-mock-adapter
### 安装依赖
```bash
npm install --save-dev jest axios-mock-adapter @testing-library/react
```
### 基础 Mock 测试
```javascript
// api/user.js
import axios from 'axios';
export const fetchUser = async (userId) => {
const response = await axios.get(`/api/users/${userId}`);
return response.data;
};
export const createUser = async (userData) => {
const response = await axios.post('/api/users', userData);
return response.data;
};
// __tests__/user.test.js
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { fetchUser, createUser } from '../api/user';
describe('User API', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
test('fetchUser should return user data', async () => {
const userData = { id: 1, name: 'John', email: 'john@example.com' };
mock.onGet('/api/users/1').reply(200, userData);
const result = await fetchUser(1);
expect(result).toEqual(userData);
});
test('fetchUser should handle error', async () => {
mock.onGet('/api/users/999').reply(404, { message: 'User not found' });
await expect(fetchUser(999)).rejects.toThrow();
});
test('createUser should create new user', async () => {
const newUser = { name: 'Jane', email: 'jane@example.com' };
const createdUser = { id: 2, ...newUser };
mock.onPost('/api/users').reply(201, createdUser);
const result = await createUser(newUser);
expect(result).toEqual(createdUser);
});
});
```
### 高级 Mock 配置
```javascript
// __tests__/api.test.js
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
describe('API Testing', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
test('mock network error', async () => {
mock.onGet('/api/data').networkError();
await expect(axios.get('/api/data')).rejects.toThrow('Network Error');
});
test('mock timeout', async () => {
mock.onGet('/api/data').timeout();
await expect(axios.get('/api/data')).rejects.toThrow('timeout');
});
test('mock with function', async () => {
mock.onPost('/api/users').reply((config) => {
const data = JSON.parse(config.data);
if (!data.email) {
return [400, { error: 'Email is required' }];
}
return [201, { id: 1, ...data }];
});
// 测试成功
const response = await axios.post('/api/users', {
name: 'John',
email: 'john@example.com'
});
expect(response.status).toBe(201);
// 测试失败
await expect(
axios.post('/api/users', { name: 'John' })
).rejects.toThrow();
});
test('mock with headers', async () => {
mock.onGet('/api/protected', {
headers: { Authorization: 'Bearer token123' }
}).reply(200, { data: 'protected' });
const response = await axios.get('/api/protected', {
headers: { Authorization: 'Bearer token123' }
});
expect(response.data).toEqual({ data: 'protected' });
});
test('mock with query params', async () => {
mock.onGet('/api/search', { params: { q: 'test' } })
.reply(200, { results: [] });
const response = await axios.get('/api/search', {
params: { q: 'test' }
});
expect(response.data).toEqual({ results: [] });
});
});
```
## 2. 使用 MSW (Mock Service Worker)
### 安装和配置
```bash
npm install --save-dev msw
```
```javascript
// mocks/handlers.js
import { rest } from 'msw';
export const handlers = [
// GET 请求
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
])
);
}),
// GET 单个用户
rest.get('/api/users/:id', (req, res, ctx) => {
const { id } = req.params;
if (id === '999') {
return res(
ctx.status(404),
ctx.json({ message: 'User not found' })
);
}
return res(
ctx.status(200),
ctx.json({ id: Number(id), name: 'John' })
);
}),
// POST 请求
rest.post('/api/users', async (req, res, ctx) => {
const body = await req.json();
return res(
ctx.status(201),
ctx.json({ id: 3, ...body })
);
}),
// PUT 请求
rest.put('/api/users/:id', async (req, res, ctx) => {
const { id } = req.params;
const body = await req.json();
return res(
ctx.status(200),
ctx.json({ id: Number(id), ...body })
);
}),
// DELETE 请求
rest.delete('/api/users/:id', (req, res, ctx) => {
return res(ctx.status(204));
})
];
// mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// jest.setup.js
import { server } from './mocks/server';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
```
### 使用 MSW 进行测试
```javascript
// __tests__/user.integration.test.js
import { fetchUser, createUser, updateUser, deleteUser } from '../api/user';
describe('User API Integration Tests', () => {
test('should fetch all users', async () => {
const users = await fetchUsers();
expect(users).toHaveLength(2);
expect(users[0]).toHaveProperty('id');
expect(users[0]).toHaveProperty('name');
});
test('should fetch single user', async () => {
const user = await fetchUser(1);
expect(user).toEqual({ id: 1, name: 'John' });
});
test('should handle 404 error', async () => {
await expect(fetchUser(999)).rejects.toThrow();
});
test('should create user', async () => {
const newUser = { name: 'Bob', email: 'bob@example.com' };
const created = await createUser(newUser);
expect(created).toMatchObject(newUser);
expect(created).toHaveProperty('id');
});
test('should update user', async () => {
const updates = { name: 'John Updated' };
const updated = await updateUser(1, updates);
expect(updated.name).toBe('John Updated');
});
test('should delete user', async () => {
await expect(deleteUser(1)).resolves.not.toThrow();
});
});
```
### 动态覆盖 Handler
```javascript
// __tests__/dynamic-mock.test.js
import { rest } from 'msw';
import { server } from '../mocks/server';
test('should handle server error', async () => {
// 临时覆盖 handler
server.use(
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.status(500),
ctx.json({ error: 'Internal Server Error' })
);
})
);
await expect(fetchUsers()).rejects.toThrow();
});
test('should handle network error', async () => {
server.use(
rest.get('/api/users', (req, res) => {
return res.networkError('Failed to connect');
})
);
await expect(fetchUsers()).rejects.toThrow('Failed to connect');
});
```
## 3. React 组件测试
### 使用 React Testing Library
```javascript
// components/UserProfile.jsx
import React, { useEffect, useState } from 'react';
import axios from 'axios';
export const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await axios.get(`/api/users/${userId}`);
setUser(response.data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
};
// __tests__/UserProfile.test.jsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { UserProfile } from '../components/UserProfile';
import { server } from '../mocks/server';
import { rest } from 'msw';
describe('UserProfile Component', () => {
test('should display loading state initially', () => {
render(<UserProfile userId={1} />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
test('should display user data after loading', async () => {
render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText('John')).toBeInTheDocument();
});
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
test('should display error message on failure', async () => {
server.use(
rest.get('/api/users/999', (req, res, ctx) => {
return res(ctx.status(404));
})
);
render(<UserProfile userId={999} />);
await waitFor(() => {
expect(screen.getByText(/Error:/)).toBeInTheDocument();
});
});
});
```
## 4. Vue 组件测试
### 使用 Vue Test Utils
```javascript
// components/UserProfile.vue
<template>
<div>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else>
<h1>{{ user.name }}</h1>
<p>{{ user.email }}</p>
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import axios from 'axios';
export default {
props: ['userId'],
setup(props) {
const user = ref(null);
const loading = ref(true);
const error = ref(null);
onMounted(async () => {
try {
const response = await axios.get(`/api/users/${props.userId}`);
user.value = response.data;
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
});
return { user, loading, error };
}
};
</script>
// __tests__/UserProfile.spec.js
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import UserProfile from '../components/UserProfile.vue';
import { server } from '../mocks/server';
describe('UserProfile', () => {
it('should display loading state initially', () => {
const wrapper = mount(UserProfile, {
props: { userId: 1 }
});
expect(wrapper.text()).toContain('Loading...');
});
it('should display user data after loading', async () => {
const wrapper = mount(UserProfile, {
props: { userId: 1 }
});
await wrapper.vm.$nextTick();
await new Promise(resolve => setTimeout(resolve, 0));
expect(wrapper.text()).toContain('John');
});
});
```
## 5. 自定义 Hook/Composable 测试
```javascript
// hooks/useApi.js
import { useState, useEffect } from 'react';
import axios from 'axios';
export const useApi = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await axios.get(url);
setData(response.data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
};
// __tests__/useApi.test.js
import { renderHook, waitFor } from '@testing-library/react';
import { useApi } from '../hooks/useApi';
describe('useApi Hook', () => {
test('should return loading state initially', () => {
const { result } = renderHook(() => useApi('/api/data'));
expect(result.current.loading).toBe(true);
expect(result.current.data).toBeNull();
expect(result.current.error).toBeNull();
});
test('should return data after successful request', async () => {
const { result } = renderHook(() => useApi('/api/users'));
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.data).toHaveLength(2);
expect(result.current.error).toBeNull();
});
test('should return error on failed request', async () => {
const { result } = renderHook(() => useApi('/api/error'));
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.error).not.toBeNull();
expect(result.current.data).toBeNull();
});
});
```
## 6. E2E 测试
### 使用 Cypress
```javascript
// cypress/integration/api.spec.js
describe('API Tests', () => {
beforeEach(() => {
// 拦截 API 请求
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
]
}).as('getUsers');
});
it('should display users list', () => {
cy.visit('/users');
cy.wait('@getUsers');
cy.get('[data-testid="user-list"]').should('have.length', 2);
cy.contains('John').should('be.visible');
});
it('should handle API error', () => {
cy.intercept('GET', '/api/users', {
statusCode: 500,
body: { error: 'Server Error' }
}).as('getUsersError');
cy.visit('/users');
cy.wait('@getUsersError');
cy.contains('Error loading users').should('be.visible');
});
it('should create new user', () => {
cy.intercept('POST', '/api/users', {
statusCode: 201,
body: { id: 3, name: 'New User' }
}).as('createUser');
cy.visit('/users/new');
cy.get('input[name="name"]').type('New User');
cy.get('button[type="submit"]').click();
cy.wait('@createUser').its('request.body').should('deep.equal', {
name: 'New User'
});
cy.url().should('include', '/users');
});
});
```
## 7. 测试最佳实践
### 测试文件组织
```
src/
├── api/
│ ├── user.js
│ └── __tests__/
│ └── user.test.js
├── components/
│ ├── UserProfile.jsx
│ └── __tests__/
│ └── UserProfile.test.jsx
├── hooks/
│ ├── useApi.js
│ └── __tests__/
│ └── useApi.test.js
└── mocks/
├── handlers.js
└── server.js
```
### 测试工具函数
```javascript
// test-utils.js
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
export const createTestQueryClient = () => new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
export function renderWithClient(ui) {
const testQueryClient = createTestQueryClient();
const { rerender, ...result } = render(
<QueryClientProvider client={testQueryClient}>{ui}</QueryClientProvider>
);
return {
...result,
rerender: (rerenderUi) =>
rerender(
<QueryClientProvider client={testQueryClient}>{rerenderUi}</QueryClientProvider>
),
};
}
```
## 测试策略总结
| 测试类型 | 工具 | 适用场景 |
| ------- | -------------------------------------- | ------------ |
| 单元测试 | Jest + axios-mock-adapter | 测试 API 函数 |
| 集成测试 | MSW | 测试组件与 API 交互 |
| 组件测试 | React Testing Library / Vue Test Utils | 测试 UI 组件 |
| Hook 测试 | React Testing Library | 测试自定义 Hooks |
| E2E 测试 | Cypress / Playwright | 端到端测试 |
## 最佳实践
1. **使用 MSW**:推荐用于集成测试,更接近真实环境
2. **分离关注点**:测试逻辑和 UI 分离
3. **清理副作用**:每次测试后清理 mocks
4. **测试错误场景**:不仅要测试成功,也要测试失败
5. **避免真实请求**:测试时不应发送真实 HTTP 请求
6. **使用数据属性**:使用 data-testid 选择元素
7. **保持测试独立**:每个测试应该独立运行
服务端 · 3月6日 23:02
axios 的版本更新历史中有哪些重要变化?如何处理不同版本的兼容性问题Axios 从 0.x 版本发展到 1.x 版本,经历了多次重大更新,了解这些变化对于维护项目和升级非常重要。
## 1. 版本更新历史概览
### 主要版本里程碑
```
┌─────────────────────────────────────────────────────────────────┐
│ Axios 版本演进时间线 │
├─────────────────────────────────────────────────────────────────┤
│ 2014 │ v0.1.0 │ 初始发布,基于 Promise 的 HTTP 客户端 │
│ 2015 │ v0.9.0 │ 添加拦截器功能 │
│ 2016 │ v0.12.0 │ 添加取消请求功能 (CancelToken) │
│ 2017 │ v0.16.0 │ 支持 async/await │
│ 2018 │ v0.18.0 │ 安全更新,修复 XSS 漏洞 │
│ 2019 │ v0.19.0 │ 改进错误处理,添加 validateStatus │
│ 2020 │ v0.20.0 │ 支持 TypeScript 类型改进 │
│ 2020 │ v0.21.0 │ 重大安全更新 │
│ 2022 │ v1.0.0 │ 正式发布 1.0 版本,Promise 化改进 │
│ 2023 │ v1.6.0 │ 支持 Fetch API 适配器 │
└─────────────────────────────────────────────────────────────────┘
```
## 2. 重要版本变更详解
### v0.19.0 重大变更
```javascript
// v0.19.0 之前 - 错误处理
axios.get('/user')
.catch(error => {
// 404 错误也会进入 catch
if (error.response) {
// 服务器响应了错误状态码
}
});
// v0.19.0 之后 - 引入 validateStatus
axios.get('/user', {
validateStatus: function (status) {
// 默认:status >= 200 && status < 300
return status < 500; // 只有 500+ 才抛出错误
}
});
// 自定义错误处理
const instance = axios.create({
validateStatus: (status) => {
return status >= 200 && status < 300; // 默认值
}
});
```
### v0.20.0 TypeScript 改进
```typescript
// v0.20.0 之前的类型定义
import axios from 'axios';
// 类型定义不够完善
axios.get('/user').then(response => {
// response.data 类型为 any
});
// v0.20.0 之后的改进
import axios, { AxiosResponse } from 'axios';
interface User {
id: number;
name: string;
}
// 泛型支持
axios.get<User>('/user').then((response: AxiosResponse<User>) => {
// response.data 类型为 User
const user: User = response.data;
});
// 请求配置类型
import { AxiosRequestConfig } from 'axios';
const config: AxiosRequestConfig = {
url: '/user',
method: 'get',
headers: {
'Content-Type': 'application/json'
}
};
```
### v1.0.0 重大变更
```javascript
// v1.0.0 之前 - CancelToken (已废弃)
import axios from 'axios';
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user', {
cancelToken: source.token
});
source.cancel('Operation canceled');
// v1.0.0 之后 - AbortController (推荐)
const controller = new AbortController();
axios.get('/user', {
signal: controller.signal
});
controller.abort('Operation canceled');
// 兼容性处理 - 同时支持两种方式
function makeRequest(url, cancelTokenOrSignal) {
const config = {};
if (cancelTokenOrSignal instanceof AbortSignal) {
config.signal = cancelTokenOrSignal;
} else {
config.cancelToken = cancelTokenOrSignal;
}
return axios.get(url, config);
}
```
### v1.6.0 Fetch API 适配器
```javascript
// v1.6.0 引入 Fetch API 适配器
import axios from 'axios';
// 使用 Fetch API 适配器
const instance = axios.create({
adapter: 'fetch' // 或 require('axios/adapters/fetch')
});
// 传统 XHR 适配器(默认)
const xhrInstance = axios.create({
adapter: 'http' // 或 require('axios/adapters/xhr')
});
// 条件选择适配器
const instance = axios.create({
adapter: typeof window !== 'undefined' && 'fetch' in window
? 'fetch'
: 'xhr'
});
```
## 3. 兼容性问题与解决方案
### 浏览器兼容性
```javascript
// 检查浏览器兼容性
function checkAxiosCompatibility() {
const issues = [];
// 检查 Promise 支持
if (typeof Promise === 'undefined') {
issues.push('Promise not supported');
}
// 检查 XMLHttpRequest
if (typeof XMLHttpRequest === 'undefined') {
issues.push('XMLHttpRequest not supported');
}
// 检查 Fetch API (如果使用 fetch 适配器)
if (typeof fetch === 'undefined') {
issues.push('Fetch API not supported (optional)');
}
return issues;
}
// Polyfill 方案
// 1. Promise Polyfill
import 'es6-promise/auto';
// 2. Fetch API Polyfill
import 'whatwg-fetch';
// 3. 完整兼容性处理
import axios from 'axios';
// 配置适配器回退
if (typeof XMLHttpRequest === 'undefined') {
// Node.js 环境
axios.defaults.adapter = require('axios/lib/adapters/http');
}
```
### Node.js 版本兼容性
```javascript
// package.json 中的引擎要求
{
"engines": {
"node": ">=12.0.0"
}
}
// 运行时检查
const semver = require('semver');
const nodeVersion = process.version;
if (!semver.satisfies(nodeVersion, '>=12.0.0')) {
console.warn(`Axios requires Node.js >= 12.0.0, current: ${nodeVersion}`);
}
// 不同 Node 版本的适配
const http = require('http');
const https = require('https');
const instance = axios.create({
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),
// Node.js 12+ 支持
maxBodyLength: Infinity,
maxContentLength: Infinity
});
```
### 版本检测与适配
```javascript
// axios-version-compat.js
import axios from 'axios';
// 获取 axios 版本
const axiosVersion = axios.VERSION;
// 版本比较工具
function compareVersions(v1, v2) {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const p1 = parts1[i] || 0;
const p2 = parts2[i] || 0;
if (p1 > p2) return 1;
if (p1 < p2) return -1;
}
return 0;
}
// 根据版本选择 API
export function createCompatibleInstance(config = {}) {
const isV1 = compareVersions(axiosVersion, '1.0.0') >= 0;
const instance = axios.create({
...config,
// v1.0.0+ 的默认配置
...(isV1 && {
transitional: {
clarifyTimeoutError: true
}
})
});
// 版本特定的拦截器
if (isV1) {
// v1.x 的拦截器
instance.interceptors.request.use(
(config) => {
// v1.x 特有的处理
return config;
},
(error) => Promise.reject(error)
);
} else {
// v0.x 的拦截器
instance.interceptors.request.use(
(config) => config,
(error) => Promise.reject(error)
);
}
return instance;
}
// 取消请求的兼容封装
export function createCancelToken() {
const isV1 = compareVersions(axiosVersion, '1.0.0') >= 0;
if (isV1) {
// v1.x 使用 AbortController
const controller = new AbortController();
return {
token: controller.signal,
cancel: (message) => controller.abort(message)
};
} else {
// v0.x 使用 CancelToken
const source = axios.CancelToken.source();
return source;
}
}
```
## 4. 升级指南
### 从 v0.x 升级到 v1.x
```javascript
// 升级检查清单
const upgradeChecklist = {
// 1. 检查 CancelToken 使用
checkCancelToken: () => {
// 替换为 AbortController
// 旧代码
const source = axios.CancelToken.source();
// 新代码
const controller = new AbortController();
},
// 2. 检查错误处理
checkErrorHandling: () => {
// 确保 validateStatus 配置正确
axios.defaults.validateStatus = (status) => {
return status >= 200 && status < 300;
};
},
// 3. 检查 TypeScript 类型
checkTypeScript: () => {
// 更新类型导入
// import { AxiosResponse } from 'axios';
},
// 4. 检查适配器配置
checkAdapter: () => {
// 如果使用自定义适配器,需要更新
}
};
// 自动升级脚本
function migrateAxiosCode(code) {
// 替换 CancelToken
code = code.replace(
/axios\.CancelToken\.source\(\)/g,
'new AbortController()'
);
// 替换 cancelToken 配置
code = code.replace(
/cancelToken:\s*source\.token/g,
'signal: controller.signal'
);
// 替换 cancel 调用
code = code.replace(
/source\.cancel\(/g,
'controller.abort('
);
return code;
}
```
### 依赖版本锁定
```json
// package.json - 锁定版本
{
"dependencies": {
"axios": "^1.6.0"
},
"devDependencies": {
"@types/axios": "^0.14.0"
},
"resolutions": {
"axios": "1.6.0"
}
}
// package-lock.json / yarn.lock
// 确保锁定文件提交到版本控制
```
## 5. 版本兼容性测试
```javascript
// __tests__/axios-compat.test.js
import axios from 'axios';
describe('Axios Version Compatibility', () => {
test('should have correct version', () => {
expect(axios.VERSION).toBeDefined();
expect(axios.VERSION).toMatch(/^\d+\.\d+\.\d+/);
});
test('should support AbortController in v1.x', () => {
const [major] = axios.VERSION.split('.').map(Number);
if (major >= 1) {
const controller = new AbortController();
expect(() => {
axios.get('/test', { signal: controller.signal });
}).not.toThrow();
}
});
test('should support legacy CancelToken', () => {
if (axios.CancelToken) {
const source = axios.CancelToken.source();
expect(source.token).toBeDefined();
expect(typeof source.cancel).toBe('function');
}
});
test('should handle errors consistently', async () => {
try {
await axios.get('http://invalid-domain-that-does-not-exist.com');
} catch (error) {
expect(error).toBeDefined();
expect(error.message).toBeDefined();
}
});
});
```
## 6. 最佳实践
### 版本管理策略
```javascript
// 1. 使用固定版本
// package.json
{
"dependencies": {
"axios": "1.6.0" // 不使用 ^ 或 ~
}
}
// 2. 封装 axios,隔离版本影响
// api/client.js
import axios from 'axios';
const apiClient = axios.create({
baseURL: process.env.API_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// 封装请求方法,隐藏 axios 细节
export const api = {
get: (url, config) => apiClient.get(url, config),
post: (url, data, config) => apiClient.post(url, data, config),
// ... 其他方法
};
// 3. 定期更新策略
// 创建更新脚本 scripts/update-axios.js
const { execSync } = require('child_process');
const axios = require('axios/package.json');
console.log(`Current axios version: ${axios.version}`);
// 检查最新版本
fetch('https://registry.npmjs.org/axios')
.then(res => res.json())
.then(data => {
const latest = data['dist-tags'].latest;
console.log(`Latest axios version: ${latest}`);
if (latest !== axios.version) {
console.log('Update available. Run: npm update axios');
}
});
```
### 兼容性配置模板
```javascript
// config/axios.js
import axios from 'axios';
// 检测环境
const isBrowser = typeof window !== 'undefined';
const isNode = !isBrowser;
const axiosVersion = axios.VERSION;
// 基础配置
const baseConfig = {
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
};
// 环境特定配置
const envConfig = isNode ? {
// Node.js 配置
httpAgent: new (require('http').Agent)({ keepAlive: true }),
httpsAgent: new (require('https').Agent)({ keepAlive: true })
} : {
// 浏览器配置
withCredentials: true,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN'
};
// 版本特定配置
const versionConfig = axiosVersion.startsWith('1.') ? {
// v1.x 配置
transitional: {
clarifyTimeoutError: true,
forcedJSONParsing: true
}
} : {
// v0.x 配置
};
// 创建实例
const instance = axios.create({
...baseConfig,
...envConfig,
...versionConfig
});
export default instance;
```
## 版本兼容性速查表
| 特性 | v0.18.x | v0.19.x | v0.20.x | v0.21.x | v1.0.0+ |
| --------------- | ------- | ------- | ------- | ------- | ------- |
| CancelToken | ✅ | ✅ | ✅ | ✅ | ⚠️ 废弃 |
| AbortController | ❌ | ❌ | ❌ | ❌ | ✅ |
| Fetch 适配器 | ❌ | ❌ | ❌ | ❌ | ✅ |
| validateStatus | ✅ | ✅ 改进 | ✅ | ✅ | ✅ |
| TypeScript | ⚠️ | ⚠️ | ✅ 改进 | ✅ | ✅ |
| ESM 支持 | ⚠️ | ⚠️ | ⚠️ | ✅ | ✅ |
| 安全修复 | ✅ | ✅ | ✅ | ✅ | ✅ |
## 总结
1. **版本选择**:新项目建议使用 v1.6.0+,旧项目逐步迁移
2. **兼容性处理**:封装 axios 使用,隔离版本差异
3. **升级策略**:先测试后升级,使用锁定文件
4. **API 选择**:优先使用新标准 (AbortController)
5. **监控更新**:关注安全更新和重大变更
服务端 · 3月6日 23:01