从零到一封装Axios请求

313 阅读6分钟

从零到一封装Axios请求

请求的封装主要是为了满足项目中各种业务场景下与后端交互,简化请求方法,做到快速处理应对

请求的类型

  • GET
  • POST
  • DELETE
  • PUT

GET上送参数类型

  • ?name=123&age=21

POST请求头类型

  • Content-Type: application/json : 请求体中的数据会以json字符串的形式发送到后端
  • Content-Type: application/x-www-form-urlencoded:请求体中的数据会以普通表单形式(键值对)发送到后端
  • Content-Type: multipart/form-data: 它会将请求体的数据处理为一条消息,以标签为单元,用分隔符分开。既可以上传键值对,也可以上传文件。

Axios封装

了解了以上知识,下面我们可以一步一步去封装,去完善Axios的请求适用场景 下面示例就是最简单的封装,不添加任何场景适配的封装,后面我们会针对当前最简版做渐进式增强

import axios from 'axios'


const service = axios.create({
  baseURL: process.env.VUE_APP_instanceURL,
  timeout: 10000
})

service.interceptors.request.use(config => {
  return config
}, error => {
  return Promise.reject(error)
})


service.interceptors.response.use(response => {
  return response
}, error => {
  return Promise.reject(error)
})

一、增加全局loading控制

在页面发起请求的时候有一些在请求过程中禁止用户任何操作,这个时候就需要loading,但是并非所有接口都需要,所以需要区分处理,这里我们用elementUI的组件库示例,其他组件库也都抛出了js中使用loading的方法,用法基本相同,贴上elementUI中 Loading的使用链接,点击了解

import axios from 'axios'
import { Loading } from 'element-ui'
let loading // 在这里声明是为了在下边全局使用
const loadingWhiteList = [ // 白名单,这些请求不需要loading
  '/api/web/login',
  '/api/web/user'
]

const service = axios.create({
  baseURL: process.env.VUE_APP_instanceURL,
  timeout: 10000
})

service.interceptors.request.use(config => {
  // 白名单内不加入loading
  !loadingWhiteList.includes(config.url) && (loading = Loading.service({
    text: '加载中...', // 设置加载中提示文案
    spinner: 'el-icon-loading', // 设置加载中图标样式
    background: 'rgba(0,0,0,0.2)', // 设置遮罩
    fullscreen: true, // 设置是否全屏,全屏时禁止一切操作
    customClass: 'globalLoadingWrapper' // 设置全局Loading的类名去做样式定制化
  }))
  return config
}, error => {
  loading && loading.close()
  return Promise.reject(error)
})


service.interceptors.response.use(response => {
  loading && loading.close()
  return response
}, error => {
  loading && loading.close()
  return Promise.reject(error)
})

二、封装方法,不同的上送参数类型支持设置不同的请求头

在上述,我们已经了解请求头的类型,比如说formdata时,请求头要设置成Content-Type: multipart/form-data,为了便于适配,我们让使用者传入相应的请求头,当用户没有传入,那就使用默认的

这里主要涉及的是请求头的合并,把传入的与默认的合并,并以传入的为主

贴上Axios中文网,点击查阅config配置详情

// @/utils/fetch.js
const service = axios.create({
  baseURL: process.env.VUE_APP_instanceURL,
  timeout: 10000
})
//  设置默认的post请求头
service.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
...
  
const fetch = (method, url, data, config) => {
  let configSeting = Object.assign({}, {
    paramsSerializer: function (params) {
      return qs.stringify(params, { arrayFormat: 'brackets' })
    },
    ...config,
    params: data
  })
  switch (method) {
    case 'get':
      return service.get(url, configSeting)
    default:
      return service[method](url, data, config)
  }
}
export default fetch

// 使用
import fetch from '@/utils/fetch.js'

// 获取用户信息
export function getUserinfo (parmas) {
  return fetch('get', '/api/web/userinfo', parmas,{timeout:30000})
}

// 上送用户信息
export function updateUserinfo (data) {
  return fetch('post', '/api/web/updateUserinfo', data,{
    timeout:30000,
    headers:{
      'Content-Type':'multipart/form-data'
    }
  })
}

三、返回数据处理,状态拦截

返回数据首先要处理的是错误码,这个错误码是与后端协商定的,比如6001是已退出登录或者token失效,根据返回的错误码做出对应的处理,登录失效就返回登录页等等

service.interceptors.response.use(response => {
  loading && loading.close()
  // 状态拦截
  response.data && response.data.code && errorCode(code)
  return response
}, error => {
  loading && loading.close()
  const { message } = error
  // 错误处理
  errorMessage(message)
  return Promise.reject(error)
})

// 全局状态码处理
const errorCode = (code) => {
  // 如果多的话可以用switch
  let timer
  if (code === 6001) {
    Message.error('登录失效')
    timer = setTimeout(() => {
      clearTimeout(timer)
      router.replace('/login')
    }, 200)
  }
}

// 接口错误提示
const errorMessage = (message) => {
  if (message.includes('Network Error')) {
    message = '网络环境异常'
  } else if (message.includes('timeout')) {
    message = '后端接口请求超时'
  } else if (message.includes('Request failed with status code')) {
    const code = message.substr(message.length - 3)
    message = '后端接口' + code + '异常'
  }
  Message.error(message)
}

四、切换页面终止白名单以外的请求

上述已经基本封装好了axios,那么在路由发生变化时,之前页面的请求可能就没有用了,这个时候为了优化性能,去终止之前的请求

当然,这里有一些接口可能不能一概而论,所以还是要有白名单不可终止

