如何在Angular 4+中取消/退订所有挂起的HTTP请求

73

如何在Angular 4+中取消/中止所有待处理的HTTP请求。

有一个"unsubscribe"方法可以取消HTTP请求,但如何一次性取消所有待处理的请求。

尤其是在路由更改时。

我做过一件事

ngOnDestroy() {
  this.subscription.unsubscribe();
}

但如何在全球范围内实现这一目标?

有什么想法吗?


拦截器没有帮助@SibiRaj。 - Rahul Singh
不行,我试过了,但似乎没有什么帮助。我能设置超时时间,但无法将其与路由一起使用。你有任何想法吗? - Sibiraj
请查看此帖子:https://dev59.com/ZFoT5IYBdhLWcg3w8S-2#41177163 - David
您是指整个应用程序中所有未完成的请求,还是某些服务中的请求? - Aluan Haddad
是的,@AluanHaddad。在整个应用程序中。 - Sibiraj
显示剩余4条评论
11个回答

98

使用RxJS中的takeUntil()运算符全局取消订阅:

- RxJS 6+(使用pipe语法)

import { takeUntil } from 'rxjs/operators';

export class YourComponent {
   protected ngUnsubscribe: Subject<void> = new Subject<void>();

   [...]

   public httpGet(): void {
      this.http.get()
          .pipe( takeUntil(this.ngUnsubscribe) )
          .subscribe( (data) => { ... });
   }

   public ngOnDestroy(): void {
       // This aborts all HTTP requests.
       this.ngUnsubscribe.next();
       // This completes the subject properlly.
       this.ngUnsubscribe.complete();
   }
}

- RxJS < 6

import 'rxjs/add/operator/takeUntil'

export class YourComponent {
   protected ngUnsubscribe: Subject<void> = new Subject<void>();

   [...]

   public httpGet(): void {
      this.http.get()
         .takeUntil(this.ngUnsubscribe)
         .subscribe( (data) => { ... })
   }

   public ngOnDestroy(): void {
       this.ngUnsubscribe.next();
       this.ngUnsubscribe.complete();
   }
}

在你想要完成一堆流时,可以使用next()在取消订阅的Subject上发出事件。此外,在组件销毁时取消订阅处于活动状态的Observables以避免内存泄漏也是一个好习惯。

值得阅读的文章:


请查看seangwright在该主题上的非常完整的答案;) https://dev59.com/ZFoT5IYBdhLWcg3w8S-2#41177163希望这有所帮助! - Alexis Facques
不要忘记从RxJs导入takeUntil,import 'rxjs/add/operator/takeUntil'; - Rishi0405

74

你可以创建一个拦截器来对每个请求应用takeUntil操作符。然后在路由改变时,您将发出事件来取消所有待处理的请求。

@Injectable()
export class HttpCancelInterceptor implements HttpInterceptor {
  constructor(private httpCancelService: HttpCancelService) { }

  intercept<T>(req: HttpRequest<T>, next: HttpHandler): Observable<HttpEvent<T>> {
    return next.handle(req).pipe(takeUntil(this.httpCancelService.onCancelPendingRequests()))
  }
}

辅助服务。

@Injectable()
export class HttpCancelService {
  private cancelPendingRequests$ = new Subject<void>()

  constructor() { }

  /** Cancels all pending Http requests. */
  public cancelPendingRequests() {
    this.cancelPendingRequests$.next()
  }

  public onCancelPendingRequests() {
    return this.cancelPendingRequests$.asObservable()
  }

}

在您的应用程序中的某个地方(例如在appComponent的onInit中),挂钩路由更改。

this.router.events.subscribe(event => {
  if (event instanceof ActivationEnd) {
    this.httpCancelService.cancelPendingRequests()
  }
})

