axios引发的一系列问题

1,858 阅读4分钟

前后端分离模式下接口的请求方式主要以下两种

  • fetch API
  • axios库
    首先来回顾一下基础知识

post请求content-type

HTTP协议规定post请求的数据必须放在请求体内,content-type规定请求体数据的编码格式,服务端需要正确解析不同编码格式的请求体数据。Content-Type 用于规定客户端通过http或https协议向服务器发起请求时,传递的请求体中数据的编码格式。因为get请求是直接将请求数据以键值对通过&号连接(key1=value1&key2=value2)的方式附加到url地址后面,不在请求体中,所以get请求中不需要设置Content-Type。

application/x-www-form-urlencoded(默认格式)

  • 数据以key=value&key=value的形式传递
  • key和value中的特殊字符会进行编码,如空格被编译为20%

application/json

  • 上一种方式不能满足传递比较复杂的数据结构,如对象数组多层嵌套。直接传递json对象时使用。Angular中的Ajax,默认就是提交 JSON 字符串

multipart/form-data(表单上传文件,暂不深入)

text/xml

序列化

  • 序列化是将对象的状态信息转换为可以存储或传输的形式的过程,例如{a:1, b:2}转换成a=1&b=2
  • 当content-type为application/x-www-form-urlencoded 格式进行post请求传递数据时,需要使用qs.stringify来转换data的格式。
  • 注意在传递数组,对象这种形式时arr = [1,2,3]会被转为a[0]=1&a[1]=2&a[2]=3,obj= { a: 1, b: 2 }会被转为obj[a]=1&obj[b]=2这种形式,所以传递复杂数据类型时最好使用application/json
  • 常用qs库
    • qs.stringify(obj)序列化
    • qs.parse(string)反序列化

拦截器

设置全局loading

import axios from 'axios'
import { Toast } from 'vant'
const instance = axios.create()
instance.defaults.headers.post['Content-type'] = 'application/x-www-form-urlencoded'

let pendingRequestCount = 0 // 计数器,多个请求时进行loading合并
function showLoading () {
  if (pendingRequestCount === 0) {
    Toast.loading({
      message: '加载中...',
      forbidClick: true
    })
  }
  pendingRequestCount++
}

function hideLoading () {
  if (pendingRequestCount === 0) {
    Toast.clear()
  }
  pendingRequestCount-- // 注意不是if...else...关系 调用即进行数目更新
}
// 添加请求拦截器
instance.interceptors.request.use(function (config) {
  showLoading()
  return config
}, function (error) {
  return Promise.reject(error)
})
// 添加响应拦截器
instance.interceptors.response.use(
  res => {
    hideLoading()
    return res
  },
  error => {
    return Promise.reject(error)
  })
export default instance

结合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.baseURL + config.url.substring(1, config.url.length)
    : config.url
  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) {
    console.log(pending[key])
    pending[key]('取消重复请求')
  }
  delete pending[key]
}

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

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

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

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

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

我的使用

  • 业务场景(每个网络请求都新增一个参数project_id)
// 添加请求拦截器
instance.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  if (config.method === 'post' && config.data.indexOf('project_id') === 0) {
    config.data = config.data + `&project_id=${getQueryString('project_id')}`
  } else if (config.method === 'get' && (!config.params || !config.params.project_id)) {
    config.params = {
      ...config.params || {},
      project_id: getQueryString('project_id')
    }
  }
  return config
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})
// 发送请求前处理request的数据
instance.defaults.transformRequest = [function (data) {
  return data + `&project_id=${getQueryString('project_id')}`
}]

transformRequest和interceptors的区别?

  • 两者的执行顺序不同,先 request interceptors再transformRequest
  • transformRequest主要对数据data进行处理
  • transformRequest是同步的,拦截器可异步

我在transformRequest中做了什么:post数据序列化

const instance = axios.create({
  transformRequest: [function (data) {
    data = qs.stringify(data)   // 进行post请求数据的序列化
    return data
  }]
}
  • 其实在拦截器中做也可以
if (config.method === 'post') {
    config.data = qs.stringify({
      ...config.data
    })
  }
  • 不进行qs.stringfy当直接传递JS对象默认会被转换为application/json会发送json格式,qs.stringfy让axios发送表单请求形式的键值对post数据

我在响应拦截器里做了什么:res的处理

  • 起因:res.data.data,res.data.error,res.data.msg繁琐的写法,层次深,更好的做法是在业务代码中这样使用res的数据:res.data,res.error,res.msg,添加响应拦截器或transformResponse
instance.interceptors.response.use(
  res => {
    // 对响应数据做点什么
    if (res.data && res.error + '' && handleError[res.error]) {
      handleError[res.error](res)
    }
    return res.data
  },
  error => {
    // 对响应错误做点什么
    return Promise.reject(error)
  })

设计请求层(axios封装和api管理)

封装有两种方法:一种是创建一个axios实例,另外一种是直接修改axios的defaults

api
├── config.js // baseUrl,Authorization,Content-Type拦截器
├── index.js // 引入配置文件和各个功能模块
├── home.js // api接口模块化
├── list.js
├── detail.js
└── order.js

环境的切换(online,pre,dev) | proxy切换

import api from '@/api' // 导入api接口
Vue.prototype.$api = api; // 将api挂载到vue的原型上直接在业务代码中调用`this.$api.xxx`而不是`import { xxx } from '@/api'`