以下使用的是接口名称去做的key,不建议使用

// 终止请求的载体   Map不可重复
window.globalCancelRequest = new Map()
// 终止请求的白名单载体
window.globalRequestWhiteList = [
  '/api/web/login'
]

const addPending = (config) => {
  config.cancelToken = new axios.CancelToken((cancel) => {
    window.globalCancelRequest.set(config.url, cancel)
  })
}
const removePending = (config) => {
  if (window.globalCancelRequest.get(config.url)) {
    window.globalCancelRequest.get(config.url)()
    window.globalCancelRequest.delete(config.url)
  }
}

// 取消白名单外所有请求  在路由跳转前调用
export const cancelAllPrevRequest = () => {
  Array.from(window.globalCancelRequest).forEach(([key, cancel]) => {
    if (!window.globalRequestWhiteList.includes(key)) {
      cancel()
    }
  })
}

service.interceptors.request.use(config => {
  // 白名单内不加入loading
  !loadingWhiteList.includes(config.url) && (loading = Loading.service({
    text: '加载中...', // 设置加载中提示文案
    spinner: 'el-icon-loading', // 设置加载中图标样式
    background: 'rgba(0,0,0,0.2)', // 设置遮罩
    fullscreen: true, // 设置是否全屏,全屏时禁止一切操作
    customClass: 'globalLoadingWrapper' // 设置全局Loading的类名去做样式定制化
  }))

  // 存放请求  优先调用避免同一个请求重复调用
  removePending(config)
  addPending(config)

  return config
}, error => {
  loading && loading.close()
  return Promise.reject(error)
})


service.interceptors.response.use(response => {
  loading && loading.close()

  // 从pending请求列表移除
  removePending(response.config)

  // 状态拦截
  response.data && response.data.code && errorCode(response.data.code)
  return response
}, error => {
  loading && loading.close()

  // 从pending请求列表移除
  error.config && removePending(error.config)

  // 错误处理
  const { message } = error
  errorMessage(message)
  return Promise.reject(error)
})

Congratulations 恭喜,到此,你已经可以尝试自己封装了

贴上全部代码,仅供参考

import axios from 'axios'
import qs from 'qs'
import { Loading, Message } from 'element-ui'
import router from '@/router'
let loading // 在这里声明是为了在下边全局使用
const loadingWhiteList = [ // 白名单,这些请求不需要loading
  '/api/web/login',
  '/api/web/user'
]

// 终止请求的载体   Map不可重复
window.globalCancelRequest = new Map()
// 终止请求的白名单载体
window.globalRequestWhiteList = [
  '/api/web/login'
]

const addPending = (config) => {
  config.cancelToken = new axios.CancelToken((cancel) => {
    window.globalCancelRequest.set(config.url, cancel)
  })
}
const removePending = (config) => {
  if (window.globalCancelRequest.get(config.url)) {
    window.globalCancelRequest.get(config.url)()
    window.globalCancelRequest.delete(config.url)
  }
}

// 取消白名单外所有请求  在路由跳转前调用
export const cancelAllPrevRequest = () => {
  Array.from(window.globalCancelRequest).forEach(([key, cancel]) => {
    if (!window.globalRequestWhiteList.includes(key)) {
      cancel()
    }
  })
}

const service = axios.create({
  baseURL: process.env.VUE_APP_instanceURL,
  timeout: 10000
})
//  设置默认的post请求头
service.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'

service.interceptors.request.use(config => {
  // 白名单内不加入loading
  !loadingWhiteList.includes(config.url) && (loading = Loading.service({
    text: '加载中...', // 设置加载中提示文案
    spinner: 'el-icon-loading', // 设置加载中图标样式
    background: 'rgba(0,0,0,0.2)', // 设置遮罩
    fullscreen: true, // 设置是否全屏,全屏时禁止一切操作
    customClass: 'globalLoadingWrapper' // 设置全局Loading的类名去做样式定制化
  }))

  // 存放请求  优先调用避免同一个请求重复调用
  removePending(config)
  addPending(config)

  return config
}, error => {
  loading && loading.close()
  return Promise.reject(error)
})


service.interceptors.response.use(response => {
  loading && loading.close()

  // 从pending请求列表移除
  removePending(response.config)

  // 状态拦截
  response.data && response.data.code && errorCode(response.data.code)
  return response
}, error => {
  loading && loading.close()

  // 从pending请求列表移除
  error.config && removePending(error.config)

  // 错误处理
  const { message } = error
  errorMessage(message)
  return Promise.reject(error)
})

// 全局状态码处理
const errorCode = (code) => {
  // 如果多的话可以用switch
  if (code === 6001) {
    Message.error('登录失效')
    let timer = setTimeout(() => {
      clearTimeout(timer)
      router.replace('/login')
    }, 500)
  }
}


// 接口错误提示
const errorMessage = (message) => {
  if (message.includes('Network Error')) {
    message = '网络环境异常'
  } else if (message.includes('timeout')) {
    message = '后端接口请求超时'
  } else if (message.includes('Request failed with status code')) {
    const code = message.substr(message.length - 3)
    message = '后端接口' + code + '异常'
  }
  Message.error(message)
}

const fetch = (method, url, data, config) => {
  let configSeting = Object.assign({}, {
    paramsSerializer: function (params) {
      return qs.stringify(params, { arrayFormat: 'brackets' })
    },
    ...config,
    params: data
  })
  switch (method) {
    case 'get':
      return service.get(url, configSeting)
    default:
      return service[method](url, data, config)
  }
}
export default fetch