关于axios的二次封装

1,488 阅读12分钟

一、了解axios

1、axios是什么

官方解释:

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中

ajax/fetch/axios 的区别:

ajax是一种技术统称,基于XMLHttpRequest;

fetch是具体API,基于promise设计的,采用模块化设计;

axios是一个基于promise封装的网络请求库,它是基于XHR进行二次封装。

Ajax、Fetch、Axios三者之间的关系可以用一张图来清晰的表示,如图:

image.png

2、为什么要封装axios

axios 的 API 很友好 也很全面,你完全可以很轻松地在项目中直接使用。

不过随着项目规模增大,如果每发起一次HTTP请求,就要把这些比如设置超时时间、设置请求头、根据项目环境判断使用哪个请求地址、错误处理等等操作,都需要写一遍

这种重复劳动不仅浪费时间,而且让代码变得冗余不堪,难以维护。为了提高我们的代码质量,我们应该在项目中二次封装一下 axios 再使用

3、如何封装:

你需要和 后端协商好一些约定,请求头,状态码,请求超时时间.......

  • 设置接口请求前缀:根据开发、测试、生产环境的不同,前缀需要加以区分

  • 请求头:来实现一些具体的业务,必须携带一些参数才可以请求(例如:会员业务)

  • 状态码:根据接口返回的不同status,来执行不同的业务,这块需要和后端约定好

  • 请求方法:根据get、post等方法进行一个再次封装,使用起来更为方便

  • 请求拦截器:根据请求的请求头设定,来决定哪些请求可以访问

1.当发送网络请求时, 在页面中添加一个loading组件, 作为动画
2.某些请求要求用户必须登录, 判断用户是否有token, 如果没有token跳转到login页面
3.对请求的参数进行序列化(看服务器是否需要序列化) // config.data = qs.stringify(config.data)
4.比如config中的一些信息不符合服务器的要求
  • 响应拦截器:这块就是根据后端返回来的状态码判定执行不同业务
switch (err.response.status) {
  case 400:
    err.message = '请求错误'
    break
  case 401:
    err.message = '未授权的访问'
    break
}

前端发送请求--请求拦截器--服务器--服务器返回消息--拦截的统一处理--前端获取到消息

axios请求方法

主要有get,post,put,patch,delete

get方法

写法:

调用型

 axios.get('/data.json',{
      params:{
        id:12
      }
    }).then((res)=>{
      console.log(res)
    })

axios()型

axios({
  method:'get',
  url:'/data.json',
  params:{
    id:12
  }
}).then((res)=>{
     console.log(res)
})

post方法

写法

调用型

axios.post('/post',data).then((res)=>{
  console.log(res)
})

axios()型

axios({
  method:'post',
  url:'/post',
  data:data
}).then(...)

二、封装axios

1.引入库

npm install axios qs

2.建立封装axios实例文件

因为项目一般都是工程化,所以我们得建立一个文件来单独封装axios,这样才能更加清晰。

通常我们在项目src目录下新建utils或者api文件夹,然后在其中新建 request.js或者http.js文件,这个文件是主要书写axios的封装过程。

3.导入所需依赖

// 导入axios
import axios from 'axios'

4.环境区分:开发、测试、生产

// webpack里面有个node环境变量:process.env.NODE_ENV
// 根据环境变量进行接口区分:
switch (process.env.NODE_ENV) {
  case "production":
    axios.defaults.baseURL = "http://api.wuproduction.cn"
    break
  case "test":
    axios.defaults.baseURL = "http://wutest.cn"
    break
  default:
    axios.defaults.baseURL = "http://127.0.0.3000"
}

package.json文件可执行命令里配置:

"scripts": {
  "serve": "vue-cli-service serve",
  "serve:test": "set NODE_ENV=test&&vue-cli-service serve",
  "serve:production": "set NODE_ENV=production&&vue-cli-service serve",
  ......
}

5.设置请求超时时间

// 设置请求超时时间10s
axios.defaults.timeout = 10000

6.设置跨域是否允许携带cookie

// 跨域是否允许携带cookie
axios.defaults.withCredentials = true

7.设置请求传递数据的格式

import qs from 'qs'

// 设置请求传递数据的格式 通常JSON格式
// x-www-form-urlencoded
axios.defaults.headers['Content-Type'] = 'application/x-www-form-urlencoded'
// 对于post请求  (安装qs依赖 第三方库)
axios.defaults.transformRequest = data => qs.stringify(data)  //这个方法是把对象变成xxx=xxx

8.设置请求拦截器

