<think>我们使用JavaScript(而不是TypeScript)来二次封装Axios,以便在不支持TypeScript的环境中使用。封装目标:1.设置基础URL和超时时间2.请求拦截器:例如添加token等认证信息3.响应拦截器:统一处理响应数据和错误4.封装常用的HTTP方法(GET, POST, PUT, DELETE等)5.支持取消请求6.支持文件上传和下载我们将创建一个名为`http`的实例,并添加上述功能。代码结构:1.创建axios实例2.请求拦截器3.响应拦截器4.封装方法5.取消请求6.文件上传和下载注意:由于是JavaScript,我们不使用类型注解。</think>### 使用 JavaScript 二次封装 Axios
以下是完整的 Axios 二次封装方案,包含基础封装、拦截器管理、错误处理、请求取消等高级功能:
```javascript
// api/http.js
import axios from 'axios';
// 创建 Axios 实例
const createAxiosInstance = (baseURL) => {
const instance = axios.create({
baseURL,
timeout: 30000, // 30秒超时
headers: {
'Content-Type': 'application/json',
},
withCredentials: true, // 携带凭证
});
// 请求拦截器
instance.interceptors.request.use(
(config) => {
// 添加认证令牌
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 显示加载提示
if (config.showLoading) {
console.log('[API] 请求开始:', config.url);
// 实际项目中可触发全局加载状态
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
instance.interceptors.response.use(
(response) => {
// 隐藏加载提示
if (response.config.showLoading) {
console.log('[API] 请求完成:', response.config.url);
}
// 处理标准响应格式
if (response.data && typeof response.data === 'object') {
return response.data;
}
return response;
},
(error) => {
// 隐藏加载提示
if (error.config?.showLoading) {
console.log('[API] 请求失败:', error.config.url);
}
// 自定义错误处理
if (typeof error.config?.errorHandler === 'function') {
error.config.errorHandler(error);
return Promise.reject(error);
}
// 默认错误处理
return handleError(error);
}
);
return instance;
};
// 全局错误处理
const handleError = (error) => {
if (error.response) {
// 服务器返回错误 (4xx, 5xx)
const { status, data } = error.response;
const message = data?.message || `HTTP 错误 ${status}`;
switch (status) {
case 401:
console.error('[API] 认证失败:', message);
// 触发重新登录逻辑
break;
case 403:
console.error('[API] 权限不足:', message);
break;
case 404:
console.error('[API] 资源不存在:', message);
break;
case 500:
console.error('[API] 服务器错误:', message);
break;
default:
console.error(`[API] 请求错误 (${status}):`, message);
}
} else if (error.request) {
// 请求已发送但无响应
console.error('[API] 无服务器响应:', error.message);
} else {
// 请求配置错误
console.error('[API] 请求配置错误:', error.message);
}
return Promise.reject(error);
};
// 创建 API 实例
const baseURL = process.env.VUE_APP_API_BASE_URL || '/api';
export const http = createAxiosInstance(baseURL);
// 封装请求方法
export const get = (url, config = {}) => http.get(url, config);
export const post = (url, data, config = {}) => http.post(url, data, config);
export const put = (url, data, config = {}) => http.put(url, data, config);
export const del = (url, config = {}) => http.delete(url, config);
export const patch = (url, data, config = {}) => http.patch(url, data, config);
// 文件上传
export const uploadFile = (url, file, fieldName = 'file', config = {}) => {
const formData = new FormData();
formData.append(fieldName, file);
return http.post(url, formData, {
...config,
headers: {
'Content-Type': 'multipart/form-data',
},
});
};
// 文件下载
export const downloadFile = (url, filename, config = {}) => {
return http
.get(url, {
...config,
responseType: 'blob',
})
.then((response) => {
const blob = new Blob([response.data]);
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
});
};
// 请求取消功能
const cancelTokenMap = new Map();
export const createCancelToken = (key) => {
// 如果已有相同key的请求,先取消
cancelRequest(key);
const source = axios.CancelToken.source();
cancelTokenMap.set(key, source);
return source.token;
};
export const cancelRequest = (key, message = '请求被取消') => {
const source = cancelTokenMap.get(key);
if (source) {
source.cancel(message);
cancelTokenMap.delete(key);
return true;
}
return false;
};
export const cancelAllRequests = (message = '所有请求被取消') => {
cancelTokenMap.forEach((source) => {
source.cancel(message);
});
cancelTokenMap.clear();
};
// 自动重试功能
export const setupRetryInterceptor = (instance, options = {}) => {
const defaultOptions = {
retries: 3,
retryDelay: 1000,
retryCondition: (error) => {
// 仅对网络错误和超时重试
return !error.response || error.code === 'ECONNABORTED';
},
};
const mergedOptions = { ...defaultOptions, ...options };
instance.interceptors.response.use(null, async (error) => {
const { config } = error;
// 检查是否需要重试
if (!config || !mergedOptions.retryCondition(error)) {
return Promise.reject(error);
}
// 初始化重试计数器
config.__retryCount = config.__retryCount || 0;
// 检查是否达到最大重试次数
if (config.__retryCount >= mergedOptions.retries) {
return Promise.reject(error);
}
// 增加重试计数器
config.__retryCount += 1;
// 创建重试延迟
const delay = new Promise((resolve) => {
setTimeout(() => resolve(true), mergedOptions.retryDelay);
});
// 等待延迟后重试
return delay.then(() => instance(config));
});
};
// 初始化时添加重试拦截器
setupRetryInterceptor(http, { retries: 2 });
```
### 使用示例
#### 1. 基本请求
```javascript
import { get, post } from './api/http';
// GET 请求
get('/users')
.then(response => console.log('用户列表:', response.data))
.catch(error => console.error('获取用户失败:', error));
// POST 请求
post('/login', { username: 'admin', password: 'password' }, {
showLoading: true // 显示加载提示
})
.then(response => console.log('登录成功:', response.data))
.catch(error => console.error('登录失败:', error));
```
#### 2. 文件上传
```javascript
import { uploadFile } from './api/http';
const fileInput = document.getElementById('file-input');
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
uploadFile('/upload', file, 'document', {
showLoading: true,
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(`上传进度: ${percentCompleted}%`);
}
})
.then(response => console.log('上传成功:', response.data))
.catch(error => console.error('上传失败:', error));
}
});
```
#### 3. 请求取消
```javascript
import { get, createCancelToken, cancelRequest } from './api/http';
// 发送可取消的请求
function searchProducts(query) {
return get('/products/search', {
params: { q: query },
cancelToken: createCancelToken('productSearch')
});
}
// 用户输入时触发搜索
const searchInput = document.getElementById('search-input');
let searchTimeout;
searchInput.addEventListener('input', (event) => {
const query = event.target.value.trim();
// 取消之前的请求
cancelRequest('productSearch');
if (query.length > 2) {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
searchProducts(query)
.then(response => displayResults(response.data))
.catch(error => {
if (!axios.isCancel(error)) {
console.error('搜索失败:', error);
}
});
}, 300);
}
});
```
### 高级功能:JWT 令牌自动刷新
```javascript
// 在 http.js 中添加
let isRefreshing = false;
let refreshSubscribers = [];
// 添加响应拦截器处理 401 错误
http.interceptors.response.use(null, (error) => {
const { config, response } = error;
if (response && response.status === 401 && !config._retry) {
// 标记请求已重试
config._retry = true;
if (!isRefreshing) {
isRefreshing = true;
// 刷新令牌
return http.post('/auth/refresh')
.then((res) => {
const newToken = res.data.token;
localStorage.setItem('access_token', newToken);
// 重试所有挂起的请求
refreshSubscribers.forEach((cb) => cb(newToken));
refreshSubscribers = [];
// 重试原始请求
config.headers.Authorization = `Bearer ${newToken}`;
return http(config);
})
.catch((refreshError) => {
// 刷新失败,跳转到登录页
console.error('令牌刷新失败:', refreshError);
window.location.href = '/login';
return Promise.reject(refreshError);
})
.finally(() => {
isRefreshing = false;
});
} else {
// 将请求加入等待队列
return new Promise((resolve) => {
refreshSubscribers.push((token) => {
config.headers.Authorization = `Bearer ${token}`;
resolve(http(config));
});
});
}
}
return Promise.reject(error);
});
```
### 完整 API 服务层示例
```javascript
// api/services/authService.js
import { post, get } from '../http';
export default {
/**
* 用户登录
* @param {Object} credentials - 登录凭证
* @param {string} credentials.username - 用户名
* @param {string} credentials.password - 密码
* @returns {Promise} 登录结果
*/
login(credentials) {
return post('/auth/login', credentials, {
showLoading: true,
skipErrorHandler: true // 跳过全局错误处理
});
},
/**
* 获取当前用户信息
* @returns {Promise} 用户信息
*/
getCurrentUser() {
return get('/auth/me', {
showLoading: true
});
},
/**
* 用户登出
* @returns {Promise} 登出结果
*/
logout() {
return post('/auth/logout');
},
/**
* 刷新访问令牌
* @returns {Promise} 新的访问令牌
*/
refreshToken() {
return post('/auth/refresh', null, {
skipErrorHandler: true // 跳过全局错误处理
});
}
};
```
### 在 Vue/React 中使用
#### Vue 示例
```javascript
import { createApp } from 'vue';
import App from './App.vue';
import http from './api/http';
const app = createApp(App);
app.config.globalProperties.$http = http;
// 或者使用 provide/inject
app.provide('http', http);
app.mount('#app');
// 在组件中使用
export default {
methods: {
async fetchUsers() {
try {
const response = await this.$http.get('/users');
this.users = response.data;
} catch (error) {
console.error('获取用户失败:', error);
}
}
}
}
```
#### React 示例
```javascript
// 创建 Context
import React from 'react';
import http from './api/http';
export const HttpContext = React.createContext(http);
// 在 App 中提供
function App() {
return (
<HttpContext.Provider value={http}>
{/* 应用组件 */}
</HttpContext.Provider>
);
}
// 在组件中使用
import { useContext } from 'react';
import { HttpContext } from './HttpContext';
function UserList() {
const http = useContext(HttpContext);
const [users, setUsers] = React.useState([]);
React.useEffect(() => {
const fetchUsers = async () => {
try {
const response = await http.get('/users');
setUsers(response.data);
} catch (error) {
console.error('获取用户失败:', error);
}
};
fetchUsers();
}, []);
return (
<div>
{/* 渲染用户列表 */}
</div>
);
}
```
### 最佳实践建议
1. **环境配置**:
```javascript
// .env.development
VUE_APP_API_BASE_URL=http://localhost:3000/api
// .env.production
VUE_APP_API_BASE_URL=https://api.yourdomain.com/v1
```
2. **错误处理增强**:
```javascript
// 全局捕获未处理的 Promise 错误
window.addEventListener('unhandledrejection', (event) => {
const error = event.reason;
if (error.isAxiosError) {
// 显示全局错误通知
showErrorNotification(error.message);
event.preventDefault();
}
});
```
3. **性能监控**:
```javascript
// 在请求拦截器中
http.interceptors.request.use((config) => {
config.metadata = { startTime: Date.now() };
return config;
});
// 在响应拦截器中
http.interceptors.response.use((response) => {
const endTime = Date.now();
const duration = endTime - response.config.metadata.startTime;
console.log(`请求 ${response.config.url} 耗时 ${duration}ms`);
return response;
});
```
4. **安全增强**:
```javascript
// 防止 CSRF 攻击
http.interceptors.request.use((config) => {
const csrfToken = getCSRFToken(); // 从 cookie 或 meta 标签获取
if (csrfToken) {
config.headers['X-CSRF-Token'] = csrfToken;
}
return config;
});
```
### 功能总结
1. **核心功能**:
- 基础请求封装(GET/POST/PUT/DELETE)
- 请求/响应拦截器
- 统一错误处理
- 文件上传/下载
- 请求取消
2. **高级功能**:
- JWT 令牌自动刷新
- 请求重试机制
- 全局加载状态管理
- 性能监控
- 安全增强
3. **架构优势**:
- 模块化设计
- 服务层分离
- 全局配置管理
- 跨框架兼容(Vue/React/纯JS)