【项目起步】逐步构建你的 axios

227 阅读3分钟

前言

前后端项目分离的情况下,往往需要很标准的请求处理方式,以此统一处理所有的请求,来降低开发人员的心智负担,所以本篇分享一下我们项目中构建的 axios。

标准返回

先来看一个我们与后端约定的标准返回

// 成功
{
      code: 200,
      code_msg: 'Success',
      trace_id: '1d75a6a5477b740',
      resp_data: null,
      data: {
        token:
          'MJh1MSpll-7a8D7IbD_L5IojkZBLuoQrKSTNy3aCwVHV3Kyn-EDw5Eh5c0W51xr0FUAZ6TsFjVQaWhTB9fqfXfse8qu_wtwNva8WJFy7Fz-gS-7xGNYPdXCT3xnJwbFl0MNgE3RgO_JyNXbFIS0F6zRHJwisLhbKU.cv0',
        user: {
          user_id: 16128,
          user_name: 'd',
        },
      },
};
// 失败
{
      code: 700000,
      code_msg: '登录失败,请查核后重新登陆',
      trace_id: '6d36f1ed572b32c',
      resp_data: null,
      data: null,
};

请求后处理

可以观察到,并不是直接使用状态码走对应的请求,无论是什么返回,都走了 200。

这样一来,无论请求成功与否,都可以拿到基本的数据结构。所以只需要额外关心两点

  1. 请求失败(服务器挂掉)
  2. 网络错误

这两点很好处理

  (error) => {
    if (!error.response) {
      // 处理用户网络问题
      Message.error('网络错误:无法连接到服务器,请检查网络连接');
    } else {
      // 处理服务器错误
      if (error.response.status >= 500) {
        Message.error('服务器开小差了,请稍后再试。');
      } else {
        handleError(error?.msg || '请求失败');
      }
    }
    return Promise.reject(error);
  }

再把服务正确的逻辑加上,另外我们 axios 还要负责解析接口,去掉无用的数据,所以应该直接返回给用户 response.data


axios.interceptors.response.use(
  (response: AxiosResponse<HttpResponse>) => {
    const { code, code_msg } = response.data;
    if (code === 200) {
      return response.data;
    }
    if (TOKEN_ERROR_CODES.includes(code)) {
      Modal.error({
        title: '提示',
        content: '登录已过期,请先登录',
        okText: '跳转',
        async onOk() {
          window.location.reload();
        },
      });
    }
    handleError(code_msg || '服务端返回错误');
    return Promise.reject(new Error(code_msg || '服务端返回错误'));
  },
  (error) => {
    if (!error.response) {
      // 处理用户网络问题
      Message.error('网络错误:无法连接到服务器,请检查网络连接');
    } else {
      // 处理服务器错误
      if (error.response.status >= 500) {
        Message.error('服务器开小差了,请稍后再试。');
      } else {
        handleError(error?.msg || '请求失败');
      }
    }
    return Promise.reject(error);
  }
);

请求前处理

请求前一般需要有两个环节

  1. 设定请求头

const handleRequestHeader = (config: InternalAxiosRequestConfig) => {
  config.headers['xxxx'] = 'xxx';
  return config;
};
  1. 让每个请求携带 token

const handleAuth = (config: InternalAxiosRequestConfig) => {
  const token = getToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
};

放进前置环节中

axios.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    config = handleRequestHeader(config);
    config = handleAuth(config);
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

关于环境

对于不同的环境,我们需要打到不同的接口去,这里可以使用 process.env.NODE_ENV 来做环境的区分

export const ENV_TEST = 'https://api-ai.com/fuxi';
// 根据环境设置baseURL
// const baseURL =
//   process.env.NODE_ENV === 'development'
//     ? ENV_MOCK
//     : ENV_TEST;
const baseURL = ENV_TEST;
axios.defaults.baseURL = baseURL;

完整代码

import { getToken } from '@/utils/auth';
import { Message, Modal } from '@arco-design/web-react';
import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import axios from 'axios';

const TOKEN_ERROR_CODES = [50008, 50012, 50014];
export const ENV_TEST = 'https://api-ai.com/fuxi';

