都过了多少个愚人节了,还用担心axios是否适合自己的业务?

1,290 阅读9分钟

封装 axios 请求大同小异,本文提供思路和源码,不要以偏概全,适合自己业务才是最重要的!

功能点实现

  1. 基于 Restful 风格进行封装
  2. 取消请求与拦截重复请求
  3. token 拦截
  4. 接口白名单
  5. token 自动登录(看自己个人业务是否需要,一般用于移动端)
  6. 导出封装 GET/POST/DELETE/PUT/PATCH/...请求函数

话不多说,万事总要开头,先来引入开头

import axios from 'axios'

1. 基于 Restful 风格进行封装

GETPOST明显的区别就是参数放置位置不同,前者放在url?后面,后者放在body体内。

然鹅,Restful 风格在其基础添加了DELETEPUTPATCH请求类型,其中DELETE请求参数和GET一样放在url?后面,PUTPATCH这两个的请求参数则是和POST一样放在body体内。至于怎么放,看你们规范怎么定

至于想怎么改更请求参数放置位置,请继续往下看!

类型 请求参数放置位置 含义
GET url?后面 请求资源(查询用户)
DELETE url?后面 删除数据(删除用户)
POST body体内 创建数据(创建用户)
PUT body体内 更新全部数据(修改用户信息,昵称,签名,邮箱等等...全部更新)
PATCH body体内 更新部分数据(修改用户状态)

先创建一个axios实例,然后为其赋值默认参数。

const service = axios.create({
  // 设置默认请求头
  // 比如 BASE_API = 'api'
  // 那么访问就是 {你当前的域名}/api/{接口地址} http://localhost:8080/api/users
  baseURL: process.env.BASE_API,

  // 这里因为我后台把 http状态码 跟 定制返回体状态码 看为一样的,所以 http.status === result.status
  validateStatus: status => status < 500, // 拦截状态码大于500

  // 修改默认请求头,采用json格式传输,对数组对象极其方便,不用专门下qs格式化参数
  // common对应的是参数放在请求url上(get/delete),patch/post/put参数放在body体内
  // 这里对应http Request Header.Accept
  headers: {
    common: { Accept: 'application/json; charset=UTF-8' },
    patch: { 'Content-Type': 'application/json; charset=UTF-8' },
    post: { 'Content-Type': 'application/json; charset=UTF-8' },
    put: { 'Content-Type': 'application/json; charset=UTF-8' }
  },
  timeout: 60000 // 请求超时时间
})

2. 取消请求与拦截重复请求

官方原话:Axios 的 cancel token API 基于cancelable promises proposal,它还处于第一阶段。

使用axios.CancelToken进行拦截

// 每次请求都会记录在此对象里,用于判断是否重复
const pending = {}

// axios.CancelToken
const { CancelToken } = axios

const paramsList = ['get', 'delete']
const dataList = ['post', 'put', 'patch']
// 区分参数位置
const isTypeList = method => {
  if (paramsList.includes(method)) {
    return 'params'
  } else if (dataList.includes(method)) {
    return 'data'
  }
}

/**
 * 获取请求唯一值(key)
 * @param {Object} config - axios拦截器的config
 * @param {Boolean} isResult - 截取url唯一,这里区别请求前和请求后,因为前者和后者的url不同,所以需要区分一下
 */
function getRequestIdentify(config, isResult = false) {
  const url = !isResult
    ? config.url
    : config.baseURL + config.url.substring(1, config.url.length)
  const params = { ...(config[isTypeList(config.method)] || {}) }
  delete params.t
  return encodeURIComponent(url + JSON.stringify(params))
}

/**
 * 每次请求前 清除上一个跟它相同的还在请求中的接口
 * @param {String} key - url唯一值
 * @param {Boolean} isRequest - 是否执行取消重复请求
 */
function removePending(key, isRequest = false) {
  if (pending[key] && isRequest) {
    pending[key]('取消重复请求')
  }
  delete pending[key]
}

请求前的 url

请求前的url

请求后的 url

请求后的url

在拦截器里面设置

