npm

@zd~/request
TypeScript icon, indicating that this package has built-in type declarations

0.1.9 • Public • Published

@zd~/Request

A request library based on Axios. Support WEB/UNI-APP/WX-PROGRAM

example

import type { AxiosRequestConfig, Canceler } from 'axios'

import axios, { AxiosError } from 'axios'
import { saveAs } from 'file-saver'
import { isObject, merge } from 'lodash-es'
import type { HttpRequestConfig, ResponseResult } from '@zd~/request/http'
import {
  ContentTypeEnum,
  HttpRequest,
  RequestMethodsEnum as HttpRequestMethodsEnum,
} from '@zd~/request'
import { getTokenCookie, removeTokenCookie } from '~/utils/cookie'

export interface CustomConfig {
  /**
   * @description 是否需要token
   */
  withToken?: boolean
  /**
   * @description 忽略重复请求。第一个请求未完成时进行第二个请求,第一个会被被取消
   *              参考 axios 取消请求 https://axios-http.com/zh/docs/cancellation
   */
  ignoreRepeatRequest?: boolean
  /**
   * 下载文件名称
   */
  filename?: string
};

const tokenKey = 'Authorization'
const tokenKeyScheme = 'Bearer'
const cancelMap = new Map<string, Canceler>()

function getHeaderFileName(headers: Record<string, any>) {
  ['file-name', 'download-filename', 'File-Name', 'FileName', 'Filename'].forEach((key) => {
    if (Object.prototype.hasOwnProperty.call(headers, key)) {
      if (headers[key])
        return `${headers[key]}`
    }
  })
  return ''
}

export const request = new HttpRequest<CustomConfig>({
  baseURL: import.meta.env.VITE_APP_API_URL,
  timeout: 15 * 1000,
  headers: {
    'Content-Type': ContentTypeEnum.JSON,
  },
  getResponse: false,
  ignoreRepeatRequest: false,
  withToken: true,
  onUploadProgress(_progressEvent) {
    // 处理原生进度事件
  },
  // `onDownloadProgress` 允许为下载处理进度事件
  // 浏览器专属
  onDownloadProgress(progressEvent) {
    if (progressEvent.total) {
      const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100)
      console.log(`Download Progress: ${progress}%`)
    }
  },
}, {
  request(config) {
    /**
     * token
     */
    const token = getTokenCookie()
    if (config?.withToken && token) {
      config.headers![tokenKey] = `${tokenKeyScheme} ${token}`
    }
    /**
     * 忽略重复请求。第一个请求未完成时进行第二个请求,第一个会被被取消
     */
    if (config.ignoreRepeatRequest) {
      const key = generateKey({ ...config })
      const cancelToken = new axios.CancelToken(c => cancelInterceptor(key, c))
      config.cancelToken = cancelToken
    }
    /**
     * 添加时间戳到 get 请求
     */
    if (config.method?.toUpperCase() === HttpRequestMethodsEnum.GET) {
      config.params = { _t: `${Date.now()}`, ...config.params }
    }

    return config
  },

  requestError(e) {
    console.log(e)
  },

  async response(_response) {
    cancelMap.delete(generateKey(_response.config))
    const config = _response.config as HttpRequestConfig<CustomConfig>
    if (config.getResponse) {
      // 处理下载文件
      if (_response.config.responseType === 'blob') {
        const blob = _response.data as unknown as Blob
        const filename = config.filename
        if (_response.data && blob.type !== 'application/json') {
          if (_response.data?.type && !filename) {
            saveAs(_response.data as unknown as Blob)
          }
          else {
            const urlList = config.url?.split('/')
            const extList = config.url?.split('.')
            const urlFileName = urlList && urlList?.length >= 0 ? urlList[urlList?.length - 1] : ''
            const ext = extList && extList?.length >= 0 ? extList[extList?.length - 1] : ''
            const _filename = filename || getHeaderFileName(config.headers || {}) || urlFileName || `${Date.now()}.${ext}`
            saveAs(_response.data, decodeURI(decodeURI(_filename)))
          }
        }
        else {
          const resText = await blob.text()
          const rspObj = JSON.parse(resText)
          return handleError(rspObj.msg || getSystemErrorMessage(rspObj.code))
        }
      }

      return _response
    }
    const responseData = _response.data as ResponseResult<object>

    if (responseData.code === 200) {
      return responseData as any
    }
    /**
     * 登录过期
     */
    if (responseData.code === 401) {
      removeTokenCookie()
    }

    const msg = responseData.msg || getSystemErrorMessage(responseData.code)

    return handleError(msg)
  },

  responseError(error) {
    if (error.config)
      cancelMap.delete(generateKey(error.config))

    if (error instanceof AxiosError) {
      handleError(getAxiosErrorErrorMessage(error.code))
    }

    throw error
  },
})

