如何去除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次,每次调用显示的都是不同成员的值。如果你希望它只被调用一次,调用 constructor
或 ngOnInit
中的 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 意味着只有当某些消费者订阅它时,它才会开始做事并发出值。此外,默认情况下,每个新订阅都会启动新的执行——即使对同一个可观察对象的多个订阅是并行创建的。这正是您在代码中观察到的:每个异步管道分别订阅可观察对象。
有几种方法可以解决这个问题。
确保您只有一个订阅。 @Michael D 的回答利用了这种方法。这是解决问题的一种很常见的方法。一个潜在的缺点是,当组件被销毁时,以这种方式手动创建的订阅不会自动取消订阅。在您的示例中,这可能没什么大不了的(如果 dayService$
仅发出一个值)。但是,如果组件在 http 请求完成之前被销毁,则在不编写一些额外代码(涉及实现 ngOnDestroy 生命周期方法)的情况下,不会取消此 http 请求。另一个缺点 - 如果您的组件使用 OnPush,您将需要手动调用 changeDetector.markForCheck()
。
使这个可观察"hot"。 "Hot" 表示异步操作已经启动,所有订阅者都将收到该操作的结果——无论有多少订阅者。正如@xdeepakv 所建议的那样,使用 .toPromise() 可以做到这一点。请注意,promises 根本不可取消 - 因此您将无法取消此类请求。另一个缺点 - 它仅在您的可观察对象发出单个值然后完成(例如单个 http 请求)时才有效。
您可以使用 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)
两者都有效,但如果我可以为这种情况选择,我喜欢第一个解决方案。
希望对您有所帮助
我正在使用 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次,每次调用显示的都是不同成员的值。如果你希望它只被调用一次,调用 constructor
或 ngOnInit
中的 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 意味着只有当某些消费者订阅它时,它才会开始做事并发出值。此外,默认情况下,每个新订阅都会启动新的执行——即使对同一个可观察对象的多个订阅是并行创建的。这正是您在代码中观察到的:每个异步管道分别订阅可观察对象。
有几种方法可以解决这个问题。
确保您只有一个订阅。 @Michael D 的回答利用了这种方法。这是解决问题的一种很常见的方法。一个潜在的缺点是,当组件被销毁时,以这种方式手动创建的订阅不会自动取消订阅。在您的示例中,这可能没什么大不了的(如果
dayService$
仅发出一个值)。但是,如果组件在 http 请求完成之前被销毁,则在不编写一些额外代码(涉及实现 ngOnDestroy 生命周期方法)的情况下,不会取消此 http 请求。另一个缺点 - 如果您的组件使用 OnPush,您将需要手动调用changeDetector.markForCheck()
。使这个可观察"hot"。 "Hot" 表示异步操作已经启动,所有订阅者都将收到该操作的结果——无论有多少订阅者。正如@xdeepakv 所建议的那样,使用 .toPromise() 可以做到这一点。请注意,promises 根本不可取消 - 因此您将无法取消此类请求。另一个缺点 - 它仅在您的可观察对象发出单个值然后完成(例如单个 http 请求)时才有效。
您可以使用
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)
两者都有效,但如果我可以为这种情况选择,我喜欢第一个解决方案。
希望对您有所帮助