Desvio angular de armadilha e economia de tempo

Com o Angular, você pode fazer qualquer coisa. Ou quase tudo. Mas às vezes esse "quase" insidioso leva ao fato de que o desenvolvedor está perdendo tempo criando soluções alternativas ou tentando entender por que algo está acontecendo ou por que algo não está funcionando como o esperado.



O autor do artigo que estamos traduzindo hoje diz que deseja compartilhar dicas que ajudarão os desenvolvedores Angular a economizar algum tempo. Ele vai falar sobre as armadilhas do Angular, que ele (e não apenas ele) teve a chance de conhecer.

No. 1. A diretiva personalizada que você aplicou não funciona


Então, você encontrou uma boa diretiva Angular de terceiros e decidiu usá-la com elementos padrão no modelo Angular. Ótimo! Vamos tentar fazer isso:

<span awesomeTooltip="'Tooltip text'"> </span> 

Você inicia o aplicativo ... E nada acontece. Você, como qualquer programador experiente e normal, olha para o console das Ferramentas do desenvolvedor do Chrome. E você não vê nada lá. A diretiva não funciona e Angular é silencioso.

Então, uma cabeça brilhante da sua equipe decide colocar a diretiva entre colchetes.

 <span [awesomeTooltip]="'Tooltip text'"> </span> 

Depois disso, tendo perdido um pouco de tempo, vemos o seguinte no console.


Aqui está a coisa: nós apenas esquecemos de importar o módulo com a diretiva

Agora, a causa do problema é bastante óbvia: esquecemos de importar o módulo de diretiva para o módulo de aplicativo Angular.
Isso leva a uma regra importante: nunca use diretivas sem colchetes.

Você pode experimentar as diretrizes aqui .

No. 2. ViewChild retorna indefinido


Suponha que você tenha criado um link para um elemento de entrada de texto descrito em um modelo Angular.

 <input type="text" name="fname" #inputTag> 

Você está usando a função RxJS fromEvent para criar um fluxo no qual o que é inserido no campo cairá. Para fazer isso, você precisa de um link para o campo de entrada, que pode ser obtido usando o decorador Angular ViewChild :

 class SomeComponent implements AfterViewInit {    @ViewChild('inputTag') inputTag: ElementRef;    ngAfterViewInit(){        const input$ = fromEvent(this.inputTag.nativeElement, 'keyUp')    } ...   } 

Aqui, usando a função RxJS fromEvent , criamos um fluxo no qual os dados inseridos no campo cairão.
Teste este código.


Erro

O que aconteceu

De fato, a regra a seguir se aplica aqui: se ViewChild retornar undefined , procure *ngIf no modelo.

 <div *ngIf="someCondition">    <input type="text" name="fname" #inputTag> </div> 

Aqui ele é o culpado do problema.

Além disso, verifique o modelo para outras diretivas estruturais ou ng-template acima do elemento do problema.
Considere possíveis soluções para esse problema.

Ar Variante de resolver o problema №1


Você pode simplesmente ocultar o elemento do modelo se não precisar dele. Nesse caso, o elemento sempre continuará existindo e o ViewChild poderá retornar um link para ele no gancho ngAfterViewInit .

 <div [hidden]="!someCondition">    <input type="text" name="fname" #inputTag> </div> 

Ar Variante de resolver o problema №2


Outra maneira de resolver esse problema é usar setters.

 class SomeComponent {   @ViewChild('inputTag') set inputTag(input: ElementRef|null) {    if(!input) return;       this.doSomething(input);  }  doSomething(input) {    const input$ = keysfromEvent(input.nativeElement, 'keyup');    ...  } } 

Aqui, assim que Angular atribui um valor específico à propriedade inputTag , criamos um fluxo a partir dos dados inseridos no campo de entrada.

Aqui estão alguns recursos úteis relacionados a esse problema:

  • Aqui você pode ler que os resultados do ViewChild no Angular 8 podem ser estáticos e dinâmicos.
  • Se você está tendo dificuldades para trabalhar com RxJs, dê uma olhada neste curso em vídeo.

Número 3. Execução de código ao atualizar a lista gerada com * ngFor (após os elementos aparecerem no DOM)


Suponha que você tenha alguma diretiva personalizada interessante para organizar listas roláveis. Você o aplicará a uma lista criada usando a *ngFor Angular *ngFor .

 <div *ngFor="let item of itemsList; let i = index;"     [customScroll]     >  <p *ngFor="let item of items" class="list-item">{{item}}</p>   </div> 

Normalmente, nesses casos, ao atualizar a lista, você precisa chamar algo como scrollDirective.update para configurar o comportamento da rolagem, levando em consideração as alterações que ocorreram na lista.

Pode parecer que isso possa ser feito usando o gancho ngOnChanges :

 class SomeComponent implements OnChanges {  @Input() itemsList = [];   @ViewChild(CustomScrollDirective) scroll: CustomScrollDirective;  ngOnChanges(changes) {    if (changes.itemsList) {      this.scroll.update();    }  } ... } 

