基于适配器模式的 Axios 封装实践

基于适配器模式的 Axios 封装实践

背景

前端项目经常会对接多个后端服务(当然是那种多人开发的不规范返回结构的服务或者是新前端项目需要集成多个老项目后端结构🤮🤮🤮),而这些服务的返回格式往往并不统一。

有的返回 { code, message, data },有的返回 { success, result, errorMsg },还有的直接把数据裸返。

面对这种不一致,如果不对请求层做抽象,业务代码中会充斥着各种格式判断和重复转换,后期维护成本极高。

本文介绍一种基于适配器模式的 Axios 二次封装方案,通过引入响应适配器将各类后端返回统一为标准格式,并结合集中错误处理,让业务代码只面向一种数据结构。

核心设计:引入适配器模式

定义统一的业务响应标准 StandardResponse 和一个适配器接口 ResponseAdapter

每个适配器负责将 AxiosResponse 转换为标准格式。默认适配器按最常见的 { code, message, data } 工作,而对于结构特殊的接口,可以通过配置传入自定义适配器。

ts 复制代码
// 标准响应结构
interface StandardResponse {
  success: boolean;
  code: number | string;
  message: string;
  data: any;
}

// 适配器接口
interface ResponseAdapter {
  transform(response: AxiosResponse): StandardResponse;
}

例如,某个接口返回 { success, result, errorMsg },只需编写一个适配器:

ts 复制代码
class CustomAdapter implements ResponseAdapter {
  transform(res: AxiosResponse): StandardResponse {
    const json = res.data;
    return {
      success: json.success,
      code: json.success ? 0 : -1,
      message: json.errorMsg || '',
      data: json.result,
    };
  }
}

// 使用时传入配置
httpClient.get('/api/special', { responseAdapter: new CustomAdapter() });

适配器把差异隔离在内部,业务层始终拿到的是统一的 StandardResponse

完整代码

以下是完整封装代码,保留了核心结构。

1. 类型定义 (types.ts)

ts 复制代码
import type { AxiosRequestConfig, AxiosResponse } from 'axios';

// 适配器接口,新实现的适配器都需要实现该接口
export interface ResponseAdapter {
  transform<T = any>(response: AxiosResponse): StandardResponse<T>;
}

// 统一数据格式,适配器返回的就是该类型数据
export interface StandardResponse<T = any> {
  success: boolean;
  code: number | string;
  message: string;
  data: T;
  [key: string]: any;
}

export interface RequestConfig extends AxiosRequestConfig {
  /** 单次请求使用的响应适配器,优先级高于默认适配器 */
  responseAdapter?: ResponseAdapter;
  /** 是否直接返回原始 AxiosResponse(跳过业务提取) */
  returnRawResponse?: boolean;
}

2. 自定义错误类 (AppError.ts)

ts 复制代码
export class AppError extends Error {
  public code: number | string;
  public data?: any;

  constructor(message: string, code: number | string, data?: any) {
    super(message);
    this.name = 'AppError';
    this.code = code;
    this.data = data;
  }
}

3. 响应工具函数 (responseUtils.ts)

ts 复制代码
/** 业务成功码阈值:业务码 <= 此值视为成功 
* 该工具只是应用于下面的默认适配器,没准不同服务传回的错误码限定范围也不一致呢~~~
*/
export const SUCCESS_CODE_THRESHOLD = 1000;

export const isSuccessCode = (code: number): boolean => code <= SUCCESS_CODE_THRESHOLD;

export const extractStandardFields = (raw: any) => ({
  code: raw?.code ?? -1,
  data: raw?.data ?? null,
  message: raw?.message ?? '',
});

4. 默认适配器 (DefaultAdapter)

ts 复制代码
import type { AxiosResponse } from 'axios';
import type { ResponseAdapter, StandardResponse } from './types';
import { extractStandardFields, isSuccessCode } from './responseUtils';

export class DefaultAdapter implements ResponseAdapter {
  transform<T = any>(response: AxiosResponse): StandardResponse<T> {
    const raw = response.data;
    const { code, data, message } = extractStandardFields(raw);

    return {
      ...raw, // 保留原始字段,方便某些场景下获取额外信息
      success: isSuccessCode(code),
      code,
      message,
      data,
    };
  }
}

5. 原始数据适配器 (RawDataAdapter)

对于直接返回数据、无需业务码判断的接口:

ts 复制代码
import type { AxiosResponse } from 'axios';
import type { ResponseAdapter, StandardResponse } from './types';

