阅读 1296

vue3 + vite 项目搭建 - 封装全局请求axios (单例模式)

  1. 安装依赖
"dependencies": {
    "qs": "^6.10.1",
    "axios": "^0.21.1"
  },
复制代码
  1. 统一管理配置,创建src/config/net.config.ts
//src/config/net.config.ts
type NetConfigSuccessCode = 200 | '200' | '000000'
// 正式项目可以选择自己配置成需要的接口地址,如"https://api.xxx.com"
// 问号后边代表开发环境,冒号后边代表生产环境
export const baseURL: string = import.meta.env.MODE === 'development'
  ? '/xz-risk'
  : `${import.meta.env.VITE_RES_URL}/xz-risk`
// 配后端数据的接收方式application/json;charset=UTF-8 或 application/x-www-form-urlencoded;charset=UTF-8
export const contentType: string = 'application/json;charset=UTF-8'
// 最长请求时间
export const requestTimeout: number = 10000
// 超时尝试次数
export const timeoutNum: number = 3
// 超时重新请求间隔
export const intervalTime: number = 1000
// 操作正常code,支持String、Array、int多种类型
export const successCode: NetConfigSuccessCode[] = [200, '200', '000000']
// 数据状态的字段名称
export const statusName: string = 'code'
// 状态信息的字段名称
export const messageName: string = 'msg'

复制代码
  1. 封装api模块,用于管理请求路径 src/api/index.ts
// src/api/index.ts
import common from '@/api/common'

interface UrlDict {
  [key: string]: {
    [key: string]: string
  }
}

const urlDict: UrlDict = {
  common
}

const getUrl = (url: string): string => {
  try {
    if (url === '') throw new Error('请求路径为空')
    const [modelName, urlName] = url.split('.')
    if (!Object.keys(urlDict).includes(modelName)) throw new Error('未获取到请求模块')
    const reqUrl = urlDict[modelName][urlName]
    if (!reqUrl) throw new Error('未获取到请求所需url')
    return reqUrl
  } catch (e) {
    console.error(e)
    return ''
  }
}

export default getUrl

复制代码
// src/api/common.ts
export default {
  token: '/common/token'
}

复制代码
  1. 创建请求所需类型
// src/types/utils/request.d.ts
declare namespace MyRequest {
  interface response {
    code: number | string,
    msg: string,
    data: any
  }
  class request {
    /**
       * POST方法
       * @param url 请求路径,模式:[模块名称.接口名称] 如 common.token
       * @param data 请求参数
       * @param config 请求配置
       */
    public post(url: string, data?: any, config?: object): Promise<response>

    /**
       * POST方法
       * @param url 请求路径,模式:[模块名称.接口名称] 如 common.token
       * @param params 请求参数
       * @param config 请求配置
       */
    public get(url: string, params?: any, config?: object): Promise<response>
  }
}

复制代码
  1. 封装请求
import axios, { AxiosResponse, AxiosRequestConfig, CancelTokenStatic, AxiosInstance } from 'axios'
import {
  baseURL,
  successCode,
  contentType,
  requestTimeout,
  statusName,
  messageName
} from '@/config/net.config'
import qs from 'qs'
import getUrl from '@/api'

const CODE_MESSAGE: any = {
  200: '服务器成功返回请求数据',
  201: '新建或修改数据成功',
  202: '一个请求已经进入后台排队(异步任务)',
  204: '删除数据成功',
  400: '发出信息有误',
  401: '用户没有权限(令牌失效、用户名、密码错误、登录过期)',
  402: '前端无痛刷新token',
  403: '用户得到授权,但是访问是被禁止的',
  404: '访问资源不存在',
  406: '请求格式不可得',
  410: '请求资源被永久删除,且不会被看到',
  500: '服务器发生错误',
  502: '网关错误',
  503: '服务不可用,服务器暂时过载或维护',
  504: '网关超时'
}

