Skip to content

Axios

ts
const LoginName = PageEnum.BASE_LOGIN_NAME
const LoginPath = PageEnum.BASE_LOGIN

const transform: AxiosTransform = {
  /**
   * @description: 处理请求数据
   */
  transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => {
    const {
      isShowMessage = true,
      isShowErrorMessage,
      isShowSuccessMessage,
      successMessageText,
      errorMessageText,
      isTransformResponse,
      isReturnNativeResponse,
    } = options

    // 是否返回原生响应头 比如:需要获取响应头时使用该属性
    if (isReturnNativeResponse)
      return res

    // 不进行任何处理,直接返回
    // 用于页面代码可能需要直接获取code,data,message这些信息时开启
    if (!isTransformResponse)
      return res.data

    const { data } = res

    const $dialog = window.$dialog
    const $message = window.$message

    if (!data) {
      // return '[HTTP] Request has no return value';
      throw new Error('请求出错,请稍候重试')
    }
    //  这里 code,result,message为 后台统一的字段,需要修改为项目自己的接口返回格式
    const { code, result, message } = data
    // 请求成功
    const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS
    // 是否显示提示信息
    if (isShowMessage) {
      if (hasSuccess && (successMessageText || isShowSuccessMessage)) {
        // 是否显示自定义信息提示
        $dialog.success({
          type: 'success',
          content: successMessageText || message || '操作成功!',
        })
      }
      else if (!hasSuccess && (errorMessageText || isShowErrorMessage)) {
        // 是否显示自定义信息提示
        $message.error(message || errorMessageText || '操作失败!')
      }
      else if (!hasSuccess && options.errorMessageMode === 'modal') {
        // errorMessageMode=‘custom-modal’的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
        $dialog.info({
          title: '提示',
          content: message,
          positiveText: '确定',
          onPositiveClick: () => {},
        })
      }
    }

    // 接口请求成功,直接返回结果
    if (code === ResultEnum.SUCCESS)
      return result

    // 接口请求错误,统一提示错误信息 这里逻辑可以根据项目进行修改
    let errorMsg = message
    switch (code) {
      // 请求失败
      case ResultEnum.ERROR:
        $message.error(errorMsg)
        break
      // 登录超时
      case ResultEnum.TIMEOUT:

        if (router.currentRoute.value?.name === LoginName)
          return
        // 到登录页
        errorMsg = '登录超时,请重新登录!'
        $dialog.warning({
          title: '提示',
          content: '登录身份已失效,请重新登录!',
          positiveText: '确定',
          // negativeText: '取消',
          closable: false,
          maskClosable: false,
          onPositiveClick: () => {
            storage.clear()
            window.location.href = LoginPath
          },
          onNegativeClick: () => {},
        })
        break
    }
    throw new Error(errorMsg)
  },

  // 请求之前处理config
  beforeRequestHook: (config, options) => {
    const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options

    const isUrlStr = isUrl(config.url as string)

    if (!isUrlStr && joinPrefix)
      config.url = `${urlPrefix}${config.url}`

    if (!isUrlStr && apiUrl && isString(apiUrl))
      config.url = `${apiUrl}${config.url}`

    const params = config.params || {}
    const data = config.data || false
    if (config.method?.toUpperCase() === RequestEnum.GET) {
      if (!isString(params)) {
        // 给 get 请求加上时间戳参数,避免从缓存中拿数据。
        config.params = Object.assign(params || {}, joinTimestamp(joinTime, false))
      }
      else {
        // 兼容restful风格
        config.url = `${config.url + params}${joinTimestamp(joinTime, true)}`
        config.params = undefined
      }
    }
    else {
      if (!isString(params)) {
        formatDate && formatRequestDate(params)
        if (Reflect.has(config, 'data') && config.data && Object.keys(config.data).length > 0) {
          config.data = data
          config.params = params
        }
        else {
          config.data = params
          config.params = undefined
        }
        if (joinParamsToUrl) {
          config.url = setObjToUrlParams(
            config.url as string,
            Object.assign({}, config.params, config.data)
          )
        }
      }
      else {
        // 兼容restful风格
        config.url = config.url + params
        config.params = undefined
      }
    }
    return config
  },

  /**
   * @description: 请求拦截器处理
   */
  requestInterceptors: (config, options) => {
    // 请求之前处理config
    const userStore = useUser()
    const token = userStore.getToken
    if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
      // jwt token
      (config as Recordable).headers.Authorization = options.authenticationScheme
        ? `${options.authenticationScheme} ${token}`
        : token
    }
    return config
  },

  /**
   * @description: 响应错误处理
   */
  responseInterceptorsCatch: (error: any) => {
    const $dialog = window.$dialog
    const $message = window.$message
    const { response, code, message } = error || {}
    // TODO 此处要根据后端接口返回格式修改
    const msg: string
      = response && response.data && response.data.message ? response.data.message : ''
    const err: string = error.toString()
    try {
      if (code === 'ECONNABORTED' && message.includes('timeout')) {
        $message.error('接口请求超时,请刷新页面重试!')
        return
      }
      if (err && err.includes('Network Error')) {
        $dialog.info({
          title: '网络异常',
          content: '请检查您的网络连接是否正常',
          positiveText: '确定',
          // negativeText: '取消',
          closable: false,
          maskClosable: false,
          onPositiveClick: () => {},
          onNegativeClick: () => {},
        })
        return Promise.reject(error)
      }
    }
    catch (error) {
      throw new Error(error as any)
    }
    // 请求是否被取消
    const isCancel = axios.isCancel(error)
    if (!isCancel)
      checkStatus(error.response && error.response.status, msg)

    else
      console.warn(error, '请求被取消!')

    // return Promise.reject(error);
    return Promise.reject(response?.data)
  },
}
js
const a = {
  timeout: 10 * 1000,
  //   authentication
  authenticationScheme: '',
  // 接口前缀
  prefixUrl: urlPrefix,
  headers: { 'Content-Type': ContentTypeEnum.JSON },
  // 数据处理方式
  transform,
  // 配置项,下面的选项都可以在独立的接口请求中覆盖
  requestOptions: {
    // 默认将prefix 添加到url
    joinPrefix: true,
    // 是否返回原生响应头 比如:需要获取响应头时使用该属性
    isReturnNativeResponse: false,
    // 需要对返回数据进行处理
    isTransformResponse: true,
    // post请求的时候添加参数到url
    joinParamsToUrl: false,
    // 格式化提交参数时间
    formatDate: true,
    // 消息提示类型
    errorMessageMode: 'none',
    // 接口地址
    apiUrl: globSetting.apiUrl,
    // 接口拼接地址
    urlPrefix,
    //  是否加入时间戳
    joinTime: true,
    // 忽略重复请求
    ignoreCancelToken: true,
    // 是否携带token
    withToken: true,
  },
  withCredentials: false,
}