export class RawDataAdapter implements ResponseAdapter {
  transform<T = any>(response: AxiosResponse): StandardResponse<T> {
    return {
      success: true,
      code: response.status,
      message: '',
      data: response.data as T,
    };
  }
}

6. 核心 HttpClient 类 (HttpClient.ts)

ts 复制代码
import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios';
import type { RequestConfig, ResponseAdapter } from './types';
import { AppError } from './appError';
import { DefaultAdapter } from './adapters/default.adapter';

export class HttpClient {
  private instance: AxiosInstance;
  private readonly defaultAdapter: ResponseAdapter;

  constructor(baseConfig?: RequestConfig, defaultAdapter?: ResponseAdapter) {
    this.instance = axios.create(baseConfig);
    this.defaultAdapter = defaultAdapter || new DefaultAdapter();
    this._setupInterceptors();
  }

  private _setupInterceptors() {
    // ---------- 请求拦截器 ----------
    this.instance.interceptors.request.use(
      (config) => {
        // 示例:如何注入 Token
        // 将 getToken 替换为你的实际获取逻辑
        const token = getTokenFromSomewhere();
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      (error) => Promise.reject(error),
    );

    // ---------- 响应拦截器(只处理通用错误)----------
    this.instance.interceptors.response.use(
      (response: AxiosResponse) => response,
      (error: AxiosError) => {
        if (error.response) {
          const config = error.response.config as RequestConfig;
          const adapter = config.responseAdapter || this.defaultAdapter;

          if (error.response.status === 401) {
            // 这里可以触发全局登出逻辑,例如跳转登录页
          }

          try {
            const standard = adapter.transform(error.response);
            throw new AppError(
              standard.message || `请求错误 ${error.response.status}`,
              standard.code || error.response.status,
              standard.data,
            );
          } catch (e) {
            throw new AppError(error.message, error.response.status, error.response.data);
          }
        }

        if (error.request) {
          throw new AppError(error.message, 'NETWORK_ERROR');
        }

        throw new AppError(error.message, 'REQUEST_SETUP_ERROR');
      },
    );
  }

  async request<T = any>(config: RequestConfig): Promise<T> {
    try {
      const response = await this.instance.request<AxiosResponse>({ ...config });

      if (config.returnRawResponse) {
        return response as any;
      }

      const adapter = config.responseAdapter || this.defaultAdapter;
      const standard = adapter.transform(response);

      if (!standard.success) {
        throw new AppError(standard.message || '业务处理失败', standard.code, standard.data);
      }

      return standard as T;
    } catch (error) {
      if (error instanceof AppError) throw error;
      throw new AppError((error as Error).message, 'UNKNOWN_ERROR');
    }
  }

  // 便捷方法
  get<T = any>(url: string, params?: any, config?: RequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'GET', url, params });
  }

  post<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'POST', url, data });
  }

  put<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'PUT', url, data });
  }

  delete<T = any>(url: string, config?: RequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'DELETE', url });
  }

  patch<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'PATCH', url, data });
  }
}

/**
 * 创建 HttpClient 实例
 * @param baseConfig  基础配置(baseURL、timeout 等)
 * @param defaultAdapter 默认响应适配器,不传则使用 DefaultAdapter
 */
export const createHttpClient = (
  baseConfig?: RequestConfig,
  defaultAdapter?: ResponseAdapter,
): HttpClient => {
  return new HttpClient(baseConfig, defaultAdapter);
};

注: 上述代码中的 getTokenFromSomewhere() 需要你替换为项目实际的 Token 获取方式(如从 Pinia Store、Cookie、localStorage 中读取)。

使用示例

ts 复制代码
import { createHttpClient } from '@/infrastructure/http';
import type { LoginInfo, User } from '@/types';

// 使用默认适配器,无需再传入具体适配器
const api = createHttpClient({
  baseURL: '/api',
  timeout: 10000,
});

export const login = (loginInfo: LoginInfo) =>
  api.post('/user/login', loginInfo);

export const getUserInfo = () =>
  api.get('/user/me');

// 特殊接口使用自定义适配器,则传入具体的某一适配器实例
import { CustomAdapter } from '@/adapters/custom.adapter';
export const getSpecialData = () =>
  api.get('/special', {}, { responseAdapter: new CustomAdapter() });

调用方拿到的直接就是标准化结构:

ts 复制代码
const { success, data, message } = await login({ ... });

扩展使用

1. 自定义适配器

实现 ResponseAdapter 接口即可,适用于:

  • 后端返回格式与默认 { code, message, data } 不一致
  • 需要对原始响应做复杂计算
  • 嵌入额外的数据清洗逻辑

示例:处理 { status, payload, error } 结构