class MyRequest {
  // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
  protected service: AxiosInstance = axios
  protected pending: Array<{ url: string, cancel: Function }> = []
  protected CancelToken: CancelTokenStatic = axios.CancelToken
  protected axiosRequestConfig: AxiosRequestConfig = {}
  private static _instance: MyRequest;

  constructor () {
    this.requestConfig()
    this.service = axios.create(this.axiosRequestConfig)
    this.interceptorsRequest()
    this.interceptorsResponse()
  }

  /**
     * 初始化配置
     * @protected
     */
  protected requestConfig (): void {
    this.axiosRequestConfig = {
      baseURL: baseURL,
      headers: {
        timestamp: new Date().getTime(),
        'Content-Type': contentType
      },
      // transformRequest: [obj => qs.stringify(obj)],
      transformResponse: [function (data: AxiosResponse) {
        return data
      }],
      paramsSerializer: function (params: any) {
        return qs.stringify(params, { arrayFormat: 'brackets' })
      },
      timeout: requestTimeout,
      withCredentials: false,
      responseType: 'json',
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
      maxRedirects: 5,
      maxContentLength: 2000,
      validateStatus: function (status: number) {
        return status >= 200 && status < 500
      }
      // httpAgent: new http.Agent({keepAlive: true}),
      // httpsAgent: new https.Agent({keepAlive: true})
    }
  }

  /**
     * 请求拦截
     * @protected
     */
  protected interceptorsRequest (): void {
    this.service.interceptors.request.use(
      (config: AxiosRequestConfig) => {
        const keyOfRequest = this.getKeyOfRequest(config)
        this.removePending(keyOfRequest, true)
        config.cancelToken = new this.CancelToken((c: any) => {
          this.pending.push({
            url: keyOfRequest,
            cancel: c
          })
        })
        this.requestLog(config)
        return config
      },
      (error) => {
        return Promise.reject(error)
      }
    )
  }

  /**
     * 响应拦截
     * @protected
     */
  protected interceptorsResponse (): void {
    this.service.interceptors.response.use((response: AxiosResponse) => {
      return this.handleResponse(response)
    }, error => {
      const { response } = error
      if (response === undefined) {
        return Promise.reject(new Error(error))
      } else {
        return this.handleResponse(response)
      }
    })
  }

  protected handleResponse (response: AxiosResponse): Promise<AxiosResponse<any>> {
    this.responseLog(response)
    this.removePending(this.getKeyOfRequest(response.config))
    const { data, status, config, statusText } = response
    let code = data && data[statusName]
      ? data[statusName]
      : status
    if (successCode.indexOf(data[statusName]) + 1) code = 200
    switch (code) {
      case 200:
        return Promise.resolve(response)
      case 401:
        // TODO token失效,跳转登录页
        break
      case 403:
        // TODO 没有权限,跳转403页面
        break
    }
    // 异常处理
    const errMsg = data && data[messageName]
      ? data[messageName]
      : CODE_MESSAGE[code]
        ? CODE_MESSAGE[code]
        : statusText
    return Promise.reject(errMsg)
  }

  /**
     * 取消重复请求
     * @protected
     * @param key
     * @param request
     */
  protected removePending (key: string, request: boolean = false): void {
    this.pending.some((item, index) => {
      if (item.url === key) {
        if (request) console.log('=====  取消重复请求  =====', item)
        item.cancel()
        this.pending.splice(index, 1)
        return true
      }
      return false
    })
  }

  /**
     * 获取请求配置拼装的key
     * @param config
     * @protected
     */
  protected getKeyOfRequest (config: AxiosRequestConfig): string {
    let key = config.url
    if (config.params) key += JSON.stringify(config.params)
    if (config.data) key += JSON.stringify(config.data)
    key += `&request_type=${config.method}`
    return key as string
  }

  /**
     * 请求日志
     * @param config
     * @protected
     */
  protected requestLog (config: any): void {
  }

