如何去除RXJS中多余的请求?

How to remove the redundant requests in RXJS?

我正在使用 Rxjs 和 Angular 框架开发一个前端项目,我想从 api "api/data_processor_classlib.php...." 中获取 json 数据。 HTML 管道 this.webProtectionHTML$ 订阅了三个部分。我不知道为什么管道 this.webProtectionHTML$ 发出了 3 次请求。是否有任何可能的解决方案只发送一个请求并更新 HTML 中的所有数据?谢谢

HTML代码:

    <div class="tr">
      <div class="align-left">Phishing & Other Frauds</div>
      <div class="align-right">{{ (webProtectionHTML$|async)?.phishing}}</div>
    </div>
    <div class="tr">
      <div class="align-left">Spam URLs</div>
      <div class="align-right">{{ (webProtectionHTML$|async)?.spamURLs}}</div>
    </div>
    <div class="tr">
      <div class="align-left">Malware Sites</div>
      <div class="align-right">{{ (webProtectionHTML$|async)?.malware}}</div>
    </div>

组件:

this.webProtectionHTML$ = this.dayService$
      .pipe(
        mergeMap((days: DaysPeriod // params added to request url) => this.httpClient.get(`api/data_processor_classlib.php....`//request url, { responseType: 'text' })),
        map((html: string) => {
          //get html code and find data return as json data
          let result = this.getWebProtectionData(html)
          return result
        }))

网络日志:

它被调用了三次,因为每个 async 管道都会触发一个请求。相反,在这些情况下,您可以订阅组件并使用成员变量。然后您可以取消订阅 ngOnDestroy 挂钩中的订阅以避免内存泄漏。尝试以下

控制器

private dayServiceSubscription: Subscription;
public webProtectionHTML: any;

ngOnInit() {
  this.dayServiceSubscription = this.dayService$
    .pipe(
      mergeMap((days: DaysPeriod) => this.httpClient.get(`api/data_processor_classlib.php....`//request url, { responseType: 'text' })),
      map((html: string) => this.getWebProtectionData(html)))
    .subscribe(response => this.webProtectionHTML = response);
}

ngOnDestroy() {
  if (this.dayServiceSubscription) {
    this.dayServiceSubscription.unsubscribe();
  }
}  

模板

<div class="tr">
  <div class="align-left">Phishing & Other Frauds</div>
  <div class="align-right">{{ webProtectionHTML?.phishing}}</div>
</div>
<div class="tr">
  <div class="align-left">Spam URLs</div>
  <div class="align-right">{{ webProtectionHTML?.spamURLs}}</div>
</div>
<div class="tr">
  <div class="align-left">Malware Sites</div>
  <div class="align-right">{{ webProtectionHTML?.malware}}</div>
</div>

因为你用(webProtectionHTML$|async)调用了3次,每次调用显示的都是不同成员的值。如果你希望它只被调用一次,调用 constructorngOnInit 中的 this.webProtectionHTML$ 并将返回值分配给一个局部变量,你可以使用它的值来绑定它。

对于API,您可以return作为承诺使用toPromise方法。它将 convert 订阅承诺。所以即使你使用 async 3 times。它将解决承诺 once.

this.webProtectionHTML$ = this.dayService$
      .pipe(
        mergeMap((days: DaysPeriod // params added to request url) => this.httpClient.get(`api/data_processor_classlib.php....`//request url, { responseType: 'text' })),
        map((html: string) => {
    //get html code and find data return as json data
    let result = this.getWebProtectionData(html)
          return result
        })).toPromise()

之前的一些回答正确地指出了 async 管道的每次使用都会导致 http 请求 - 但没有解释原因。事实上,这不是异步管道本身的问题。这是因为您的 http 请求 observable 是 "cold"(如此处所述:https://www.learnrxjs.io/learn-rxjs/concepts/rxjs-primer#what-is-an-observable)。

"Cold" observable 意味着只有当某些消费者订阅它时,它才会开始做事并发出值。此外,默认情况下,每个新订阅都会启动新的执行——即使对同一个可观察对象的多个订阅是并行创建的。这正是您在代码中观察到的:每个异步管道分别订阅可观察对象。

有几种方法可以解决这个问题。

  1. 确保您只有一个订阅。 @Michael D 的回答利用了这种方法。这是解决问题的一种很常见的方法。一个潜在的缺点是,当组件被销毁时,以这种方式手动创建的订阅不会自动取消订阅。在您的示例中,这可能没什么大不了的(如果 dayService$ 仅发出一个值)。但是,如果组件在 http 请求完成之前被销毁,则在不编写一些额外代码(涉及实现 ngOnDestroy 生命周期方法)的情况下,不会取消此 http 请求。另一个缺点 - 如果您的组件使用 OnPush,您将需要手动调用 changeDetector.markForCheck()

  2. 使这个可观察"hot"。 "Hot" 表示异步操作已经启动,所有订阅者都将收到该操作的结果——无论有多少订阅者。正如@xdeepakv 所建议的那样,使用 .toPromise() 可以做到这一点。请注意,promises 根本不可取消 - 因此您将无法取消此类请求。另一个缺点 - 它仅在您的可观察对象发出单个值然后完成(例如单个 http 请求)时才有效。

  3. 您可以使用 shareReplay({refCount: true}) 运算符来制作可观察的多播 - 这允许多个订阅者共享相同的结果。在这种情况下,您不需要更改模板(可以有多个异步管道)并受益于在异步管道中实现的自动取消订阅/http 请求取消。

this.webProtectionHTML$ = dayService$.pipe(
  mergeMap(...),
  map(...),
  shareReplay({refCount: true}) // <- making it a multicast
)

它发出了 3 个请求,因为代码在模板中为 this.webProtectionHTML$ 指定了 3 个异步管道。

您有两个解决方案:

1.在模板中使用单个异步管道

使用 ngIf 并在条件

中使用别名 as 语句设置异步管道
<div *ngIf="webProtectionHTML$ | async as webProtection"> // define async here
    <div class="tr">
      <div class="align-left">Phishing & Other Frauds</div>
      <div class="align-right">{{ webProtection.phishing}}</div>
    </div>
    <div class="tr">
      <div class="align-left">Spam URLs</div>
      <div class="align-right">{{ webProtection.spamURLs}}</div>
    </div>
    <div class="tr">
      <div class="align-left">Malware Sites</div>
      <div class="align-right">{{ webProtection.malware}}</div>
    </div>
</div>

2。使用 shareReplay() rxjs 运算符 运营商用于向所有订阅者重播以前的数据。 shareReplay(1) 表示重放最后一条数据。

this.webProtectionHTML$ = this.dayService$
      .pipe(
        mergeMap((days: DaysPeriod // params added to request url) => this.httpClient.get(`api/data_processor_classlib.php....`//request url, { responseType: 'text' })),
        map((html: string) => {
          //get html code and find data return as json data
          let result = this.getWebProtectionData(html)
          return result
        })),
        shareReplay(1)

两者都有效,但如果我可以为这种情况选择,我喜欢第一个解决方案。

希望对您有所帮助