ts 复制代码
export class StatusPayloadAdapter implements ResponseAdapter {
  transform(response: AxiosResponse): StandardResponse {
    const { status, payload, error } = response.data;
    return {
      success: status === 'ok',
      code: status === 'ok' ? 0 : -1,
      message: error || '',
      data: payload,
    };
  }
}

使用时,可以全局替换默认适配器,也可以按需覆盖:

ts 复制代码
// 整个实例的默认适配器
const api = createHttpClient({ baseURL: '/v2' }, new StatusPayloadAdapter());

// 或者单个请求覆盖
api.get('/old-api', { responseAdapter: new DefaultAdapter() });

2. 扩展拦截器

当然,你可以在创建实例后动态追加拦截器,实现诸如请求 loading、重复请求取消等能力。

不过你需要改造 HttpClient 以暴露了内部的 axiosInstanceprivate设置为public readonly、给instance编写一个getter),然后使用 Axios 原生拦截器接口进行任意扩展。(作者并不建议你这么做,不过或许真有这样的需求。)

ts 复制代码
const http = createHttpClient({ baseURL: '/api' });

// 示例:添加请求 loading
http.instance.interceptors.request.use((config) => {
  showLoading();
  return config;
});

http.instance.interceptors.response.use(
  (res) => { hideLoading(); return res; },
  (err) => { hideLoading(); return Promise.reject(err); }
);

3. 返回原始响应

在极少数情况下(如下载文件、需要访问响应头),可以设置 returnRawResponse: true,此时方法直接返回原始 AxiosResponse,不会走适配器与成功判断。

ts 复制代码
const response = await http.get('/file', {}, { returnRawResponse: true });
console.log(response.headers['content-disposition']);

4. 对接多个不同的后端服务

通过创建多个 HttpClient 实例,每个实例配置不同的 baseURL 和默认适配器,可以轻松应对多个后端的差异。

ts 复制代码
const userApi = createHttpClient({ baseURL: 'https://user.service.com' }, new DefaultAdapter());
const orderApi = createHttpClient({ baseURL: 'https://order.service.com' }, new StatusPayloadAdapter());

// 业务代码中按需引用
const user = await userApi.get('/profile');
const orders = await orderApi.get('/list');

当然你也可以只使用一个实例,在具体接口使用的时候传入具体适配器:

ts 复制代码
const http = createHttpClient();

const user = await http.get('https://user.service.com/profile', new DefaultAdapter())
const orders = await http.get('https://order.service.com/list', new StatusPayloadAdapter())

5. 全局统一错误提示

实际项目中,你可以在响应拦截器中直接调用 UI 组件库的 toast 或 message 方法,避免在每个业务调用处重复写错误提示。但会与其他组件库产生耦合,还请自己考量之后使用!!!

ts 复制代码
// 在 _setupInterceptors 的 401 或其他错误处理中
if (error.response.status === 401) {
  message.error('登录已过期,请重新登录');
  router.push('/login');
}

也可以结合 AppError 在业务层做精细化提示。

6. 文件上传 / 下载等特殊配置

RequestConfig 继承自 AxiosRequestConfig,因此可以直接传入 onUploadProgressresponseType 等 Axios 原生配置,配合 returnRawResponse 处理下载场景。

ts 复制代码
await http.post('/upload', formData, {
  headers: { 'Content-Type': 'multipart/form-data' },
  onUploadProgress: (e) => console.log(e.progress),
});

const blobRes = await http.get('/download', {}, {
  responseType: 'blob',
  returnRawResponse: true,
});
相关推荐
Java面试题总结1 小时前
【设计模式03】使用模版模式+责任链模式优化实战
设计模式·责任链模式
庞轩px1 小时前
Redis工具类重构——从臃肿到优雅的门面模式实践
数据库·redis·设计模式·重构·门面模式·可扩展性·可维护性
Supersist16 小时前
【设计模式03】使用模版模式+责任链模式优化实战
后端·设计模式·代码规范
烛衔溟17 小时前
TypeScript 接口继承与混合类型
linux·ubuntu·typescript
送鱼的老默17 小时前
学习笔记--入门typescript直接案例开搞
前端·typescript
geovindu17 小时前
go: Interpreter Pattern
开发语言·设计模式·golang·解释器模式
workflower18 小时前
从拿订单到看方向
大数据·人工智能·设计模式·机器人·动态规划
sensen_kiss1 天前
CPT304 SoftwareEngineeringII 软件工程 2 Pt.3 设计模式(上)
设计模式·软件工程
mit6.8241 天前
20种Agent 设计模式
人工智能·设计模式