É verdade que aqui estamos diante de um problema. O gancho é chamado antes do navegador exibir a lista atualizada. Como resultado, o recálculo dos parâmetros da diretiva para rolar a lista é realizado incorretamente.

Como fazer uma ligação logo após *ngFor terminar o trabalho?

Você pode fazer isso seguindo estas 3 etapas simples:

▍Etapa número 1


Coloque os links para os elementos em que *ngFor ( #listItems ) é #listItems .

 <div [customScroll]>    <p *ngFor="let item of items" #listItems>{{item}}</p> </div> 

▍Etapa número 2


Obtenha uma lista desses elementos usando o decorador Angular ViewChildren . Retorna uma entidade do tipo QueryList .

▍Etapa número 3


A classe QueryList possui uma propriedade de alterações somente leitura que QueryList eventos toda vez que a lista é alterada.

 class SomeComponent implements AfterViewInit {  @Input() itemsList = [];   @ViewChild(CustomScrollDirective) scroll: CustomScrollDirective;  @ViewChildren('listItems') listItems: QueryList<any>;  private sub: Subscription;   ngAfterViewInit() {    this.sub = this.listItems.changes.subscribe(() => this.scroll.update())  } ... } 

Agora o problema está resolvido. Aqui você pode experimentar o exemplo correspondente.

Número 4. Problemas com ActivatedRoute.queryParam que ocorrem quando as consultas podem ser executadas sem parâmetros


O código a seguir nos ajudará a entender a essência desse problema.

 // app-routing.module.ts const routes: Routes = [    {path: '', redirectTo: '/home', pathMatch: 'full'},    {path: 'home', component: HomeComponent},  ];   @NgModule({    imports: [RouterModule.forRoot(routes)], //  #1    exports: [RouterModule]  })  export class AppRoutingModule { }    //app.module.ts  @NgModule({    ...    bootstrap: [AppComponent] //  #2  })  export class AppModule { }   // app.component.html  <router-outlet></router-outlet> //  #3    // app.component.ts  export class AppComponent implements OnInit {    title = 'QueryTest';     constructor(private route: ActivatedRoute) { }     ngOnInit() {      this.route.queryParams          .subscribe(params => {            console.log('saveToken', params); //  #4          });    }  } 

Alguns fragmentos deste código são feitos comentários como #x . Considere-os:

  1. No módulo principal do aplicativo, definimos as rotas e adicionamos o RouterModule lá. As rotas são configuradas para que, se uma rota não for fornecida no URL, redirecionemos o usuário para a /home page.
  2. Como componente para download, especificamos no módulo principal AppComponent .
  3. AppComponent usa <router-outlet> para AppComponent os componentes de rota apropriados.
  4. Agora a coisa mais importante. Precisamos obter queryParams para a rota a partir do URL

Suponha que tenhamos o seguinte URL:

 https://localhost:4400/home?accessToken=someTokenSequence 

Nesse caso, queryParams terá a seguinte aparência:

 {accessToken: 'someTokenSequence'} 

Vejamos o trabalho de tudo isso no navegador.


Testando um aplicativo que implementa um sistema de roteamento

Aqui você pode ter uma pergunta sobre a essência do problema. Temos os parâmetros, tudo funciona como esperado ...

Dê uma olhada na captura de tela acima do navegador e no que é exibido no console. Aqui você pode ver que o objeto queryParams é emitido duas vezes. O primeiro objeto está vazio e é emitido durante o processo de inicialização do roteador Angular. Somente depois disso obtemos um objeto que contém os parâmetros de solicitação (no nosso caso - {accessToken: 'someTokenSequence'} ).

O problema é que, se não houver parâmetros de solicitação no URL, o roteador não retornará nada. Ou seja - depois de emitir o primeiro objeto vazio, o segundo objeto, também vazio, que pode indicar a ausência de parâmetros, não será emitido.


Ao executar uma solicitação sem parâmetros, o segundo objeto não é emitido

Como resultado, verifica-se que, se o código esperar um segundo objeto do qual possa receber dados de solicitação, ele não será iniciado se não houver parâmetros de solicitação no URL.

Como resolver este problema? Aqui os RxJs podem nos ajudar. Vamos criar dois objetos observáveis ​​baseados em ActivatedRoute.queryParams . Como sempre - considere uma solução passo a passo para o problema.

▍Etapa número 1


O primeiro objeto observável, paramsInUrl$ , paramsInUrl$ dados se queryParams não queryParams vazio:

 export class AppComponent implements OnInit {    constructor(private route: ActivatedRoute,                private locationService: Location) {    }    ngOnInit() {             //            //              const paramsInUrl$ = this.route.queryParams.pipe(            filter(params => Object.keys(params).length > 0)        );     ...    } } 

▍Etapa número 2


O segundo objeto observável, noParamsInUrl$ , noParamsInUrl$ valor vazio apenas se nenhum parâmetro de solicitação for encontrado na URL:

 export class AppComponent implements OnInit {    title = 'QueryTest';    constructor(private route: ActivatedRoute,                private locationService: Location) {    }    ngOnInit() {                 ...        //   ,     ,   URL        //             const noParamsInUrl$ = this.route.queryParams.pipe(            filter(() => !this.locationService.path().includes('?')),            map(() => ({}))        );            ...    } } 

▍Etapa número 3


Agora combine os objetos observados usando a função de mesclagem RxJS:

 export class AppComponent implements OnInit {    title = 'QueryTest';    constructor(private route: ActivatedRoute,                private locationService: Location) {    }    ngOnInit() {             //            //              const paramsInUrl$ = this.route.queryParams.pipe(            filter(params => Object.keys(params).length > 0)        );        //   ,     ,   URL        //             const noParamsInUrl$ = this.route.queryParams.pipe(            filter(() => !this.locationService.path().includes('?')),            map(() => ({}))        );        const params$ = merge(paramsInUrl$, noParamsInUrl$);        params$.subscribe(params => {            console.log('saveToken', params);        });    } } 

Agora, o param$ object observado param$ valor apenas uma vez - independentemente de algo estar queryParams em queryParams (um objeto com parâmetros de consulta é queryParams ) ou não (um objeto vazio é queryParams ).

Você pode experimentar esse código aqui .

No. 5. Páginas lentas


Suponha que você tenha um componente que exiba alguns dados formatados:

 // home.component.html <div class="wrapper" (mousemove)="mouseCoordinates = {x: $event.x, y: $event.y}">  <div *ngFor="let item of items">     <span>{{formatItem(item)}}</span>  </div> </div> {{mouseCoordinates | json}} // home.component.ts export class HomeComponent {    items = [1, 2, 3, 4, 5, 6];    mouseCoordinates = {};    formatItem(item) {              //           const t = Array.apply(null, Array(5)).map(() => 1);             console.log('formatItem');        return item + '%';    } } 

Este componente resolve dois problemas:

  1. Ele exibe uma matriz de elementos (pressupõe-se que esta operação seja executada uma vez). Além disso, ele formata o que é exibido chamando o método formatItem .
  2. Ele exibe as coordenadas do mouse (esse valor obviamente será atualizado com muita frequência).

Você não espera que este componente tenha problemas de desempenho. Portanto, execute um teste de desempenho apenas para cumprir todas as formalidades. No entanto, algumas curiosidades aparecem durante este teste.


Muitas chamadas formatItem e bastante carga de CPU

Qual é o problema? Mas o fato é que, quando o Angular redesenha o modelo, ele chama todas as funções do modelo (no nosso caso, a função formatItem ). Como resultado, se qualquer cálculo pesado for realizado nas funções do modelo, isso sobrecarrega o processador e afeta a maneira como os usuários percebem a página correspondente.

Como consertar isso? Basta realizar os cálculos realizados no formatItem com antecedência e exibir os dados já prontos na página.

 // home.component.html <div class="wrapper" (mousemove)="mouseCoordinates = {x: $event.x, y: $event.y}">  <div *ngFor="let item of displayedItems">    <span>{{item}}</span>  </div> </div> {{mouseCoordinates | json}} // home.component.ts @Component({    selector: 'app-home',    templateUrl: './home.component.html',    styleUrls: ['./home.component.sass'] }) export class HomeComponent implements OnInit {    items = [1, 2, 3, 4, 5, 6];    displayedItems = [];    mouseCoordinates = {};    ngOnInit() {        this.displayedItems = this.items.map((item) => this.formatItem(item));    }    formatItem(item) {        console.log('formatItem');        const t = Array.apply(null, Array(5)).map(() => 1);        return item + '%';    } } 

Agora, o teste de desempenho parece muito mais decente.


Chamadas de item e baixa carga do processador

Agora, o aplicativo funciona muito melhor. Mas a solução usada aqui tem alguns recursos que nem sempre são agradáveis:

  • Como exibimos as coordenadas do mouse no modelo, o evento mousemove ainda aciona uma verificação de alteração. Mas, como precisamos das coordenadas do mouse, não podemos nos livrar disso.
  • Se o manipulador de eventos mousemove precisar executar apenas alguns cálculos (que não afetam o que é exibido na página), para acelerar o aplicativo, você pode fazer o seguinte:

    1. Você pode usar NgZone.runOutsideOfAngular dentro da função de manipulador de eventos. Isso impede que a verificação das alterações seja iniciada quando o evento mousemove (isso afetará apenas esse manipulador).
    2. Você pode impedir o patch zone.js para alguns eventos usando a seguinte linha de código em polyfills.ts . Isso afetará todo o aplicativo Angular.

 * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; //      

Se você estiver interessado nas questões de melhoria do desempenho de aplicativos Angular - aqui , aqui , aqui e aqui - materiais úteis sobre isso.

Sumário


Agora que você leu este artigo, cinco novas ferramentas devem aparecer no seu arsenal de desenvolvedores Angular com o qual você pode resolver alguns problemas comuns. Esperamos que as dicas que você encontrou aqui ajudem você a economizar algum tempo.

Caros leitores! Você sabe alguma coisa que ajuda a economizar tempo ao desenvolver aplicativos Angular?

Source: https://habr.com/ru/post/pt459304/


All Articles