工作中接手他人的项目,看到一些 axios 封装很是复杂,难用,现在来总结一下 axios 封装 xhr 的问题。
在 vue 项目中使用,希望达到下面的效果:
-
引用方便,在组件中,通过 this.$http[method]
使用;
-
兼容 REST 风格封装,使用 JSON 进行交互,提供常用的四种方法;
-
不同请求方法,参数格式一致,this.$http.get(url,params)
、this.$http.post(url,params)
;
-
可进行二次确认,this.$http.delete(url,params,{content:'删除后不可恢复!',type:'danger'})
;
-
统一处理错误,同时提供抛出错误的方法,比如 this.$http.getWithError(url)
,使用时可在函数里捕获错误;
-
错误时,控制台有特别的日志输出,要是公司有条件,可提交到服务器,方便排查问题;
-
重复请求,可取消;
-
用户切换路径,取消请求;
-
文件下载;
额外的功能
有 mock 服务的,可指定是否走 mock 服务,似乎很多公司都没有这个。
能想到的就是以上了,根据上面的需求,封装 GET 和 POST
封装实现#
基本封装#
关于请求头的指定
axios 根据参数格式,自动采设置content-type
:传递对象,设置为 json 提交,传递字符串时,资源设置为 application/x-www-form-urlencoded;charset=UTF-8
。
希望只传递对象,且不想 axios 自动设置,就手动设置 content-type 为 json
关于拦截器
可在请求发送之前和返回之后,在拦截器里做一些你想要的操作,比如转化格式,添加自定义的请求头,处理错误,都是在拦截器里操作。这正是我们使用它的理由。
拦截器接收两个函数,第一个 onResolved
,第二个为onRejected
,他们的作用和 promise.then 的参数一致。
希望在请求发送之前执行某些操作,自请求拦截器里操作。
希望在响应返回 JS 代码之前,执行操作在响应拦截器里操作。
axios 不知道返回我们希望的数据,需要在响应拦截器里处理一下:
错误处理,很关键
在响应拦截器里统一处理常见的错误,比如404
、403
、断网等。
争论
只有 status 为 200 --- 299 才是成功吗?
REST 风格主张在接口设计中充分利用 http 语义,使用 http 状态码来表示接口状态,我也喜欢这种方式。有几个好处:
- 通用:http 是通用协议,没有额外的沟通成本;
- 自带文档:充分利用 http 的语义写 REST 风格的 API,可不写文档。理由:它们已被很多人了解。
- 好理解:由于上面的原因,接口好理解。
- 方便调试:基于 1、2 的原因,联调很方便。
- 对新人友好:新成员加入团队,没有规范文档,没有项目说明,基于前面三点,能让新人快速投入。
需要额外定义错误状态吗?
遵循 REST 风格的 API,额外指定错误状态码,违背了充分利用 http 语义的原则,增加了沟通成本,是有害的。
不额外指定错误状态码,如何指定错误信息?
利用 http 状态码和返回消息表示错误,可减少联调难度
,减少沟通。
有些公司路径错误、参数格式不对,状态码都是 200,而且返回的错误信息不具体,随着接口参数字段的增加,调试难度成倍增加,文档还写得垃圾。最后进度延期,就让前端背锅。
http 状态码本身对 http 请求结果进行了分类,而且浏览器会显示错误的请求,外加自定义的信息,可大大降低调试成本。
比如参数不符合预期,就设置状态码为400
,在消息里面给出正确的参数提示和用户用户的信息:
额外指定错误状态码,是非常不好的实践。
给出具体的错误信息会带来安全风险?
有人说,给出具体的接口错误信息,有安全风险。这属于后台代码问题,而不是接口设计问题。
好的接口设计应该是易用的、能提高团队协作效率的。
如何在组件种中更方便的使用#
在 main.js 挂载到 vue 的原型上。
如何优雅得封装 POST#
优雅
是关键词。有些团队使用 GET 或者 POST 来执行一些危险操作,比如删除,再执行前,往往需要用户二次确认,通常的做法:每个需要确认的接口,都写一遍二次确认的代码
,这是很不优雅的写法,会导致代码难以理解和维护。
优雅的写法:在接口中传递参数,指定是否需要二次确认。
以 Element UI 的确认框为例子:
用户点击取消时,执行 catch
,执行 reject ,会报错错误,但是这不是错误,所以不做处理。
这些调用:
如何处理错误#
当接口出错时,往往需要提示用户,通常的做法是在每个接口调用的 catch 函数里写,比如上面的 this.$message(error.msg)
。
可以统一处理吗?
可以,在响应拦截器中。
message 是错误提示框。
logInfo(response) 是错误输出,方便调试
,输出如下:
还有哪些可能的需求?
- 提交错误日志到服务器,方便监控状态和复现问题;
- 针对不同的错误,进行不同的处理。
401 时,跳转到注册页面,用户注册后跳转回来;
403 时,提示用户用登录,登录后再跳转回来;
断网时,显示断网组件等。
需要再 rejected 错误吗?
如果已经给可能出现的错误,统一处理了,可以不再 reject ,而是resolve()
缺点:
- 接口不遵循 RESTful 理念,会增加额外的处理成本。
比如 404 也返回 200,在消息体里使用 code:404 表示没有资源,对 axios 而言,其实是成功的,还需要在响应成功拦截器里编写处理函数。
- 使用者会困惑
都 resolve 了,还报错?还没数据?
优点:不用额外处理错误了,在 then 里拿数据即可。
可见,还是 reject 好。
其实还可再提供一个 resolve 的接口,外部这样调用。
this.$http.getNoError().then()
如何取消请求#
axios 可取消请求,我使用new CancelToken
来取消。
取消的原理#
调用 CancelToken
,得到一个取消配置,配置有 promise 实例。CancelToken 参数是一个函数,axios 调用该函数时,又传递一个函数,就是取消函数 c, c 又调用取消配置的 promise.resolve,在请求适配器内,检测到 promise 变化,在 then 中执行 xhr 的 reject 方法。
可这样调用:
比如:
CancelToken 有一个 promise 属性,可以调用这样调用:
关键源码:
组件的取消请求,需要用户手动点击按钮,要是能自动判断请求是否重复,取消重复请求才是期望的。
如何取消重复请求#
既然取消的标准还是保存在请求配置里,就可以在拦截器里判断是否重复,然后取消重复。
三种取消重复请求方案:
- 只允许最新的请求发送,老的请求都取消
实现关键:在请求拦截器里记录请求时,先判断是否已经存在,存在取消,再添加,保证记录都不同。
优点:多次请求,保证了最新的请求发出,用户拿到的一定是最新的数据,类似防抖。
缺点:
①. 由于只有一个请求发出,可能失败,同时,网络慢的话,用户会等待更久。
②. 同时可能后台已经处理请求到一半了,然后请求被取消了,马上又进来一个请求,可能会造成效率不会太高。
只允许最新的请求发出
- 只允许最老的请求发送,新的请求都取消
实现关键:在请求拦截器里记录请求和取消请求,在响应拦截器重置记录,让用户可以再次发出。
优点:多次请求,保证了最老的请求发出,服务器压力小。
缺点:用户拿到的可能不是最新的数据。
发出 14 个,后来的都取消了,第一个成功返回。
- 允许多个请求发送,有一个请求成功,取消其他还在处理的请求
实现关键:在请求拦截器记录每个请求,在响应成功拦截器中取消其他请求。
优点:能保证请求至少返回一个,尤其是当单个失败的可能性比较大,允许发出多个,那么用户频繁操作,成功的概率就更大。
缺点:
①. 用户拿到的数据不能保证是最新的。
②. 能否成功取消,由网络速度和这这部分代码执行速度决定,网络比代码快,比如网络 10 毫秒内返回,可能无法请求,原因是第一个网络成功了,第二个请求还没发出,即网络返回了,判断重复的函数还没执行。
重复的请求都发出
有一个请求成功后,其他正在进行的请求取消
取消重复请求的具体实现#
当 url、method、参数一样,就是相同请求。
第三种方案成功拿到数据的可能性大,它对用户更加友好,就讲它如何实现。
在请求拦截器里记录请求:
在响应拦截器的成功方法中取消请求:
高亮部分为关键代码。
如何判断重复请求
对于 post、put 请求, data 的格式在请求时被 axios 根据content-type
修改了,因为在请求拦截器和响应拦截器里都要获取请求标识, 都调用了 generateRequestKey
, 需要把 data 处理成相同的格式。
所以有:
typeof data === 'object' ? JSON.stringify(data) : data
优雅地封装 axios 的关键:
-
提供快捷方法;
-
统一处理错误;
-
能二次确认;
-
能取消重复请求;
-
合理使用拦截器。
实现了关键功能,其他功能可根据需要添加。