export function removeAllPenddingRequest() {
  for (const [, value] of cancelMap) {
    value?.('remove all pendding request')
  }
}

function cancelInterceptor(key: string, canceler: Canceler) {
  if (cancelMap.has(key)) {
    cancelMap.get(key)?.('cancel repeat request')
  }
  cancelMap.set(key, canceler)
}

function generateKey(config: AxiosRequestConfig) {
  const { url, method, params = {}, data = {} } = config
  return `${url}-${method}-${JSON.stringify(method === 'get' ? params : data)}`
}

async function handleError(msg: string) {
  console.error(msg)
  // window.showError?.(new Error(msg))
}

function transformRequest(params?: object) {
  if (!isObject(params))
    return ''
  let result = ''
  for (const propName of Object.keys(params)) {
    const value = params[propName as keyof typeof params]
    const part = `${encodeURIComponent(propName)}=`
    if (value !== null && typeof value !== 'undefined') {
      if (typeof value === 'object') {
        for (const key of Object.keys(value)) {
          if (value[key] !== null && typeof value[key] !== 'undefined') {
            const params = `${propName}[${key}]`
            const subPart = `${encodeURIComponent(params)}=`
            result += `${subPart + encodeURIComponent(value[key])}&`
          }
        }
      }
      else {
        result += `${part + encodeURIComponent(value)}&`
      }
    }
  }
  return result
}

export function download(config: Parameters<typeof request.request>[0]) {
  const downloadBaseConfig: typeof config = {
    transformRequest: [
      (params: object) => {
        return transformRequest(params)
      },
    ],
    responseType: 'blob',
    headers: {
      'Content-Type': ContentTypeEnum.FORM_URLENCODED,
    },
    getResponse: true,
  }
  return request.request(merge({ }, downloadBaseConfig, config))
}

function getAxiosErrorErrorMessage(code?: string): string {
  switch (code) {
    case 'ERR_BAD_OPTION_VALUE':
      return '选项设置了错误的值'
    case 'ERR_BAD_OPTION':
      return '无效的或不支持的选项'
    case 'ECONNABORTED':
      return '网络连接被中断,通常因为请求超时'
    case 'ETIMEDOUT':
      return '操作超时'
    case 'ERR_NETWORK':
      return '网络错误'
    case 'ERR_FR_TOO_MANY_REDIRECTS':
      return '请求被重定向了太多次,可能导致无限循环'
    case 'ERR_DEPRECATED':
      return '使用了已被废弃的函数或方法'
    case 'ERR_BAD_RESPONSE':
      return '从服务器接收到无效或错误的响应'
    case 'ERR_BAD_REQUEST':
      return '发送的请求格式错误或无效'
    case 'ERR_CANCELED':
      return '请求已经被取消'
    case 'ERR_NOT_SUPPORT':
      return '使用的某个功能或方法不被支持'
    case 'ERR_INVALID_URL':
      return '提供的URL无效'
    default:
      return '未知错误'
  }
}

function getSystemErrorMessage(status: number) {
  switch (status) {
    case 400:
      return '错误请求,服务器无法理解请求的格式'
    case 401:
      return '无效的会话,或者会话已过期,请重新登录。'
    case 403:
      return '当前操作没有权限'
    case 404:
      return '服务器无法根据客户端的请求找到资源'
    case 405:
      return '网络请求错误,请求方法未允许!'
    case 408:
      return '网络请求超时!'
    case 500:
      return '服务器内部错误,无法完成请求'
    case 502:
      return '网关错误'
    case 503:
      return '服务器目前无法使用(由于超载或停机维护)'
    case 504:
      return '网络超时!'
    case 505:
      return 'http版本不支持该请求!'
    default:
      return '未知错误'
  }
}

Package Sidebar

Install

npm i @zd~/request

Weekly Downloads

30

Version

0.1.9

License

ISC

Unpacked Size

296 kB

Total Files

28

Last publish

Collaborators

  • zhuddan