// 设置请求拦截器
// 通常token校验,接收服务器返回的token,存储到vuex或sessionStorage中,每一次向服务器发请求,我们应该把token带上
axios.interceptors.request.use(config => {
  // 1.当发送网络请求时, 在页面中添加一个loading组件, 作为动画
  // 2.某些请求要求用户必须登录, 判断用户是否有token, 如果没有token跳转到login页面
  // 3.对请求的参数进行序列化(看服务器是否需要序列化) // config.data = qs.stringify(config.data)
  // 4.比如config中的一些信息不符合服务器的要求
  config.headers.Authorization = sessionStorage.getItem('token')
  return config  // 一定要记得返回
}, error => {
  return Promise.reject(error)
})

9.设置响应拦截器

根据后端商定自定义响应的http状态码;axios统一处理异常

// 设置响应拦截器  axios统一处理异常
// 自定义响应成功的http状态码
axios.interceptors.response.use(response => {
  // 成功直接返回 一般2/3开头的状态码
  return response.data
}, error => {
  let { response } = error
  if (response) {
    // 服务器返回结果了
    switch (response.status) {
      case 400:
        error.message = '请求错误'
        break
      case 401:   //权限
        error.message = '未授权的访问'
        break
      case 403:  //服务器拒绝执行(token过期)
        error.message = '禁止访问'
        break
      case 404:
        error.message = '找不到页面,当前请求接口不存在'
        break
    }
  } else {
    // 服务器没有返回结果
    if (!window.navigator.onLine) {
      // 断网处理,可以跳转到断网页面
      return
    }
    return Promise.reject(error)
  }
})

export default axios

10.axios拦截器的原理

拦截器原理:

创建一个chn数组,数组中保存了拦截器相应方法以及dispatchRequest(dispatchRequest这个函数调用才会真正的开始下发请求),

把请求拦截器的方法放到chn数组中dispatchRequest的前面,

把响应拦截器的方法放到chn数组中dispatchRequest的后面,

把请求拦截器和响应拦截器 forEach 将它们分别 unshift, push 到chn数组中,为了保证它们的执行顺序,需要使用promise,以出队列的方式对chn数组中的方法挨个执行。

axios原理简化:
function Axios() {
  this.interceptors = {
    //两个拦截器
    request: new interceptorsManner(),
    response: new interceptorsManner()
  }
}

//真正的请求
Axios.prototype.request = function () {
  let chain = [dispatchRequest, undefined];//这儿的undefined是为了补位,因为拦截器的返回有两个
  let promise = Promise.resolve();
  
  //将两个拦截器中的回调加入到chain数组中
  this.interceptors.request.handler.forEach((interceptor) => {
    // 将请求拦截器压入数组的最前面
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  })
  this.interceptors.response.handler.forEach((interceptor) => {
    // 将响应拦截器压入数组的最后面
    chain.push(interceptor.fulfilled, interceptor.rejected);
  })
  
  while (chain.length) {
    //promise.then的链式调用,下一个then中的chain为上一个中的返回值,每次会减去两个
    //这样就实现了在请求的时候,先去调用请求拦截器的内容,再去请求接口,返回之后再去执行响应拦截器的内容
    promise = promise.then(chain.shift(), chain.shift());
  }
}

function interceptorsManner() {
  // 用于存放Axios拦截行为及数据请求的Promise链条
  this.handler = [];
}

// 增加拦截器
interceptorsManner.prototype.use = function (fulfilled, rejected) {
  //将成功与失败的回调push到handler中
  this.handler.push({
    fulfilled: fulfilled,
    rejected: rejected
  })
}

//类似方法批量注册,实现多种请求
util.forEach(["get", "post", "delete"], (methods) => {
  Axios.prototype[methods] = function (url, config) {
    return this.request(util.merge(config || {}, { //合并
      method: methods,
      url: url
    }))
  }
})

11.取消请求

方法一:

axios取消请求:创建一个取消请求函数,在需要的地方调用这个函数就行(封装了一个cancel函数)在别的方法中调用cancel函数:cancel();

let send_btn = document.getElementById("send_btn")       //发送请求按钮
let cancel_btn = document.getElementById("cancel_btn")	 //取消请求按钮
let cancel = null;
send_btn.onclick = function () {
  if (cancel !== null) {
    cancel()     //如果上一次的请求还在继续,则取消
//一般情况下我们需要判断请求是否发出去了再取消,如果用户频繁点击请求按钮,这时我们需要取消之前的请求而发送最后一个请求
  }
  axios({
    method: "get",
    url: "http://localhost:3000/test",
    cancelToken: new axios.CancelToken(function (c) {
    //首先把cancel赋值为null,如果每次点击的话cancel会赋值成c,也就是说再次点击的时候cancel就不是null就会进入if语句,
    //取消请求,等请求结束后就会把cancel赋值为null这样下次点击就不会进入if语句。
      cancel = c
    })
  }).then(response => {
    //处理响应数据
    //将 cancel 的值初始化
    cancel = null
  }).catch(reason => {
    //错误处理
  })
}

