问题描述:切换路由,组件销毁,请求还在继续,回调执行时有关于DOM的操作失败。
Senry上有一些报错,一直没有找到原因,同一种报错频繁出现,不同种报错,出错位置也相似,这些问题出现的时机大多是切换页面的时候触发的,经过推断之后,将网速调到最慢“Low-end mobile”,未等请求结果返回,立刻切换页面,bug就复现了,所以证明这种推断是没问题的,那么解决方案也就有了:
第一种解决方案:
切换路由的时候,取消上一个页面的所有请求或者部分请求。
第二种解决方案:
在回调里面判断页面是否销毁,然后再去做DOM相关的操作
axios
axios是一个基于promise的HTTP库,可以用在浏览器中和nodejs中
Features
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
上述第六条,正是我们需要使用的功能,官网上提供的示例是这样的:
1 | var CancelToken = axios.CancelToken; |
Note : 可以使用同一个 cancel token 取消多个请求
我们的需求是在组件卸载的时候取消请求,如果同一个页面请求数量比较多,页面比较多,针对每一个页面去添加这样的功能不太现实,所以可以在切换路由的时候触发这一事件,在公共的请求函数里做处理:(以下为react版本,vue同理)
前提是所有的页面组件都使用了同一父组件(BasicLayout),这样只在父组件里面监听路由变化即可,将取消请求的cancel token存储在redux中:
1 | /* ----------state------------- */ |
1 | componentWillReceiveProps(nextProps) { |
上面已经执行了取消请求,但是此时请求还未发送,需要设置取消请求的标志,在公共的请求函数里做处理
1 | export function useInterceptors(netApi) { |
上述逻辑已经完成需求了,即切换路由的时候,取消上一个页面的所有请求,但是考虑一种场景,如果不切换路由,用户频繁进行查询操作,为了减轻服务器压力,我们是不是可以在一段时间间隔内,以最后一次发送的请求为准呢,取消上一次发送的同一请求:
1 | const pending = []; // 声明一个数组用于存储每个ajax请求的取消函数和ajax标识 |
写到这里,可以考虑将两者结合起来使用,即切换路由,取消上一个页面的所有请求,不切换路由时,对于同一页面的某一时间间隔内频繁发送同一个请求,也进行取消操作,那么怎么知道什么时候用哪种cancel呢?
我是这样想的,在请求和响应中都做处理:
- 首先是每次响应结束都将cancelToken置为空
- 请求前,取一遍redux中的cancelToken,如果不为空,说明是切换路由之后的第一次请求,此时,请求携带此次cancelToken;如果为空,说明此时的请求不是切换路由之后的请求,视为正常请求处理,cancelToken存储在pending中,在下一次请求发送时携带
1 | export function useInterceptors(netApi) { |
取消请求的原理
1 | // 核心代码 |
1 | function CancelToken(executor) { |
XMLHttpRequest中取消请求使用的是abort()方法,用户可以主动取消请求,但是要注意,取消请求,并没有真正意义上的取消,即没有中断tcp连接,因为已经发送给服务器端的连接并不会撤销,服务器端还是会继续处理,只是关闭了这个http请求,客户端(浏览器)对响应就不受理了,所以呈现给我们的是请求被终中止了。此外,取消请求不区别对待请求方式,即无论是get还是post方式的请求都能够取消掉。
tips: 在我们发送一个请求时,首先是建立tcp连接,然后才是浏览器发送HTTP请求,服务器接受请求、处理请求、返回响应后,如果请求头的connection不是keep-alive的,就断开连接。
1 | var xhr = new XMLHttpRequest(), |
监听页面销毁
当切换路由时,上一个组件的生命周期就会进入componentWillUnmount阶段,但是因为在这个阶段state会随着页面卸载而被销毁,所以,我们不能把监听的值放在state中,如果是常量呢?显然也是不可以的,因为页面销毁,常量还会继续存在,可以将这个变量值挂载到this上,这是有效的:
1 | constructor() { |
有了上面的思路,我们就可以对每个回调就行处理了,在每个请求的回调中,如果有涉及到对dom的操作,那么判断一下,页面是否已经卸载,如果未卸载,再进行操作。
但是具体选择哪种方式,还是根据具体业务来定,存在有些请求,涉及到的业务复杂,响应时间长,用户在结果返回之前就切换了页面,如果响应的结果是非常必要的,要给出提示的,那我们这样做了就不符合需求了,虽然请求也发给了服务器端,服务器端会做处理,但是响应结果我们收不到,这种情况下考虑的就多了一些,可以继续修改一下请求的方法,比如对特定的请求,在页面里面就设定了request的cancelToken为null,在请求发送之前优先判定request的cancelToken参数,然后再判断redux里面存储的token。