Skip to content

如何优雅地封装 Axios

概述

在前端面试中,"如何封装 Axios" 是一个命中率极高的问题。作为面试官,这个问题考察的是候选人的多项能力:

  • 代码设计能力:能否写出可维护、可复用、高内聚低耦合的代码
  • 项目工程化思维:是否考虑不同环境、统一错误处理、请求/响应拦截等真实项目场景
  • 工具理解深度:是否深入理解 Axios 的核心功能,如实例、拦截器等

封装目的与价值

为什么要封装 Axios

封装 Axios 的目的,并不是重复造轮子,而是为了解决前端项目中的实际问题,让代码更优雅、更健壮:

  1. 代码复用:统一管理重复配置(baseURL、timeout),避免重复编写
  2. 提升可维护性:API 基础配置变更时,只需修改一处,所有请求生效
  3. 统一处理:提供统一的请求拦截(Token 注入)和响应拦截(数据结构处理、错误处理)
  4. 环境隔离:根据开发、测试、生产环境自动切换不同 API 地址
  5. 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 封装应该具备以下特点:

  1. 完整性:涵盖请求/响应拦截、错误处理、环境配置等核心功能
  2. 可扩展性:支持自定义配置,能够适应不同业务场景
  3. 健壮性:具备错误处理、重试机制等容错能力
  4. 易用性:API 设计简洁明了,使用方便
  5. 可维护性:代码结构清晰,便于后续维护和升级

通过系统性的封装,不仅能够提升开发效率,还能确保项目的稳定性和可维护性。这正是工程化思维在实际项目中的体现。

基于 VitePress 构建