最后但同样重要的是,将拦截器注册到你的app.module.ts中:

  import { HttpCancelInterceptor } from 'path/to/http-cancel.interceptor';
  import { HTTP_INTERCEPTORS } from '@angular/common/http';

  @NgModule({
    [...]
    providers: [
      {
        multi: true,
        provide: HTTP_INTERCEPTORS,
        useClass: HttpCancelInterceptor
      }
    ],
    [...]
  })
  export class AppModule { }

8
我认为这是最好的方式。 - walidtlili
2
这看起来很不错,但它忘记了完成主语以关闭它。 - Logus Graphics
1
@hbthanki 是的,你需要手动取消其他请求。由于取消所有订阅可能很烦人,我通常会有一个实现了onDestroy方法的类,我的组件都继承这个类(我称之为Destroyable)。它有一个公共Subject,在销毁时发出和完成。然后我的组件在每个observable上都有一个takeUntil(this.destroyed$)。因此,当你销毁组件时,这种方法会取消所有未完成的observables。 - Bladito
2
@Logus 我没有故意关闭它,因为服务在整个应用程序的生命周期内都存在,并且关闭流也不会释放任何资源。如果主题完成,则需要一遍又一遍地创建新主题。那么什么时候创建?谁有这个责任?这只会使代码更加复杂,我担心它不会增加任何价值。如果我错了,请随时纠正我。 - Bladito
1
谢谢你的有用回答。我只想补充一下 'next.handle(req).takeUntil()' 不起作用(在我的 Angular 7 和 RxJS 版本 6.3.3 中没有起作用)。我使用了 'next.handle(req).pipe(takeUntil())'。 - Adnan Sheikh
显示剩余8条评论

13
如果您不想手动取消所有订阅,那么您可以这样做:
export function AutoUnsubscribe(constructor) {

  const original = constructor.prototype.ngOnDestroy;

  constructor.prototype.ngOnDestroy = function() {
    for (const prop in this) {
      if (prop) {
        const property = this[prop];
        if (property && (typeof property.unsubscribe === 'function')) {
          property.unsubscribe();
        }
      }
    }

    if (original && typeof original === 'function') {
      original.apply(this, arguments)
    };
  };

}

那么你可以在你的组件中将它用作装饰器

@AutoUnsubscribe
export class YourComponent  {
}

但是您仍需要将订阅存储为组件属性。当您导航离开组件时,AutoUnsubscribe函数将发生。


1
我喜欢这个想法。不知道能否建议您使其更加强大,以处理具有订阅数组的组件(并非罕见情况)?例如:(Array.isArray(property) ? property : [property]).filter(property => isFunction(property.unsubscribe)).forEach(property => property.unsubscribe())); - Aluan Haddad
这是一个好主意,但需要进行优化,因为如果您有大量数据的数组,则过滤器将搜索每个元素,并且可能会有点慢。也许我们只需检查数组的第一个元素,如果它是订阅元素,则可以假定整个数组都是订阅元素。 - Anton Lee
你可以这样做,但我怀疑它会对性能产生显著影响。 - Aluan Haddad

7
我对所需的功能并不确定,但您可以通过包装框架的http服务并委托给它,在任何时候和任何地方取消所有未完成的请求来实现此功能。
然而,当我们开始实现这个服务时,一个问题很快就变得明显了。一方面,我们希望避免更改现有的代码,包括第三方代码,其利用了股票Angular http客户端。另一方面,我们希望避免实现继承。
为了兼顾两者,我们可以用我们的包装实现Angular Http服务。现有的代码将继续工作而无需更改(前提是该代码不做任何愚蠢的事情,比如使用“http instanceof Http”)。
import {Http, Request, RequestOptions, RequestOptionsArgs, Response} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import {Subscription} from 'rxjs/Subscription';



export default interface CancellationAwareHttpClient extends Http { }