创建实例

自定义实例默认参数

ts
import type { AxiosInstance } from 'axios'
import axios from 'axios'

// 设置创建的实例的默认参数
const instance: AxiosInstance = axios.create({
  baseURL: '/api',
  timeout: 30000
})

// 实例创建后修改默认值
instance.defaults.headers.common.Authorization = AUTH_TOKEN

配置的优先级

  1. 请求的配置参数
  2. 实例的 default 参数
  3. axios 库中的默认参数

Token

sh
pnpm add js-cookie
pnpm add @types/js-cookie -D
ts
import Cookies from 'js-cookie'

const TokenKey = 'Admin-Token'

export function getToken() {
  return Cookies.get(TokenKey)
}

export function setToken(token: string) {
  return Cookies.set(TokenKey, token)
}

export function removeToken() {
  return Cookies.remove(TokenKey)
}

方法封装

ts
export function useGet<R = any, T = any>(url: string, params?: T, config?: AxiosRequestConfig & RequestConfigExtra): Promise<ResponseBody<R>> {
  const options = {
    url,
    params,
    method: RequestEnum.GET,
    ...config,
  }
  return instancePromise<R, T>(options)
}

export function usePost<R = any, T = any>(url: string, data?: T, config?: AxiosRequestConfig & RequestConfigExtra): Promise<ResponseBody<R>> {
  const options = {
    url,
    data,
    method: RequestEnum.POST,
    ...config,
  }
  return instancePromise<R, T>(options)
}

export function usePut<R = any, T = any>(url: string, data?: T, config?: AxiosRequestConfig & RequestConfigExtra): Promise<ResponseBody<R>> {
  const options = {
    url,
    data,
    method: RequestEnum.PUT,
    ...config,
  }
  return instancePromise<R, T>(options)
}

export function useDelete<R = any, T = any>(url: string, data?: T, config?: AxiosRequestConfig & RequestConfigExtra): Promise<ResponseBody<R>> {
  const options = {
    url,
    data,
    method: RequestEnum.DELETE,
    ...config,
  }
  return instancePromise<R, T>(options)
}