cancel_btn.onclick = function () {
  //取消请求
  cancel()
}

这个取消请求要在axios里面而不是单独封装的一个函数,给每个按钮设置这一个函数的时候, 当点击取消按钮的时候每个相应的按钮就会执行这个取消请求的函数

方法二:

//利用source对象创建canceltoken
let send_btn = document.getElementById("send_btn") 	 	//发送请求按钮
let cancel_btn = document.getElementById("cancel_btn")  //取消请求按钮
let source = axios.CancelToken.source();
send_btn.onclick = function () {
  // 判断上一次的请求是否还在继续,如果还在继续,则取消上一次的请求
  if (source.token._listeners !== undefined) {
    source.cancel("取消请求")
    source = axios.CancelToken.source()
  }
  axios.get('http://localhost:3000/front-end/axios', {
    cancelToken: source.token
  }).then(response => {
    // 处理响应
  }).catch(reason => {
    if (axios.isCancel(reason)) {
      console.log("取消请求", reason)
    } else {
      //错误处理
    }
  })
}
//取消按钮点击,则取消请求
cancel_btn.onclick = function () {
  source.cancel("请求已被取消")
  source = axios.CancelToken.source()
}

两种方法的区别与应用

第一种方法每个请求有独立的token,适合每个请求分情况是否进行单独取消;

第二种方法是多个请求共用一个token,适合在某一时间同时取消所有请求。

实际应用场景:

vue项目使用 axios 封装,当token过期或者失效后,弹窗提示用户登陆失效,重新登陆。在一些页面初始化时调用了多个接口,发起多个http请求,会出现多次弹出重新登陆的弹框。

解决方案:

1、使用axios中的CancelToken,在判断登陆失败时,取消后面的API请求

关键代码:
//请求拦截器
const http = axios.create()
http.interceptors.request.use((config) => {
    config.cancelToken = new axios.CancelToken((cancel) => {
        //使用vuex 定义pushCancel,请求进来存入
        store.dispatch('pushCancel', {cancelToken:cancel})
      })
}),
//响应拦截器
http.interceptors.response.use((response) => {
  if (response.status === 200) {
    let result = response.data
    if(result.code !== 200 && result.code) {
      const msg = result.msg;
  //判断登陆状态, 取消后续请求
      if (result.code === 401) {
        store.dispatch('clearCancel');
        Message({
          message: '登录状态已过期,请重新登陆!',
          type: 'error'
        });
       location.href = '/login';
        return false
      }
      Message({
        message: msg,
        type: 'error'
      });
    } else {
      return response.data
    }
  }else {
    console.log(`Error Message:${response.status} ${response.statusText}`);
  }
},error{
  if (axios.isCancel(error)) {
  // 使用isCancel 判断是否是主动取消请求
    return new Promise(() => {});
  }else {
 let _errorMsg = error.response ? error.response.data.msg : error.message;
    if (_errorMsg !== '' && _errorMsg !== null) {
        Message({
          message: _errorMsg,
          type: 'error',
          duration: 3 * 1000,
          dangerouslyUseHTMLString: false
        });
      }
      return Promise.reject(error);
  }
})

2、跳转路由,取消上一个页面未请求成功的API

使用vuex全局变量存取
 
const store = new Vuex.Store({
    state: {
        axiosCancelArr:[],
    },
    mutations: {
        PUSH_CANCEL(state, cancel){
            state.axiosCancelArr.push(cancel.cancelToken);
          },
      
          CLEAR_CANCEL(state){
            state.axiosCancelArr.forEach(e=>{
              e && e()
            });
            state.axiosCancelArr = []
          }
    },
    actions:{
        pushCancel({commit}, cancel){
          commit('PUSH_CANCEL', cancel)
        },
        clearCancel({commit}){
          commit('CLEAR_CANCEL');
        }
      }
})
export default store

使用路由拦截器,跳转路由 调用clearCancel方法,清空存入的信息。终止未请求成功的请求:

router.beforeEach((to, from, next) => { 
  store.dispatch('clearCancel');
  next(); 
})

总结:

1、当页面初始化调用多个API时,使用axios响应拦截器判断状态码,如果登陆token失效,弹出请重新登陆的提示,取消后面的请求,跳转到登陆页面。

2、跳转路由时使用路由拦截器,清除vuex存入的全局变量,取消后面的API请求。

3、最近看到的一种解决方案:

image.png

12.axios防止多次重复请求

如何判断重复请求