export default class CancellationAwareHttpClient {
  constructor(private wrapped: Http) {
    const delegatedMethods: Array<keyof Http> = [
      'get', 'post', 'put', 'delete',
      'patch', 'head', 'options'
    ];
    for (const key of delegatedMethods) {
      this[key] = wrapped[key].bind(wrapped);
    }
  }

  cancelOutstandingRequests() {
    this.subscriptions.forEach(subscription => {
      subscription.unsubscribe();
    });
    this.subscriptions = [];
  }

  request(url: string | Request, options?: RequestOptionsArgs) {
    const subscription = this.wrapped.request(url, options);
    this.subscriptions.push(subscription);
    return subscription;
  }

  subscriptions: Subscription[] = [];
}

请注意,CancellationAwareHttpClientinterfaceclass 声明已经合并了。这样,我们的类通过 interface 声明中的 extends 子句来实现 Http
现在我们将提供我们的服务。
import {NgModule} from '@angular/core';
import {ConnectionBackend, RequestOptions} from '@angular/http';

import CancellationAwareHttpClient from 'app/services/cancellation-aware-http-client';

let cancellationAwareClient: CancellationAwareHttpClient;

const httpProvider = {
  provide: Http,
  deps: [ConnectionBackend, RequestOptions],
  useFactory: function (backend: ConnectionBackend, defaultOptions: RequestOptions) {
    if (!cancellationAwareClient) {
      const wrapped = new Http(backend, defaultOptions);
      cancellationAwareClient = new CancellationAwareHttpClient(wrappedHttp);
    }
    return cancellationAwareClient;
  }
};

@NgModule({
  providers: [
    // provide our service as `Http`, replacing the stock provider
    httpProvider,
    // provide the same instance of our service as `CancellationAwareHttpClient`
    // for those wanting access to `cancelOutstandingRequests`
    {...httpProvider, provide: CancellationAwareHttpClient}
  ]
}) export class SomeModule {}

请注意我们如何覆盖现有的框架提供的服务。我们使用一个工厂来创建我们的实例,并且不会为DI添加任何装饰器到包装器本身,以避免注入器中的循环。

我想要实现这个功能,因为即使我在页面之间导航时,仍有未完成的HTTP请求没有被取消。我必须在每个页面上使用destroy订阅。我想为什么不全局处理呢?也就是说,在路由更改期间取消所有未完成的HTTP请求。 - Sibiraj
如果我想要实现的内容有误,请纠正我 :) - Sibiraj
@SibiRaj 我认为这没问题 :) 只要在引入全局状态时小心。这种方法的好处是,您可以在不更改使用 Http 的任何服务或组件的情况下进行实验。 - Aluan Haddad
嗨@AluanHaddad,你从哪里得到了wrappedHttp?你的评论没有澄清这一点。你能和我们分享更多细节吗?谢谢。 - Dom
@Sibiraj 我直接进行了实例化。虽然示例代码中提供了,但为了在 Angular 6/7 中运作正常,可能需要进行一些微小的调整。 - Aluan Haddad

4

ngOnDestroy 回调通常用于在实例销毁时需要进行任何自定义清理的情况。

您想在哪里取消您的请求?

也许,如果您希望在浏览器关闭时取消您的请求,这里有一个创意点子


我想在路由更改时取消请求。 - Sibiraj
你应该在可能是路由出口的组件上调用ngOnDestroy()。 - Vala Khosravi
那我需要对所有组件都进行单独处理吗? - Sibiraj
2
只有组件可以放置在路由出口上。因为当路由出口发生变化时,“ngOnDestroy()”会被调用。 - Vala Khosravi

2
尝试这个:

试一试:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Rx';

export class Component implements OnInit, OnDestroy {
    private subscription: Subscription;
    ngOnInit() {
        this.subscription = this.route.params.subscribe();
    }
    ngOnDestroy() {
        this.subscription.unsubscribe();
    }
}

2
不,这不会取消待处理的请求。 - user663031
是的,我想一次性取消所有操作,例如在路由更改时。 - Sibiraj

