Como interromper chamadas antigas em chamadas de função repetitivas

Oct 27 2020

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

2 PoulKruijt Oct 27 2020 at 13:09

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>('')

IanPreglo Oct 27 2020 at 13:08

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:

  1. criando uma variável de tempo limite global
  2. limpe a variável de tempo limite quando houver uma mudança na entrada de pesquisa
  3. 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.