如何优雅地封装 Axios
概述
在前端面试中,"如何封装 Axios" 是一个命中率极高的问题。作为面试官,这个问题考察的是候选人的多项能力:
- 代码设计能力:能否写出可维护、可复用、高内聚低耦合的代码
- 项目工程化思维:是否考虑不同环境、统一错误处理、请求/响应拦截等真实项目场景
- 工具理解深度:是否深入理解 Axios 的核心功能,如实例、拦截器等
封装目的与价值
为什么要封装 Axios
封装 Axios 的目的,并不是重复造轮子,而是为了解决前端项目中的实际问题,让代码更优雅、更健壮:
- 代码复用:统一管理重复配置(baseURL、timeout),避免重复编写
- 提升可维护性:API 基础配置变更时,只需修改一处,所有请求生效
- 统一处理:提供统一的请求拦截(Token 注入)和响应拦截(数据结构处理、错误处理)
- 环境隔离:根据开发、测试、生产环境自动切换不同 API 地址
- API 管理:集中管理所有 API 请求函数,便于查找、复用和维护
核心实现方案
1. 创建 Axios 实例并配置多环境支持
使用 axios.create 创建独立实例,避免全局配置污染,支持为不同业务模块创建不同实例:
javascript
// src/utils/request.js
import axios from 'axios';
// 根据环境切换 baseURL
const getBaseURL = () => {
if (process.env.NODE_ENV === 'production') {
return 'https://api.prod.com';
} else if (process.env.NODE_ENV === 'development') {
return 'https://api.dev.com';
} else {
// test 环境
return 'https://api.test.com';
}
};
// 创建 Axios 实例
const service = axios.create({
baseURL: getBaseURL(), // API 的 base_url
timeout: 10000, // 请求超时时间
});
export default service;2. 请求拦截器设置
请求拦截器是发送请求前的钩子,主要用于注入认证信息和添加请求 loading:
javascript
// 设置请求拦截器
service.interceptors.request.use(
(config) => {
// 添加认证 token
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
// 添加全局 loading(可选)
// showLoading();
return config;
},
(error) => {
console.error('Request Error:', error);
return Promise.reject(error);
}
);3. 响应拦截器设置
响应拦截器是封装的精髓,用于数据结构解包、统一错误处理和关闭 loading:
javascript
// 设置响应拦截器
service.interceptors.response.use(
(response) => {
// 关闭 loading
// hideLoading();
const res = response.data;
// 假设后端返回格式:{ code: 0, data: {...}, message: 'success' }
if (res.code !== 0) {
// 业务错误处理
Message.error(res.message || 'Error');
// 处理特殊错误码(如 Token 失效)
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// 统一处理登录失效
// 可以弹窗确认后跳转登录页
}
return Promise.reject(new Error(res.message || 'Error'));
} else {
// 直接返回核心数据
return res.data;
}
},
(error) => {
// HTTP 网络错误处理
// hideLoading();
console.error('Response Error:', error);
let message = '';
if (error && error.response) {
switch (error.response.status) {
case 400: message = '请求错误(400)'; break;
case 401: message = '未授权,请重新登录(401)'; break;
case 403: message = '拒绝访问(403)'; break;
case 404: message = '请求地址出错(404)'; break;
case 408: message = '请求超时(408)'; break;
case 500: message = '服务器内部错误(500)'; break;
case 501: message = '服务未实现(501)'; break;
case 502: message = '网关错误(502)'; break;
case 503: message = '服务不可用(503)'; break;
case 504: message = '网关超时(504)'; break;
case 505: message = 'HTTP版本不受支持(505)'; break;
default: message = `连接出错(${error.response.status})!`;
}
} else {
message = '连接服务器失败!';
}
Message.error(message);
return Promise.reject(error);
}
);API 模块化管理
创建 API 模块
完成基础封装后,创建 api 目录统一管理所有接口请求函数:
javascript
// src/api/user.js
import request from '@/utils/request';
// 登录接口
export function login(data) {
return request({
url: '/user/login',
method: 'post',
data,
});
}
// 获取用户信息接口
export function getUserInfo() {
return request({
url: '/user/info',
method: 'get',
});
}
// 修改用户信息
export function updateUserInfo(data) {
return request({
url: '/user/info',
method: 'put',
data,
});
}在组件中使用
javascript
// src/views/Login.vue
import { login } from '@/api/user';
export default {
methods: {
async handleLogin() {
try {
const loginData = { username: 'admin', password: '123' };
// 响应拦截器已处理数据解包,直接获取 res.data
const response = await login(loginData);
// 处理登录成功逻辑
console.log(response);
this.$router.push('/dashboard');
} catch (error) {
// 响应拦截器已统一处理错误提示
// 这里只需处理登录失败的业务逻辑
console.error('登录失败:', error);
}
},
},
};高级封装功能
请求取消
javascript
// src/utils/request.js
import axios from 'axios';
// 创建取消令牌源
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
// 在请求配置中添加取消令牌
const service = axios.create({
// ... 其他配置
cancelToken: source.token
});
// 取消请求的方法
export const cancelRequest = (message = '操作被取消') => {
source.cancel(message);
};请求重试机制
javascript
// 添加请求重试
service.interceptors.response.use(
(response) => response,
(error) => {
const config = error.config;
// 如果没有设置重试次数,默认为0
if (!config || !config.retry) {
config.retry = 0;
}
// 检查是否已达到最大重试次数
if (config.retry >= 3) {
return Promise.reject(error);
}
// 增加重试次数
config.retry += 1;
// 创建新的Promise来处理重试
return new Promise((resolve) => {
setTimeout(() => {
resolve(service(config));
}, 1000); // 1秒后重试
});
}
);请求和响应数据转换
javascript
// 请求数据转换
service.defaults.transformRequest = [function (data, headers) {
// 对发送的 data 进行任意转换处理
if (data instanceof FormData) {
return data;
}
// 转换为 JSON 字符串
if (typeof data === 'object') {
headers['Content-Type'] = 'application/json';
return JSON.stringify(data);
}
return data;
}];
// 响应数据转换
service.defaults.transformResponse = [function (data) {
// 对接收的 data 进行任意转换处理
try {
return JSON.parse(data);
} catch (error) {
return data;
}
}];最佳实践建议
1. 错误处理策略
- 分层处理:网络错误在拦截器统一处理,业务错误在具体调用处处理
- 用户友好:提供清晰的错误提示信息
- 日志记录:开发环境详细记录,生产环境关键信息记录
2. 性能优化
- 请求去重:防止短时间内重复请求
- 缓存策略:对于不常变化的数据进行缓存
- 分片上传:大文件上传时的处理方案
3. 安全考虑
- Token 刷新:自动处理 Token 过期和刷新
- CSRF 防护:添加 CSRF Token
- 敏感信息:避免在请求中暴露敏感信息
总结
一个优秀的 Axios 封装应该具备以下特点:
- 完整性:涵盖请求/响应拦截、错误处理、环境配置等核心功能
- 可扩展性:支持自定义配置,能够适应不同业务场景
- 健壮性:具备错误处理、重试机制等容错能力
- 易用性:API 设计简洁明了,使用方便
- 可维护性:代码结构清晰,便于后续维护和升级
通过系统性的封装,不仅能够提升开发效率,还能确保项目的稳定性和可维护性。这正是工程化思维在实际项目中的体现。