还不会二次封装Axios?一篇文章手把手教你

383 阅读10分钟

使用Vue的时候,Axios几乎已经是必用的请求库了,但是为了更方便搭配项目使用,很多开发者会选择二次封装,Vue3就很多人选择二次封装elementPlus,那其实,Axios我们也是经常会去封装的。

封装有什么好处呢?

首先,封装的目的主要是便于全局化使用。

比如全局设置超时时间,固定接口的baseURL,实现请求拦截操作与响应拦截操作。

那现在我就来展示一下我经常使用的封装套路。

封装功能

首先是功能上的封装,我们新建一个js文件,我这里叫request.js

首先我们先导入axios和qs两个模块。

为什么要使用qs模块?

ajax请求的get请求是通过URL传参的(以?和&符连接),而post大多是通过json传参的。 qs是一个库。里面的stringify方法可以将一个json对象直接转为(以?和&符连接的形式)。 在开发中,发送请求的入参大多是一个对象。在发送时,如果该请求为get请求,就需要对参数进行转化。使用该库,就可以自动转化,而不需要手动去拼接

然后我这里还会用一个弹出层UI,我这里用elementUI,你也可以选择其他UI,灵活变通。但是最好不要全引入,单个引入弹出层组件就可以。

 // 导入axios
 import axios from 'axios'
 //导入QS
 import qs from 'qs'
 // 使用element-ui Message用以消息提醒
 import { Message} from 'element-ui';

导入之后,我们创建一个axios的实例,可以理解为对象吧。

 // 创建新的axios实例
 const service = axios.create({
   // 公共接口(暂未配置,预计写死)
   baseURL: "http://localhost:8081/api",
   // 超时时间 单位是ms
   timeout: 20 * 1000,
 })

Axios的官方文档也说明了创建实例的方法。

然后里面有一些配置项,比如baseURL,超时时间等,官网还要很多的配置,这里就不多说了。

此时这个实例service就是我们要用的axios了,你就当他是axios的对象。

请求拦截器

文档也提供了拦截器设置方法,我们调用这个方法,自己封装一下请求与响应拦截。

 // 添加请求拦截器
 axios.interceptors.request.use(function (config) {
     // 在发送请求之前做些什么
     return config;
   }, function (error) {
     // 对请求错误做些什么
     return Promise.reject(error);
   });

官方的拦截器是这样的。

我这里喜欢用箭头函数,所以是这样的:

 // 请求拦截器
 service.interceptors.request.use(config => {
   return config
 }, error => {
   Promise.reject(error)
 })

这里携带的config是一个数据配置项,每次发送请求后,整个axios的东西都会被我们获取到,然后我们这使用config接收。

那既然这是一个axios的数据包,那我们就可以添加修改里面的数据。

我们看看它源码对应的代码段,是TS写的,是一个泛型对象,对象中包含了一些设置参数。

图有些模糊,我贴个代码:

 export interface AxiosRequestConfig<D = any> {
   url?: string;
   method?: Method;
   baseURL?: string;
   transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[];
   transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[];
   headers?: AxiosRequestHeaders;
   params?: any;
   paramsSerializer?: (params: any) => string;
   data?: D;
   timeout?: number;
   timeoutErrorMessage?: string;
   withCredentials?: boolean;
   adapter?: AxiosAdapter;
   auth?: AxiosBasicCredentials;
   responseType?: ResponseType;
   xsrfCookieName?: string;
   xsrfHeaderName?: string;
   onUploadProgress?: (progressEvent: any) => void;
   onDownloadProgress?: (progressEvent: any) => void;
   maxContentLength?: number;
   validateStatus?: ((status: number) => boolean) | null;
   maxBodyLength?: number;
   maxRedirects?: number;
   socketPath?: string | null;
   httpAgent?: any;
   httpsAgent?: any;
   proxy?: AxiosProxyConfig | false;
   cancelToken?: CancelToken;
   decompress?: boolean;
   transitional?: TransitionalOptions;
   signal?: AbortSignal;
   insecureHTTPParser?: boolean;
 }

那我们就可以设置这些,至于这些配置项都是什么,我们可以前往官方文档查看。

在里面对基本上要操作的数据字段都写了注释。

请求拦截转换JSON数据:

 config.data = qs.stringify(config.data);

用qs转化一下,原因前面已经说了。

设置固定请求头:

   config.headers = {
       //配置请求头
       'Content-Type':'application/x-www-form-urlencoded' 
   }

携带参数/Token:

   if (localStorage.getItem('token')) {
     //携带token到axios参数
     config.headers.Authorization = '固定携带的头部';
     config.params = {
       //固定携带参数
     }
   }

这里是从浏览器内存读取token,你可以选择携带到头部。