// 请求前
service.interceptors.request.use(
  config => {
    // 获取该次请求的唯一值
    const requestData = getRequestIdentify(config, true)
    // 删除上一个相同的请求
    removePending(requestData, true)

    // 实例化取消请求,并同时注入pending
    config.cancelToken = new CancelToken(c => {
      pending[requestData] = c
    })

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

// 请求完成后
service.interceptors.response.use(
  response => {
    // 把已经完成的请求从 pending 中移除
    const requestData = getRequestIdentify(response.config)
    removePending(requestData)

    // ...
  },
  error => {
    // ...
  }
)

3. token 拦截

token是比较常见的前后端校验拦截方式

// 引入store
import store from 'store'

service.interceptors.request.use(config => {
  // ...上面的代码

  const token = store.getters.GET_TOKEN
  if (!token) {
    // 取消该次请求
    pending[requestData]('cancelToken')
    store.dispatch('logOut')
  } else {
    // 在业务约定的headers里面某个字段定义token,方便后端提取校验
    config.headers['Authorization'] = token
  }

  return config
})

4. 接口白名单

所谓白名单,就是不用任何校验权限(token)的接口,比如用户登录、用户注册、修改用户密码等等

// 定义接口白名单
const noLogin = [
  // ...
  '/login',
  '/register'
]

service.interceptors.request.use(config => {
  // ...上面代码

  if (!noLogin.includes(config.url.replace(config.baseURL, ''))) {
    // 当不在白名单内时则校验token
    // ...上面代码
  }

  return config
})

5. token 自动登录

通过每次请求完成后,如果该请求返回的是登录失效(401),则用一个数组装在该次的返回对象config

然后重启发起该获取 token 请求,完成后把数组 map 重新发起一次请求,然后清除数组

前提是把账号密码记录存储在浏览器或者有刷新 token 接口

准备好以下这些方法

// 是否正在刷新的标记
const isRefreshing = false

// 重试队列,每一项将是一个待执行的函数形式
const retryRequests = []

// 重新请求流程处理
async function reRequest(response) {
  const { config } = response
  if (!isRefreshing) {
    isRefreshing = true
    const reRes = await refreshTokenFn()
    if (reRes) {
      config.headers['Authorization'] = store.getters.GET_TOKEN

      // 已经刷新了token,将所有队列中的请求进行重试
      retryRequests.map(cb => cb(store.getters.GET_TOKEN))

      // 清空列队
      retryRequests = []

      config.baseURL = ''

      isRefreshing = false
      return service(config)
    } else {
      // 刷新token失败,跳回登录页
      store.dispatch('logOut')
    }
    isRefreshing = false
  } else {
    return new Promise(resolve => {
      // 将resolve放进列队,用一个函数形式保存,等token刷新后直接执行
      retryRequests.push(token => {
        config.baseURL = ''
        config.headers['Authorization'] = token
        resolve(service(config))
      })
    })
  }
}

// 刷新token方法
async function refreshTokenFn() {
  try {
    const { loginName, password } = store.getters.GET_USER_INFO

    // 自己定义的获取方法
    const res = await store.dispatch('getToken', { loginName, password })
    return res
  } catch (error) {
    return false
  }
}

使用

// 请求前
service.interceptors.request.use(config => {
  const token = store.getters.GET_TOKEN
  if (!token) {
    // 当token不存在时,自动重新发起请求
    return reRequest({ config })
  } else {
    config.headers['Authorization'] = token
  }

  return config
})

service.interceptors.response.use(response => {
  const res = response.data

  if (res.status === 401) {
    // 登录失效
    // 重新刷新token并发起请求
    return reRequest(response)
  }

  return data
})

6. 导出封装 GET/POST/DELETE/PUT/PATCH/...请求函数

自定义请求参数位置,在使用实例好的service(params)时,通过传参数的 params 或者 data 决定放置位置

如果有传参数,则会覆盖默认的

参数 含义 默认值
url 请求地址 ——
method 请求类型,有 GET/POST/DELETE/PUT/PATCH 可选 GET
params 请求参数在url?后面时,则把参数放在 params,适用于 GET/DELETE null
data 请求参数在body体内时,则把参数放在 data 里,适用于 POST/PUT/PATCH null
headers 请求头,跟axios.create({ headers })一样 ——
responseType 服务器响应的数据类型,可选'arraybuffer', 'blob', 'document', 'json', 'text', 'stream' json
/**
 * get请求方法
 * @export axios
 * @param {String} url - 请求地址
 * @param {Object} params - 请求参数
 * @returns
 */
export const get = (url, params = {}) => {
  params.t = new Date().getTime() // get方法加一个时间参数,解决ie下可能缓存问题.
  return service({
    url: url,
    method: 'GET',
    params
  })
}

/**
 * delete请求方法
 * @export axios
 * @param {String} url - 请求地址
 * @param {Object} params - 请求参数
 * @returns
 */
export const del = (url, params = {}) => {
  params.t = new Date().getTime() // get方法加一个时间参数,解决ie下可能缓存问题.
  return service({
    url: url,
    method: 'DELETE',
    params
  })
}

/**
 * post请求方法
 * @export axios
 * @param {String} url - 请求地址
 * @param {Object} data - 请求参数
 * @returns
 */
export const post = (url, data = {}) => {
  return service({
    url,
    method: 'POST',
    data
  })
}

/**
 * put请求方法
 * @export axios
 * @param {String} url - 请求地址
 * @param {Object} data - 请求参数
 * @returns
 */
export const put = (url, data = {}) => {
  return service({
    url,
    method: 'PUT',
    data
  })
}

/**
 * patch请求方法
 * @export axios
 * @param {String} url - 请求地址
 * @param {Object} data - 请求参数
 * @returns
 */
export const patch = (url, data = {}) => {
  return service({
    url,
    method: 'PATCH',
    data
  })
}

/**
 * 当以上方法不满足,则自定义参数和配置请求
 * @param {Object} options
 */
export const fetch = options => service(options)

源码

import axios from 'axios'
import store from 'store'
import { Message, MessageBox } from 'element-ui'

// 创建axios实例
const service = axios.create({
  baseURL: process.env.BASE_API,
  validateStatus: status => status < 500, // 拦截状态码大于500
  headers: {
    common: { Accept: 'application/json; charset=UTF-8' },
    patch: { 'Content-Type': 'application/json; charset=UTF-8' },
    post: { 'Content-Type': 'application/json; charset=UTF-8' },
    put: { 'Content-Type': 'application/json; charset=UTF-8' }
  },
  timeout: 60000 // 请求超时时间
})

const paramsList = ['get', 'delete', 'patch']
const dataList = ['post', 'put']
const isTypeList = method => {
  if (paramsList.includes(method)) {
    return 'params'
  } else if (dataList.includes(method)) {
    return 'data'
  }
}

const pending = {}
const CancelToken = axios.CancelToken
const removePending = (key, isRequest = false) => {
  if (pending[key] && isRequest) {
    pending[key]('取消重复请求')
  }
  delete pending[key]
}
const getRequestIdentify = (config, isResult = false) => {
  let url = config.url
  if (isResult) {
    url = config.baseURL + config.url.substring(1, config.url.length)
  }
  const params = { ...(config[isTypeList(config.method)] || {}) }
  delete params.t
  return encodeURIComponent(url + JSON.stringify(params))
}

// 不需要token的接口
const noLogin = [
  '/account/random',
  '/account/token',
  '/account/defaultPassword',
  '/account/changeDefaultPassword'
]

// 是否正在刷新的标记
// const isRefreshing = false

// 重试队列,每一项将是一个待执行的函数形式
// const retryRequests = []

// request拦截器
service.interceptors.request.use(
  config => {
    const requestData = getRequestIdentify(config, true)
    removePending(requestData, true)

    config.cancelToken = new CancelToken(c => {
      pending[requestData] = c
    })

    if (!noLogin.includes(config.url.replace(config.baseURL, ''))) {
      const token = store.getters.GET_TOKEN
      if (!token) {
        // 当token不存在时,自动重新发起请求
        // return reRequest({ config })

        // 取消该次请求
        pending[requestData]('cancelToken')
        store.dispatch('logOut')
      } else {
        config.headers['Authorization'] = token
      }
    }

    // 处理为空的参数,设置为null
    handlerNullParams(config)

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

// response拦截器
service.interceptors.response.use(
  response => {
    // 把已经完成的请求从 pending 中移除
    const requestData = getRequestIdentify(response.config)
    removePending(requestData)
    const res = response.data

    if (res.status === 401) {
      // 登录失效
      MessageBox.alert('登录失效,请重新登录', '权限提示', {
        confirmButtonText: '退出',
        callback: () => store.dispatch('logOut')
      })
      // 重新刷新token并发起请求
      // return reRequest(response)
    } else if (res.status !== 200) {
      Message.error(res.msg || '系统异常')
      return Promise.reject(res.msg || '系统异常')
    }

    return res
  },
  error => {
    if (
      !(
        error &&
        (error.message === '取消重复请求' ||
          ~error.message.indexOf('cancelToken'))
      )
    ) {
      if (error.code === 'ECONNABORTED') {
        Message.error('请求超时')
      } else if (error && error.response) {
        // error.response.status
        Message.error(error.response.data.msg || '系统异常')
      } else {
        Message.error('系统异常')
        console.log(error)
      }
    }
    return Promise.reject(error)
  }
)

export default service

// 重新请求流程处理
// async function reRequest(response) {
//   const { config } = response
//   if (!isRefreshing) {
//     isRefreshing = true
//     const reRes = await refreshTokenFn()
//     if (reRes) {
//       config.headers['Authorization'] = store.getters.GET_TOKEN

//       // 已经刷新了token,将所有队列中的请求进行重试
//       retryRequests.map(cb => cb(store.getters.GET_TOKEN))

//       // 清空列队
//       retryRequests = []

//       config.baseURL = ''

//       isRefreshing = false
//       return service(config)
//     } else {
//       // 刷新token失败,跳回登录页
//       store.dispatch('logOut')
//     }
//     isRefreshing = false
//   } else {
//     return new Promise((resolve) => {
//       // 将resolve放进列队,用一个函数形式保存,等token刷新后直接执行
//       retryRequests.push(token => {
//         config.baseURL = ''
//         config.headers['Authorization'] = token
//         resolve(service(config))
//       })
//     })
//   }
// }

// // 刷新token方法
// async function refreshTokenFn() {
//   try {
//     const { loginName, password } = store.getters.GET_USER_INFO
//     const res = await store.dispatch('getToken', { loginName, password })
//     return res
//   } catch (error) {
//     return false
//   }
// }

/**
 * get请求方法
 * @export axios
 * @param {String} url - 请求地址
 * @param {Object} params - 请求参数
 * @returns
 */
export const get = (url, params = {}) => {
  params.t = new Date().getTime() // get方法加一个时间参数,解决ie下可能缓存问题.
  return service({
    url: url,
    method: 'GET',
    params
  })
}

/**
 * delete请求方法
 * @export axios
 * @param {String} url - 请求地址
 * @param {Object} params - 请求参数
 * @returns
 */
export const del = (url, params = {}) => {
  params.t = new Date().getTime() // get方法加一个时间参数,解决ie下可能缓存问题.
  return service({
    url: url,
    method: 'DELETE',
    params
  })
}

/**
 * post请求方法
 * @export axios
 * @param {String} url - 请求地址
 * @param {Object} data - 请求参数
 * @returns
 */
export const post = (url, data = {}) => {
  return service({
    url,
    method: 'POST',
    data
  })
}

/**
 * put请求方法
 * @export axios
 * @param {String} url - 请求地址
 * @param {Object} data - 请求参数
 * @returns
 */
export const put = (url, data = {}) => {
  return service({
    url,
    method: 'PUT',
    data
  })
}

/**
 * patch请求方法
 * @export axios
 * @param {String} url - 请求地址
 * @param {Object} data - 请求参数
 * @returns
 */
export const patch = (url, data = {}) => {
  return service({
    url,
    method: 'PATCH',
    data
  })
}

/**
 * post上传文件请求方法
 * !! 必须使用formData方式
 * @export axios
 * @param {String} url - 请求地址
 * @param {Object} data - 请求参数
 * @returns
 */
export const postFile = (url, data = {}) => {
  return service({
    url,
    method: 'POST',
    data,
    headers: {
      'Content-Type': 'multipart/form-data'
    },
    timeout: 1000 * 60 * 3
  })
}

/**
 * get导出文件
 * @export axios
 * @param {String} url - 请求地址
 * @param {Object} data - 请求参数
 * @returns
 */
export const getExport = (url, params = {}) => {
  return service({
    url,
    method: 'GET',
    params,
    responseType: 'blob',
    timeout: 1000 * 60 * 3
  })
}

/**
 * 当以上方法不满足,则自定义参数和配置请求
 * @param {Object} options
 */
export const fetch = options => service(options)

结语

广州找工作img...简历仿佛入了海底一下...

个人博客