请求方式、请求URL地址和请求参数都一样时,我们就可以认为请求是一样的。因此在每次发起请求时,我们就可以根据当前请求的请求方式、请求 URL 地址和请求参数来生成一个唯一的 key,同时为每个请求创建一个专属的 CancelToken,然后把 key 和 cancel 函数以键值对的形式保存到 Map 对象中,使用 Map 的好处是可以快速的判断是否有重复的请求:

import qs from 'qs'

const pendingRequest = new Map();
// GET -> params;POST -> data
const requestKey = [method, url, qs.stringify(params), qs.stringify(data)].join('&'); 
const cancelToken = new CancelToken(function executor(cancel) {
  if(!pendingRequest.has(requestKey)){
    pendingRequest.set(requestKey, cancel);
  }
})

当出现重复请求的时候,我们就可以使用 cancel 函数来取消前面已经发出的请求,在取消请求之后,我们还需要把取消的请求从 pendingRequest 中移除。现在我们已经知道如何取消请求和如何判断重复请求,下面我们来介绍如何取消重复请求。

如何取消重复请求

1、定义辅助函数

// generateReqKey:用于根据当前请求的信息,生成请求 Key
function generateReqKey(config) {
  const { method, url, params, data } = config;
  return [method, url, Qs.stringify(params), Qs.stringify(data)].join("&");
}

// addPendingRequest:用于把当前请求信息添加到pendingRequest对象中
const pendingRequest = new Map();
function addPendingRequest(config) {
  const requestKey = generateReqKey(config);
  config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
    if (!pendingRequest.has(requestKey)) {
       pendingRequest.set(requestKey, cancel);
    }
  });
}

// removePendingRequest:检查是否存在重复请求,若存在则取消已发的请求
function removePendingRequest(config) {
  const requestKey = generateReqKey(config);
  if (pendingRequest.has(requestKey)) {
     const cancelToken = pendingRequest.get(requestKey);
     cancelToken(requestKey);
     pendingRequest.delete(requestKey);
  }
}

2、设置请求拦截器

axios.interceptors.request.use(
  function (config) {
    removePendingRequest(config); // 检查是否存在重复请求,若存在则取消已发的请求
    addPendingRequest(config); // 把当前请求信息添加到pendingRequest对象中
    return config;
  },
  (error) => {
     return Promise.reject(error);
  }
);

3、设置响应拦截器

axios.interceptors.response.use(
  (response) => {
     removePendingRequest(response.config); // 从pendingRequest对象中移除请求
     return response;
   },
   (error) => {
      removePendingRequest(error.config || {}); // 从pendingRequest对象中移除请求
      if (axios.isCancel(error)) {
        console.log("已取消的重复请求:" + error.message);
      } else {
        // 添加异常处理
      }
      return Promise.reject(error);
   }
);

---万能模板:---

import axios from 'axios'
const  baseURL = "http://xxxx";
const instance = axios.create({ baseURL: baseURL, timeout: 30000 });
// 定义存放已发送但未响应的请求
const requestMap = new Map();
// 获取请求的唯一标识
const getRequestIdentify = (config, isReuest = false) => {
  // 由于不同项目的实现方式略有不同,url、params和data的格式可能略有不同,使用该方法时要根据实际情况进行调试修改
  let url = config.url
  if (isReuest) {
    url = url.startsWith('/') ? baseURL + url : baseURL + '/' + url;
  }
  return config.method === 'get' ? encodeURI(url + JSON.stringify(config.params)) : encodeURI(url + (typeof config.data === 'string' ? config.data : JSON.stringify(config.data)))
}
// HTTPrequest拦截
instance.interceptors.request.use(config => {
  const requestData = getRequestIdentify(config, true);
  if (requestMap.has(requestData)) {
    // map中存在该请求,不再重复发起
    return Promise.reject(new Error('重复请求'))
  } else if (!config.canReapeat) {
    // 通过在请求中设置 canReapeat 参数,允许特殊接口的重复提交
    // 若map中没有该请求,则将请求存入map,并发起请求
    requestMap.set(requestData, true)
  }
  //...
  return config
}, error => {
  return Promise.reject(error)
})
 
// HTTPresponse拦截
instance.interceptors.response.use(res => {
  const requestData = getRequestIdentify(res.config);
  // 请求获得响应后,将该请求从map中移除,以使后面的请求正常发送。
  if (requestMap.has(requestData)) {
    requestMap.delete(requestData)
  }
  let data = res.data;
  if(data.code==666){
    return data;
  }
}, (error) => {
  return Promise.reject(new Error(error))
})

axios封装没有一个绝对的标准,且需要结合项目中实际场景业务来设计

好了,此文到这就结束了,如有错误,欢迎大佬指正~

完整代码: github