当然,你也可以携带其他数据,也可以在config.params中携带一些其他参数,每次请求都会默认携带到后端。

你也可以选择在cookie里面获取:

   const token = getCookie('名称');//这里取token之前,需要先拿到token,存一下
   if(token){
       config.params = {'token':token} //如果要求携带在参数中
       config.headers.token= token; //如果要求携带在请求头中
   }

最后,不要忘记return config,不然设置的字段不会生效。

然后我们Axios因为是基于Promise的,所以我们最后可以使用Promise.reject捕捉他的错误信息。

Promise.reject会在error中返回一个Promise错误对象对象。

这里不懂请查阅Promise相关资料。

那为了方便查看,我就整个拦截器代码放出来了:

 // 请求拦截器
 service.interceptors.request.use(config => {
   //发请求前做的一些处理,数据转化,配置请求头,设置token,设置loading等,根据需求添加
   config.data = qs.stringify(config.data); //json数据转化
   config.headers = {
       'Content-Type':'application/x-www-form-urlencoded' //配置请求头
   }
   //注意使用token的时候需要引入cookie方法或者用本地localStorage等方法,推荐js-cookie
   //判断localStorage是否存在token
   if (localStorage.getItem('token')) {
     //携带token到axios参数
     config.headers.Authorization = '固定携带的头部';
     config.params = {
       //固定携带参数
     }
   }
   // const token = getCookie('名称');//这里取token之前,需要先拿到token,存一下
   // if(token){
   //     config.params = {'token':token} //如果要求携带在参数中
   //     config.headers.token= token; //如果要求携带在请求头中
   // }
   return config
 }, error => {
   Promise.reject(error)
 })

这部分就是捕捉错误的代码。

响应拦截器

响应拦截器将会搭配elementUI的弹出层提示组件,当返回响应报错时,自动弹出提示,优化用户体验。

官方是这样写的:

 // 添加响应拦截器
 axios.interceptors.response.use(function (response) {
     // 2xx 范围内的状态码都会触发该函数。
     // 对响应数据做点什么
     return response;
   }, function (error) {
     // 超出 2xx 范围的状态码都会触发该函数。
     // 对响应错误做点什么
     return Promise.reject(error);
   });

那我们还是使用箭头函数来写,这里我先给出所以代码,在分段解析。

 service.interceptors.response.use(response => {
     console.log("进入响应拦截器");
   //接收到响应数据并成功后的一些共有的处理,关闭loading等  
   return response
 }, error => {
    /***** 接收到异常响应的处理开始 *****/
   if (error && error.response) {
     // 根据响应码具体处理
     switch (error.response.status) {
       case 400:
         error.message = '错误请求'
         break;
       case 401:
         error.message = '未授权,请重新登录'
         break;
       case 403:
         error.message = '拒绝访问'
         break;
       case 404:
         error.message = '请求错误,未找到该资源'
         window.location.href = "/NotFound"
         break;
       case 405:
         error.message = '请求方法未允许'
         break;
       case 408:
         error.message = '请求超时'
         break;
       case 500:
         error.message = '服务器端出错'
         break;
       case 501:
         error.message = '网络未实现'
         break;
       case 502:
         error.message = '网络错误'
         break;
       case 503:
         error.message = '服务不可用'
         break;
       case 504:
         error.message = '网络超时'
         break;
       case 505:
         error.message = 'http版本不支持该请求'
         break;
       default:
         error.message = `连接错误${error.response.status}`
     }
   } else {
     // 超时处理
     if (JSON.stringify(error).includes('timeout')) {
       Message.error('服务器响应超时,请刷新当前页')
     }
     error.message = '连接服务器失败'
   }
   Message.error(error.message)
   /***** 处理结束 *****/
   return Promise.resolve(error.response)
 })