请求拦截器

请求发送前添加 token

ts
import Axios, { AxiosRequestConfig } from 'axios'
import storage from '@/utils/storage'

function authRequestInterceptor(config: AxiosRequestConfig) {
  const token = storage.getToken()
  if (token)
    config.headers.authorization = `${token}`

  config.headers.Accept = 'application/json'
  return config
}

export const axios = Axios.create({})

axios.interceptors.request.use(authRequestInterceptor)

响应拦截器

ts
import Axios, { AxiosRequestConfig } from 'axios'

import { API_URL } from '@/config'
import { useNotificationStore } from '@/stores/notifications'

export const axios = Axios.create({
  baseURL: API_URL,
})

axios.interceptors.response.use(
  (response) => {
    return response.data
  },
  (error) => {
    const message = error.response?.data?.message || error.message
    useNotificationStore.getState().addNotification({
      type: 'error',
      title: 'Error',
      message,
    })

    return Promise.reject(error)
  },
)

Adapter

Axios 可以在浏览器和 Node.js 中使用,浏览器中创建 XMLHttpRequests, node.js 创建 http 请求

ts
function getDefaultAdapter() {
  let adapter
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('../adapters/xhr')
  }
  else if (
    typeof process !== 'undefined'
    && Object.prototype.toString.call(process) === '[object process]'
  ) {
    // For node use HTTP adapter
    adapter = require('../adapters/http')
  }
  return adapter
}

允许指定已处理请求

ts
const settle = require('./../core/settle')

module.exports = function myAdapter(config) {
  // At this point:
  //  - config has been merged with defaults
  //  - request transformers have already run
  //  - request interceptors have already run

  // Make the request using config provided
  // Upon response settle the Promise

  return new Promise((resolve, reject) => {
    const response = {
      data: responseData,
      status: request.status,
      statusText: request.statusText,
      headers: responseHeaders,
      config,
      request,
    }

    settle(resolve, reject, response)

    // From here:
    //  - response transformers will run
    //  - response interceptors will run
  })
}
ts
instance.defaults.adapter = (config: AxiosRequestConfig) => {
  return new Promise((resolve, reject) => {
    const getUrl = () => {
      const fullPath = buildFullPath(config.baseURL, config.url)
      return buildURL(fullPath, config.params, config.paramsSerializer)
    }

    const method = (config.method || 'get').toUpperCase()

    const requestTask = uni.request({
      method: method as uniapp.RequestOptions['method'],
      url: getUrl(),
      header: config.headers,
      data: config.data,
      responseType: 'text',
      dataType: 'json',

      complete: function complete(response) {
        const {
          data,
          statusCode,
          errMsg,
          header,
        } = response as RequestCompleteCallbackResult

        const axiosResponse: AxiosResponse = {
          data,
          status: statusCode,
          statusText: errMsg,
          headers: header,
          config,
          request: requestTask,
        }

        settle(resolve, reject, axiosResponse)
      },
    })
  })
}

取消请求

XSRF

文件上传

form-data

ts
export interface UploadFileParams {
  // Other parameters
  data?: Recordable
  // File parameter interface field name
  name?: string
  // file name
  file: File | Blob
  // file name
  filename?: string
  [key: string]: any
}

文件下载

ts
async function downloadTo(url: string, filepath: string, { retryTooManyRequests }: { retryTooManyRequests: boolean }): Promise<void> {
  const writer = fs.createWriteStream(filepath)

  const response = await axios({
    url,
    method: 'GET',
    validateStatus: (status) => {
      if (status >= 200 && status < 300)
        return true
      else if (retryTooManyRequests && status === 429)
        return true
      else
        return false
    },
    responseType: 'stream',
  })

  if (response.status === 429) {
    const retryAfter = response.headers['retry-after']
    if (!retryAfter) {
      throw new Error(`${url}: 429 without retry-after header`)
    }
    else {
      debug(`${url}: 429, retry after ${retryAfter} seconds`)
      await sleep(retryAfter)
      return await downloadTo(url, filepath, { retryTooManyRequests })
    }
  }

  response.data.pipe(writer)

  return new Promise((resolve, reject) => {
    writer.on('finish', resolve)
    writer.on('error', reject)
  })
}

错误处理

ts
export interface AxiosError<T = any, D = any> extends Error {
  config: AxiosRequestConfig<D>
  code?: string
  request?: any
  response?: AxiosResponse<T, D>
  isAxiosError: boolean
  toJSON: () => object
}