  /**
     * 响应日志
     * @protected
     * @param response
     */
  protected responseLog (response: AxiosResponse) {
    if (import.meta.env.MODE === 'development') {
      const randomColor = `rgba(${Math.round(Math.random() * 255)},${Math.round(
        Math.random() * 255
      )},${Math.round(Math.random() * 255)})`
      console.log(
        '%c┍------------------------------------------------------------------┑',
        `color:${randomColor};`
      )
      console.log('| 请求地址:', response.config.url)
      console.log('| 请求参数:', qs.parse(response.config.data))
      console.log('| 返回数据:', response.data)
      console.log(
        '%c┕------------------------------------------------------------------┙',
        `color:${randomColor};`
      )
    }
  }

  /**
     * post方法
     * @param url
     * @param data
     * @param config
     */
  public post (url: string, data: any = {}, config: object = {}): Promise<MyRequest.response> {
    return new Promise((resolve, reject) => {
      this.service.post(getUrl(url), data, config).then(result => {
        resolve({
          msg: result.data.msg,
          data: result.data.data,
          code: result.data.code
        })
      }, reject)
    })
  }

  /**
     * post方法
     * @param url
     * @param params
     * @param config
     */
  public get (url: string, params: any = {}, config: object = {}): Promise<MyRequest.response> {
    return new Promise((resolve, reject) => {
      this.service.get(`${getUrl(url)}?${qs.stringify(params)}`, config).then(result => {
        resolve({
          msg: result.data.msg,
          data: result.data.data,
          code: result.data.code
        })
      }, reject)
    })
  }

  /**
     * 创建唯一实例(单例模式)
     */
  public static getInstance (): MyRequest {
    // 如果 instance 是一个实例 直接返回,  如果不是 实例化后返回
    this._instance || (this._instance = new MyRequest())
    return this._instance
  }
}

export default MyRequest.getInstance()

复制代码
  1. 全局注册,这里是挂载到globalProperties
// src/vab/plugins/globaProperties.ts
import { App } from 'vue'
import request from '@/utils/request'
import router from '@/router'
import { store } from '@/store'
/**
 * @name: sww
 * @date: 2021-06-29
 * @desc: 获取表格高度
 */
const baseTableHeight = (formType: number = 1): number => {
  const mainInfo = store.getters.layoutMainInfo
  return (mainInfo.height - 130) * formType
}

/**
 * @name: sww
 * @date: 2021-07-19
 * @desc: 格式化网络资源
 */
const formatImage = (src: string): string => {
  if (!src) return ''
  if (src.includes('http')) return src
  return `${import.meta.env.VITE_RES_URL}${src}`
}

const install = (app: App) => {
  // 注册请求实例
  app.config.globalProperties.$request = request
  app.config.globalProperties.$baseTableHeight = baseTableHeight
  app.config.globalProperties.$image = formatImage
}

export default install

复制代码
  1. 全局声明,如果不声明,在组件中使用 this.没有代码提示
// src/types/vab/plugins/globalProperties.d.ts
export {}
declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    // this.代码补全配置
    $request: MyRequest.request,
    $baseTableHeight: (formType?: number) => number,
    $image: (src: string) => void
  }
}

复制代码
  1. 使用
//组件中使用
this.$request.post('common.token', {})
this.$request.get('common.token', {})
//ts文件中使用
import request from '@/utils/request'
request.post('common.token', {})
request.get('common.token', {})
复制代码
  1. 注意新增api模块后,要在 api/index.ts中导入,比如新增一个用户模块
// src/api/user.ts
export default {
  list: '/user/list'
}

复制代码
// src/api/index.ts
import common from '@/api/common'
// 导入新增模块
import user from '@/api/user'

interface UrlDict {
  [key: string]: {
    [key: string]: string
  }
}

const urlDict: UrlDict = {
  common,
  user //新增模块
}

// 使用
this.$request.get('user.list', {})
复制代码
文章分类
前端
文章标签