这里有一个返回的参数response

 service.interceptors.response.use(response => {
     console.log("进入响应拦截器");
   //接收到响应数据并成功后的一些共有的处理,关闭loading等  
   return response
 },

这个也是Promise的,所以,我们在正常运行的时候,会正常进入方法,所以返回接收的数据。

如果出现错误,他是不会进入到上面的方法的,而是进入error

 error => {
    /***** 接收到异常响应的处理开始 *****/
   if (error && error.response) {
     // 根据响应码具体处理
     switch (error.response.status) {
       case 400:
         error.message = '错误请求'
         break;
       case 401:
         error.message = '未授权,请重新登录'
         break;
       case 403:
         error.message = '拒绝访问'
         break;
       case 404:
         error.message = '请求错误,未找到该资源'
         window.location.href = "/NotFound"
         break;
       case 405:
         error.message = '请求方法未允许'
         break;
       case 408:
         error.message = '请求超时'
         break;
       case 500:
         error.message = '服务器端出错'
         break;
       case 501:
         error.message = '网络未实现'
         break;
       case 502:
         error.message = '网络错误'
         break;
       case 503:
         error.message = '服务不可用'
         break;
       case 504:
         error.message = '网络超时'
         break;
       case 505:
         error.message = 'http版本不支持该请求'
         break;
       default:
         error.message = `连接错误${error.response.status}`
     }else {
     // 超时处理
     if (JSON.stringify(error).includes('timeout')) {
       Message.error('服务器响应超时,请刷新当前页')
     }
     error.message = '连接服务器失败'
   }
   Message.error(error.message)
   /***** 处理结束 *****/
   return Promise.resolve(error.response)
 })

也就是进入以上代码。

那首先进入这个方法,我们先来一个判断。

 if (error && error.response) {
     //错误码判断
 }else{
     //超时处理
 }

这个判断,我去除中间的部分,先看这个判断。

如果有error对象,并且error对象有response参数时,我们此时就会确定这是请求状态错误。

为什么呢?因为error.response中的status会返回浏览器爆出的状态码。

那如果没有报状态码,那就说明非直接的错误,那就可能是超时了,我们在else中进一步处理。

状态码处理

那我们还是先看直接错误处理:

我们获取到状态码,根据不同状态码弹出不同错误提示,这里我们将错误提示文字报错到这个error中。

这里还只是保存错误信息,还没有调用elementUI弹出层哦!

是不是很方便呢?

进一步处理
 else {
     // 超时处理
     if (JSON.stringify(error).includes('timeout')) {
       Message.error('服务器响应超时,请刷新当前页')
     }
     error.message = '连接服务器失败'
 }

那如果没有状态码,基本上就是超时,获取其他问题。

那我们if判断一下看看是否超时,先使用JSON.stringify将对象转化为字符串。

includes方法是用于判断字符串中有没有对应字符串。

然后使用includes判断有没有timeout这个字符串,有就是超时了。

没有我们就默认给他抛出一个error.message = '连接服务器失败'

弹出提示:

不要忘了,我们还只是保存错误提示的字符串,没有调用elementUI的弹出层组件,我们最后调用一下。

   Message.error(error.message)

调用后不要忘了返回参数,我们需要使用Promise.resolve来返回一个error.response

Promise.resolve作用是将参数转为Promise对象。

具体请自行查阅相关资料,不懂就按照这个来,官方也是这样的。

暴露实例

最后不要忘记将整个封装后的实例暴露出去:

 //暴露文件
 export default service

全部代码

 /**** 全局封装axios配置与消息 ****/
 // 导入axios
 import axios from 'axios'
 //导入QS
 import qs from 'qs'
 // 使用element-ui Message用以消息提醒
 import { Message} from 'element-ui';
 ​
 ​
 // 创建新的axios实例
 const service = axios.create({
   // 公共接口(暂未配置,预计写死)
   baseURL: "http://localhost:8081/api",
   // 超时时间 单位是ms
   timeout: 20 * 1000,
 })
 ​
 ​
 // 请求拦截器
 service.interceptors.request.use(config => {
   //发请求前做的一些处理,数据转化,配置请求头,设置token,设置loading等,根据需求添加
   config.data = qs.stringify(config.data); //json数据转化
   config.headers = {
       'Content-Type':'application/x-www-form-urlencoded' //配置请求头
   }
   //注意使用token的时候需要引入cookie方法或者用本地localStorage等方法,推荐js-cookie
   //判断localStorage是否存在token
   if (localStorage.getItem('token')) {
     //携带token到axios参数
     config.headers.Authorization = '固定携带的头部';
     config.params = {
       //固定携带参数
     }
   }
   // const token = getCookie('名称');//这里取token之前,需要先拿到token,存一下
   // if(token){
   //     config.params = {'token':token} //如果要求携带在参数中
   //     config.headers.token= token; //如果要求携带在请求头中
   // }
   return config
 }, error => {
   Promise.reject(error)
 })
 ​
 ​
 // 响应拦截器
 service.interceptors.response.use(response => {
     console.log("进入响应拦截器");
   //接收到响应数据并成功后的一些共有的处理,关闭loading等  
   return response
 }, error => {
    /***** 接收到异常响应的处理开始 *****/
   if (error && error.response) {
     // 根据响应码具体处理
     switch (error.response.status) {
       case 400:
         error.message = '错误请求'
         break;
       case 401:
         error.message = '未授权,请重新登录'
         break;
       case 403:
         error.message = '拒绝访问'
         break;
       case 404:
         error.message = '请求错误,未找到该资源'
         window.location.href = "/NotFound"
         break;
       case 405:
         error.message = '请求方法未允许'
         break;
       case 408:
         error.message = '请求超时'
         break;
       case 500:
         error.message = '服务器端出错'
         break;
       case 501:
         error.message = '网络未实现'
         break;
       case 502:
         error.message = '网络错误'
         break;
       case 503:
         error.message = '服务不可用'
         break;
       case 504:
         error.message = '网络超时'
         break;
       case 505:
         error.message = 'http版本不支持该请求'
         break;
       default:
         error.message = `连接错误${error.response.status}`
     }
   } else {
     // 超时处理
     if (JSON.stringify(error).includes('timeout')) {
       Message.error('服务器响应超时,请刷新当前页')
     }
     error.message = '连接服务器失败'
   }
   Message.error(error.message)
   /***** 处理结束 *****/
   return Promise.resolve(error.response)
 })
 //暴露文件
 export default service
 ​

封装请求信息

我们这只是封装了功能配置方面的代码,我们需要进一步封装需要携带的参数和baseURL后面路径。

看看这个,注意,baseURLurl不是同一个东西。

baseURL是固定的请求地址,url是请求地址后的路径。

比如baseURL127.0.0.1/api/url/user,那这样,请求地址就是,127.0.0.1/api/user

开始封装

创建一个js文件,我这叫http.js

导入封装好功能的实例。

 // 导入封装好的axios实例
 import request from './request'

创建一个对象:

 const http ={
     //方法
 }

里面可以写常用请求方法:

 const http ={
     /**
      * methods: 请求
      * @param url 请求地址 
      * @param params 请求参数
      */
     get(url,params){
         const config = {
             method: 'get',
             url:url
         }
         //如果非空,则添加参数,下文同理
         if(params) config.params = params
         //调用封装好的axios实例,下文同理
         return request(config)
     },
     post(url,params){
         const config = {
             method: 'post',
             url:url
         }
         if(params) {
             config.data = params
             console.log(config.data) 
         }
         return request(config)
     },
     put(url,params){
         const config = {
             method: 'put',
             url:url
         }
         if(params) config.params = params
         return request(config)
     },
     delete(url,params){
         const config = {
             method: 'delete',
             url:url
         }
         if(params) config.params = params
         return request(config)
     }
 }

看看,这里面对不同请求都封装了方法,postgetput等等。

我们以post方法为例:

 post(url,params){
         const config = {
             method: 'post',
             url:url
         }
         if(params) {
             config.data = params
             console.log(config.data) 
         }
         return request(config)
 },

携带了两个参数,urlparamsparams是携带的参数。

创建一个配置对象config,对象method指定axios使用什么方法请求,url就不必说了。

然后给出一个判断:

 if(params) {
     config.data = params
 }

如果有参数传入,我们就给config对象添加一个data,将参数赋值给data

注意:config就当作axios实例,所以字段是固定的,这里必须叫data

然后返回中调用request,也就是axios实例,将配置携带在里面,这样这个config对象里面的配置就会与axios实例的字段信息相互补充,相当于为axios实例增加了methodurl以及数据(如果不为null的话)。

这一层请求信息的封装也就好了,目的是补充配置。

封装请求方法

我们在封装一次调用方法,便于调用请求。

创建一个js文件,我这是api.js

不罗嗦,贴上全部代码:

 import http from '../utils/http'
 /**
  *  @parms url 请求地址
  *  @param '/testIp'代表vue-cil中config,index.js中配置的代理
  */
 // get请求
 function getListAPI(url,params){
     return http.get(`${url}`,params)
 }
 // post请求
 function postFormAPI(url,params){
     return http.post(`${url}`,params)
 }
 ​
 // put 请求
 function putSomeAPI(url,params){
     return http.put(`${url}`,params)
 }
 // delete 请求
 function deleteListAPI(url,params){
     return http.delete(`${url}`,params)
 }
 // 导出整个api模块
 export default {
     getListAPI,
     postFormAPI,
     putSomeAPI,
     deleteListAPI
 }

首先是导入上一层封装的请求信息。

 import http from '../utils/http'

然后对应不同请求写不同方法。

以get为例:

 // get请求
 function getListAPI(url,params){
     return http.get(`${url}`,params)
 }

携带参数urlparams,然后调用第二次封装的方法。

话说这儿我是借鉴了许多网上的封装形式总结的,但是这一次我感觉必要性不大,但是应该是有意义的,我也不明白,有大佬看到还麻烦点醒一番。

最后单个暴露每个请求模块就可以。

 // 导出整个api模块
 export default {
     getListAPI,
     postFormAPI,
     putSomeAPI,
     deleteListAPI
 }

请求示范

这样调用起来也是挺方便的。

你只需要给出请求的后缀,比如你后端请求路径是/user,那就直接:

 api.postFormAPI("/user, {
     //携带参数
    topicUid: this.topic.topicUid,
 }).then(
     //.....
 )