如何取消掉上一次的http请求

记录开发测试过程中遇到的关于接口过慢导致的bug

背景


项目测试过程中发现了一个不太常见的bug, 在测试项目的搜索功能的时候发现搜索出来的结果与输入的关键字不匹配的问题。


问题重现

为了让问题变得更加直观,我将后端搜索接口模拟某一请求响应非常缓慢。

可以直观的看到,当我输入v的时候, v 的搜索请求已经发出。紧接着,我改变了输入为a,这时 a 的搜索请求也发出并且立即返回了结果。我们的页面同时也正确渲染了,但过了一会v的请求得到相应,拿到了数据渲染到页面上,就造成了搜索与结果不匹配的现象。需要注意的是,这个问题并不是增加防抖所能解决的。

解决办法

既然问题的根本在于无效的请求数据,那么最好的办法就是当我们发出新的请求时,将上一个还没有响应的请求cancel 掉,保证我们每次获取到的数据都没有过期,问题便迎刃而解。
我们查看一下我们搜索的effect文件的代码。

typescript 复制代码
searchPosts$ = createEffect(() =>
    this.actions$.pipe(
        ofType(PostsActions.searchPosts),
        debounceTime(500),
        mergeMap((action) =>
            this.dataService.search(action.keyword).pipe(
                map(posts => PostsActions.loadPostsSuccess({ posts }))
            )
        )
    )
);

可以看到代码中我们已经添加了防抖,为我们的搜索功能做了一定的优化。怎样改动才能新的搜索发出后取消掉上一次的没有响应的请求呢 ?
其实很简单, 我们只需要将 mergerMap 改为 switchMap

typescript 复制代码
searchPosts$ = createEffect(() =>
    this.actions$.pipe(
        ofType(PostsActions.searchPosts),
        debounceTime(500),
        switchMap((action) =>
            this.dataService.search(action.keyword).pipe(
                map(posts => PostsActions.loadPostsSuccess({ posts }))
            )
        )
    )
);

为什么改为switchMap 就可以fix 这个bug呢,让我们一起来看一看什么是switchMap.
rxjs文档这样描述

switchMap:

Projects each source value to an Observable which is merged in the output Observable, emitting values only from the most recently projected Observable.

可以概括为切换到最新的内部 Observable,取消订阅之前的 Observable。
感兴趣的可以查看switchMap
这是修改后的效果:

可以看到我们已经成功取消掉了“过期”的请求。我们通过rxjs提供给我们的强大功能解决掉了这个问题, 可如果我们项目没有使用rxjs,我们该如何解决呢 ?

取消请求发送的实现

typescript 复制代码
lastController: AbortController | null = null;
handleInput() {
  if (this.lastController) {
    this.lastController.abort();
  }

  const controller = new AbortController();
  this.lastController = controller;

  // 发起新请求
  fetch(`http://localhost:8080/api/posts/search?keyword=${this.keyword}`, { signal: controller.signal })
    .then(response => response.json())
    .then(data => {
      this.posts = data;
    })
    .catch(error => {
      if (error.name === 'AbortError') {
        // 请求被取消,忽略
        return;
      }
      console.error(error);
    });
}

我们借助了AbortController API,通过 controller.signal 与 fetch 请求关联,调用 abort() 会触发请求中止。被取消的请求会抛出 AbortError,又做了相应的错误处理。实现了与switchMap类似的功能。