Como interromper chamadas antigas em chamadas de função repetitivas
Estou usando a entrada de pesquisa para pesquisar no servidor em cada entrada de letra. Mas não sei como interromper ligações antigas. Quando eu digito uma palavra com 6 letras, a lista é atualizada 6 vezes consecutivas após segundos. Como posso interromper chamadas antigas em uma nova chamada?
Entrada em html
<input class="form-control form-control-rounded" placeholder="" [(ngModel)]="searchBody.searchString" (ngModelChange)="searchChanged()">
Função de acionamento da entrada:
async searchChanged() {
this.spinner.show('mainSpinner');
await this.getOrders(this.searchBody);
this.spinner.hide('mainSpinner');
}
Ativação de função de searchChanged ()
async getOrders(body) {
let response: any = await this.orderService.getOrders(body);
this.totalOrders = response[0].pagingData.totalCount;
this.orders = response[0].data;
}
Função em orderService
async getOrders(body: any) {
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json'
})
}
const url = this.baseURL;
let promise = await this.httpClient.post(url , body, httpOptions).toPromise();
let response = await Promise.all([promise]);
return response;
}
Alguma ideia?
Respostas
Você não será capaz de fazer isso perfeitamente com promessas. É aqui que Observablesse torna muito útil. Por exemplo, com o uso de, switchMapvocê pode cancelar solicitações anteriores quando uma nova solicitação surge, e com a debounceTimeoperadora, você pode controlar um certo debounce em suas solicitações.
Isso também requer o uso do asynctubo para limpeza. A melhor coisa seria fazer também seu totalOrderse ordersobserváveis, para que você não precise subscribeentrar no componente e, portanto, também cancele a assinatura ao destruir.
Não vejo nenhum código de modelo para essas duas variáveis, então vou apenas mostrar um exemplo de como fazer isso apenas com a variável de pesquisa:
Removendo coisas desnecessárias por uma questão de simplicidade
<input [ngModel]="searchString$ | async" (ngModelChange)="searchString$.next($event)">
export class SearchComponent implements OnDestroy {
readonly searchString$ = new ReplaySubject<string>(1);
private readonly searchSub: Subscription = this.searchString$.pipe(
tap(() => this.spinner.show('mainSpinner')),
debounceTime(200),
switchMap((search) => this.getOrders(search)),
tap(() => this.spinner.hide('mainSpinner'))
).subscribe()
ngOnDestroy(): void {
this.searchSub.unsubscribe();
}
private getOrders(search: string): Observable<void> {
return this.orderService.getOrders({
...this.searchBody,
searchString: search
}).pipe(
map((response) => {
this.totalOrders = response.pagingData.totalCount;
this.orders = response.data;
})
)
}
}
export class OrderService {
getOrders(body: any): Observable<any> {
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json'
})
}
const url = this.baseURL;
return this.httpClient.post(url, body, httpOptions);
}
}
O switchMapfará com que a solicitação http anterior seja cancelada. Há também um debounceTimede 200, que garantirá que, se você continuar digitando, nada será solicitado até que o usuário pare de digitar por 200ms. Isso reduzirá drasticamente a quantidade de solicitações (desnecessárias) para o seu servidor.
Solução alternativa
O subscribe/unsubscribepadrão é algo que eu não gosto, e a equipe angular na verdade tem aquele asynctubo bacana que cuida de todas essas coisas. Assim, você pode até simplificar mais seu código usando isso. Vou dar um exemplo de código:
Considere este como o seu HTML:
<input [ngModel]="searchString$ | async (ngModelChange)="searchString$.next($event)">
<div *ngFor="let order of orders$ | async; trackBy: trackByOrder"></div> <div>Total orders: {{ totalOrders$ | async}}</div>
Você pode atualizar seu componente assim:
export class SearchComponent {
readonly searchString$ = new ReplaySubject<string>(1); readonly orderReponse$ = this.searchString$.pipe( tap(() => this.spinner.show('mainSpinner')), debounceTime(200), switchMap((search) => this.orderService.getOrders({ ...this.searchBody, searchString: search })), tap(() => this.spinner.hide('mainSpinner')), shareReplay({ refCount: true, bufferSize: 1 }) ); readonly orders$ = this.orderReponse$.pipe( map(({ data }) => data) ); readonly totalOrders$ = this.orderReponse$.pipe(
map(({ pagingData }) => pagingData.totalCount)
);
constructor(private orderService: OrderService) {}
trackByOrder(idx: number, order: any): string {
// very important for performance sake. Return a unique order id
}
}
observação: se você quiser que a pesquisa comece na inicialização do componente e não quando um usuário digitar, você deve alterar o ReplaySubject<string>(1)para umBehaviorSubject<string>('')
As pesquisas geralmente precisam de um tempo limite para contabilizar o tempo que o usuário está pressionando as teclas antes de enviar a solicitação real; normalmente, fica entre 150 e 200 ms.
Eu sugeriria:
- criando uma variável de tempo limite global
- limpe a variável de tempo limite quando houver uma mudança na entrada de pesquisa
- enviar a solicitação de pesquisa após alguns segundos.
traduzindo isso para código, sua função searchChanged () seria semelhante a abaixo:
var searchRequestTimeout;
async searchChanged() {
this.spinner.show('mainSpinner');
clearTimeout(this.searchRequestTimeout);
this.searchRequestTimeout = setTimeout(() => {
await this.getOrders(this.searchBody);
this.spinner.hide('mainSpinner');
}, 150);
}
Isso garantiria que uma solicitação nunca fosse enviada enquanto o usuário ainda estivesse digitando.