1

在 @Bladito 的回答上补充一点,他的回答已经非常完美。

实际上,HttpCancelService 堆栈是完美的,但问题在于它的调用位置。如果您有子路由,在导航结束时调用此函数可能会导致问题。

因此,我创建了一个抽象容器组件,在销毁时调用 HttpCancelService。这样我就可以更细致地管理何时切断任何 Http 取消请求。

import { Component, OnDestroy, OnInit } from '@angular/core';
import { HttpCancelService } from '../../services/http-cancel-service.service';

@Component({
  selector: 'some-abstract-container',
  template: `
    ABSTRACT COMPONENT
  `,
  styleUrls: ['./abstract-container.component.scss']
})
export class AbstractContainerComponent implements OnInit, OnDestroy {
  constructor(protected readonly httpCancelService: HttpCancelService) {}

  ngOnInit() {}

  ngOnDestroy(): void {
    this.httpCancelService.cancelPendingRequests();
  }
}


这是一个扩展抽象组件的具体组件:

import { Component, OnInit } from '@angular/core';
import { AbstractContainerComponent } from '../../../shared/components/abstract-container/abstract-container.component';
import { HttpCancelService } from '../../../shared/services/http-cancel-service.service';

@Component({
  selector: 'some-concrete-container',
  templateUrl: '.some-concrete-container.component.html',
  styleUrls: ['./some-concrete-container.component.scss']
})
export class SomeConcreteContainerComponent extends AbstractContainerComponent implements OnInit {
  constructor(protected readonly httpCancelService: HttpCancelService) {
    super(httpCancelService);
  }

  ngOnInit() {}
}


1
我认为在路由更改层面上取消请求并不是一个好主意,因为这样会失去细粒度。
例如,也许你想在一个组件上取消请求,而在另一个组件上不取消请求,因为它不会被销毁。最重要的是,那么如何处理后台请求呢?这将非常棘手,因为难以调试为什么一些请求已经被随机取消了。
但是,通常情况下,无论路由是否更改,都应该取消 get 请求,因为其所在的组件即将被销毁。

在销毁时取消订阅可观察对象

如果您想让生活变得更轻松,则可以使用until-destroy。它将在组件即将销毁(ngOnDestroy)时自动取消订阅所有可观察对象。 它足够细粒度且更通用(不仅限于 Http 请求,而是所有的 observables 都将被取消订阅)。

import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
     
@UntilDestroy()
@Component({})
export class InboxComponent {
  ngOnInit() {
    interval(1000)
      .pipe(untilDestroyed(this))
      .subscribe();
  }
}

1
    //This is the example of cancelling the get request once you leave the TestComponent.

    import { Component, OnInit} from '@angular/core';

    @Component({
      selector: 'app-test',
      templateUrl: './test.component.html'
    })
    export class TestComponent implements OnInit {

      request: any;
someList: any;

      constructor( private _someService: SomeService) {

      }

    ngOnInit() {
        this.getList();
      }

      ngOnDestroy(){
        this.request.unsubscribe(); // To cancel the get request.
      }

      getList() {
        this.request= this._someService.getAll()
          .subscribe((response: any) => {
            this.someList= response;
          }, (error) => {
            console.log("Error fetching List", error);
          })
      }

    }

0
你可以创建一个自定义的 Http 服务(使用 HttpClient),它维护了一个挂起请求列表。每当你发出一个 http 请求时,使用这个自定义服务而不是 Http/HttpClient,现在将订阅推送到列表中,在返回响应时弹出该订阅。使用这种方法,你将拥有所有未完成的订阅列表。
现在在同一个自定义服务中,在构造函数中注入路由器并订阅它以获取路由更改事件。现在每当这个可观察对象发出信号时,你需要做的就是取消订阅列表中的所有订阅并从列表中弹出所有元素。
如果你需要代码片段,请在评论中提及。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接