export interface HttpResponse<T = unknown> extends AxiosResponse {
  status: number;
  msg?: string;
  code_msg?: string;
  code: number;
  data: T;
}

const handleRequestHeader = (config: InternalAxiosRequestConfig) => {
  // config.headers['xxxx'] = 'xxx';
  return config;
};
const handleAuth = (config: InternalAxiosRequestConfig) => {
  const token = getToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
};
const handleError = (msg: string) => {
  Message.error({ content: msg, duration: 5 * 1000 });
};

// 根据环境设置baseURL
// const baseURL =
//   process.env.NODE_ENV === 'development'
//     ? ENV_MOCK
//     : ENV_TEST;
const baseURL = ENV_TEST;
axios.defaults.baseURL = baseURL;

axios.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    config = handleRequestHeader(config);
    config = handleAuth(config);
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);
axios.interceptors.response.use(
  (response: AxiosResponse<HttpResponse>) => {
    const { code, code_msg } = response.data;
    if (code === 200) {
      return response.data;
    }
    if (TOKEN_ERROR_CODES.includes(code)) {
      Modal.error({
        title: '提示',
        content: '登录已过期,请先登录',
        okText: '跳转',
        async onOk() {
          window.location.reload();
        },
      });
    }
    handleError(code_msg || '服务端返回错误');
    return Promise.reject(new Error(code_msg || '服务端返回错误'));
  },
  (error) => {
    if (!error.response) {
      // 处理用户网络问题
      Message.error('网络错误:无法连接到服务器,请检查网络连接');
    } else {
      // 处理服务器错误
      if (error.response.status >= 500) {
        Message.error('服务器开小差了,请稍后再试。');
      } else {
        handleError(error?.msg || '请求失败');
      }
    }
    return Promise.reject(error);
  }
);

请求方法封装

如果用传统的 axios 来写无疑是冗长的,类型也不友好,如

export function GameModelApprovalList(data: GameModelApprovalListRequest) {
  return axios.post<GameModelApprovalListResponse>(
    '/api/v3/approval/',
    data,
    {
      baseURL: 'https://api.com/fuxi',
    }
  );
}

我们一步步来优化,先来看理想的调用方式

export const login = (data: LoginData) => {
  return post<LoginData, LoginRes>('/api/v1/passport/login', data);
};

有几个关键点

  1. 直接用 post 而不是 axios.post
  2. 类型要收束在泛型里
  3. baseUrl 应该置于请求前

根据这些点,我们封装一下 get

export const get = async <P = Params, T = any>(
  url: string,
  params?: P
): Promise<T> => {
  const res: AxiosResponse<T> = await axios.get(url, { params });
  return res.data;
};

post 也差不多,但是要额外考虑 qs 转化

export const post = async <P = Params, T = any>(
  url: string,
  params?: P,
  qs: boolean = false
): Promise<T> => {
  const requestBody = qs && params ? QS.stringify(params) : params || {};
  const res: AxiosResponse<T> = await axios.post(url, requestBody);
  return res.data;
};

完整代码

import axios, { AxiosResponse } from 'axios';
import QS from 'qs';

interface Params {
  [key: string]: any;
}

export const get = async <P = Params, T = any>(
  url: string,
  params?: P
): Promise<T> => {
  const res: AxiosResponse<T> = await axios.get(url, { params });
  return res.data;
};

export const post = async <P = Params, T = any>(
  url: string,
  params?: P,
  qs: boolean = false
): Promise<T> => {
  const requestBody = qs && params ? QS.stringify(params) : params || {};
  const res: AxiosResponse<T> = await axios.post(url, requestBody);
  return res.data;
};

这样一来,我们的接口就很容易写了:


export const login = (data: LoginData) => {
  return post<LoginData, LoginRes>('/api/v1/passport/login', data);
};

export const logout = () => {
  return post('/api/v1/passport/logout');
};

export const getUserInfo = () => {
  return post<any, UserState>('/api/v1/user/info');
};

如果这篇文章对你有帮助的话,不妨点个赞吧~

下篇预告:《【项目起步】mockjs 使用攻略》