Bypass de trampa angular y ahorro de tiempo

Con Angular, puedes hacer cualquier cosa. O casi todo. Pero a veces este "casi" insidioso lleva al hecho de que el desarrollador está perdiendo el tiempo creando soluciones o tratando de entender por qué sucede algo o por qué algo no funciona como se esperaba.



El autor del artículo que estamos traduciendo hoy dice que quiere compartir consejos que ayudarán a los desarrolladores de Angular a ahorrar algo de tiempo. Él va a hablar sobre las trampas de Angular, que él (y no solo él) tuvo la oportunidad de conocer.

No 1. La directiva personalizada que aplicó no funciona


Entonces, encontró una buena directiva Angular de terceros y decidió usarla con elementos estándar en la plantilla Angular. Genial Intentemos hacer esto:

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

Inicia la aplicación ... Y no pasa nada. Usted, como cualquier programador normal y experimentado, busca en la consola de Chrome Developer Tools. Y no ves nada allí. La directiva no funciona y Angular está en silencio.

Luego, una cabeza brillante de su equipo decide colocar la directiva entre corchetes.

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

Después de eso, habiendo perdido un poco de tiempo, vemos lo siguiente en la consola.


Aquí está la cosa: simplemente olvidamos importar el módulo con la directiva

Ahora la causa del problema es bastante obvia: simplemente olvidamos importar el módulo de directiva en el módulo de aplicación angular.
Esto lleva a una regla importante: nunca use directivas sin corchetes.

Puedes experimentar con las directivas aquí .

No 2. ViewChild regresa indefinido


Suponga que creó un enlace a un elemento de entrada de texto descrito en una plantilla angular.

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

Usted va, utilizando la función RxJS fromEvent , para crear una secuencia en la que caerá lo que se ingresa en el campo. Para hacer esto, necesita un enlace al campo de entrada, que se puede obtener utilizando el decorador Angular ViewChild :

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

Aquí, usando la función RxJS fromEvent , creamos una secuencia en la que caerán los datos ingresados ​​en el campo.
Prueba este código.


Error

Que paso

De hecho, la siguiente regla se aplica aquí: si ViewChild devuelve undefined , busque *ngIf en la plantilla.

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

Aquí él es el culpable del problema.

Además, verifique la plantilla para otras directivas estructurales o ng-template sobre el elemento del problema.
Considere posibles soluciones a este problema.

▍Variante de resolver el problema №1


Simplemente puede ocultar el elemento de plantilla si no lo necesita. En este caso, el elemento siempre seguirá existiendo y ViewChild podrá devolverle un enlace en el ngAfterViewInit .

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

▍Variante de resolver el problema №2


Otra forma de resolver este problema es 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');    ...  } } 

Aquí, tan pronto como Angular asigna un valor específico a la propiedad inputTag , creamos una secuencia a partir de los datos ingresados ​​en el campo de entrada.

Aquí hay un par de recursos útiles relacionados con este problema:

  • Aquí puede leer que los resultados de ViewChild en Angular 8 pueden ser estáticos y dinámicos.
  • Si tiene dificultades para trabajar con RxJs, eche un vistazo a este video curso.

Número 3. Ejecución de código al actualizar la lista generada con * ngFor (después de que los elementos aparecieron en el DOM)


Supongamos que tiene una directiva personalizada interesante para organizar listas desplazables. Lo aplicará a una lista que se crea utilizando la *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> 

Por lo general, en estos casos, al actualizar la lista, debe llamar a algo como scrollDirective.update para configurar el comportamiento de desplazamiento, teniendo en cuenta los cambios que se han producido en la lista.

Puede parecer que esto se puede hacer usando el gancho ngOnChanges :

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

Es cierto, aquí nos enfrentamos a un problema. Se llama al gancho antes de que el navegador muestre la lista actualizada. Como resultado, el recálculo de los parámetros de la directiva para desplazarse por la lista se realiza incorrectamente.

¿Cómo hacer una llamada justo después de *ngFor trabajo finalice?

Puede hacer esto siguiendo estos 3 simples pasos:

▍Paso número 1


Coloque los enlaces a los elementos donde se *ngFor ( #listItems ).

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

▍Paso número 2


Obtenga una lista de estos elementos utilizando el decorador Angular ViewChildren . Devuelve una entidad de tipo QueryList .

▍Paso número 3


La clase QueryList tiene una propiedad de cambios de solo lectura que QueryList eventos cada vez que cambia la lista.

 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())  } ... } 

Ahora el problema está resuelto. Aquí puedes experimentar con el ejemplo correspondiente.

Numero 4. Problemas con ActivatedRoute.queryParam que se producen cuando las consultas se pueden ejecutar sin parámetros


El siguiente código nos ayudará a comprender la esencia de este 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          });    }  } 

Algunos fragmentos de este código se hacen comentarios como #x . Considéralos:

  1. En el módulo principal de la aplicación, definimos las rutas y agregamos el RouterModule allí. Las rutas se configuran de modo que si no se proporciona una ruta en la URL, redirigimos al usuario a la página /home .
  2. Como componente para descargar, especificamos en el módulo principal AppComponent .
  3. AppComponent usa <router-outlet> para generar los componentes de ruta apropiados.
  4. Ahora lo más importante. Necesitamos obtener queryParams para la ruta desde la URL

Supongamos que tenemos la siguiente URL:

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

En este caso, queryParams se verá así:

 {accessToken: 'someTokenSequence'} 

Veamos el trabajo de todo esto en el navegador.


Probar una aplicación que implementa un sistema de enrutamiento

Aquí puede tener una pregunta sobre la esencia del problema. Tenemos los parámetros, todo funciona como se esperaba ...

Eche un vistazo a la captura de pantalla anterior del navegador y lo que se muestra en la consola. Aquí puede ver que el objeto queryParams se emite dos veces. El primer objeto está vacío; se emite durante el proceso de inicialización del enrutador angular. Solo después de eso obtenemos un objeto que contiene los parámetros de solicitud (en nuestro caso - {accessToken: 'someTokenSequence'} ).

El problema es que si no hay parámetros de solicitud en la URL, el enrutador no devolverá nada. Es decir, después de emitir el primer objeto vacío, no se emitirá el segundo objeto, también vacío, que podría indicar la ausencia de parámetros.


Al ejecutar una solicitud sin parámetros, no se emite el segundo objeto

Como resultado, resulta que si el código espera un segundo objeto del que pueda recibir datos de solicitud, no se iniciará si no hay parámetros de solicitud en la URL.

¿Cómo resolver este problema? Aquí RxJs puede ayudarnos. Crearemos dos objetos observables basados ​​en ActivatedRoute.queryParams . Como de costumbre, considere una solución paso a paso al problema.

▍Paso número 1


El primer objeto observable, paramsInUrl$ , paramsInUrl$ datos si queryParams no queryParams vacío:

 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)        );     ...    } } 

▍Paso número 2


El segundo objeto observable, noParamsInUrl$ , noParamsInUrl$ valor vacío solo si no se encontraron parámetros de solicitud en la 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(() => ({}))        );            ...    } } 

▍Paso número 3


Ahora combine los objetos observados usando la función de fusión 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);        });    } } 

Ahora el param$ object observado param$ valor solo una vez, independientemente de si algo está queryParams en queryParams (se queryParams un objeto con parámetros de consulta) o no (se queryParams un objeto vacío).

Puedes experimentar con este código aquí .

No 5. Páginas lentas


Supongamos que tiene un componente que muestra algunos datos formateados:

 // 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 resuelve dos problemas:

  1. Muestra una matriz de elementos (se supone que esta operación se realiza una vez). Además, formatea lo que se muestra llamando al método formatItem .
  2. Muestra las coordenadas del mouse (este valor obviamente se actualizará muy a menudo).

No espera que este componente tenga problemas de rendimiento. Por lo tanto, ejecute una prueba de rendimiento solo para cumplir con todas las formalidades. Sin embargo, algunas rarezas aparecen durante esta prueba.


Muchas llamadas de formato de artículo y mucha carga de CPU

Cual es el problema Pero el hecho es que cuando Angular vuelve a dibujar la plantilla, llama a todas las funciones de la plantilla (en nuestro caso, la función formatItem ). Como resultado, si se realizan cálculos pesados ​​en las funciones de la plantilla, esto pone a prueba el procesador y afecta la forma en que los usuarios perciben la página correspondiente.

¿Cómo arreglarlo? Es suficiente realizar los cálculos realizados en formatItem de antemano y mostrar los datos que ya están listos en la 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 + '%';    } } 

Ahora la prueba de rendimiento parece mucho más decente.


Solo 6 llamadas de ítem de formato y baja carga de procesador

Ahora la aplicación funciona mucho mejor. Pero la solución utilizada aquí tiene algunas características que no siempre son agradables:

  • Como mostramos las coordenadas del mouse en la plantilla, el evento mousemove aún activa una verificación de cambio. Pero, dado que necesitamos las coordenadas del mouse, no podemos deshacernos de esto.
  • Si el mousemove eventos mousemove solo necesita realizar algunos cálculos (que no afectan lo que se muestra en la página), entonces para acelerar la aplicación, puede hacer lo siguiente:

    1. Puede usar NgZone.runOutsideOfAngular dentro de la función de controlador de eventos. Esto evita que se mousemove las comprobaciones de cambios cuando se mousemove evento mousemove (esto solo afectará a este controlador).
    2. Puede evitar el parche zone.js para algunos eventos utilizando la siguiente línea de código en polyfills.ts . Esto afectará a toda la aplicación Angular.

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

Si está interesado en los problemas de mejorar el rendimiento de las aplicaciones angulares, aquí , aquí , aquí y aquí , materiales útiles sobre esto.

Resumen


Ahora que ha leído este artículo, deberían aparecer 5 nuevas herramientas en su arsenal de desarrolladores Angular con las que puede resolver algunos problemas comunes. Esperamos que los consejos que encontraste aquí te ayuden a ahorrar tiempo.

Estimados lectores! ¿Sabes algo que te ayude a ahorrar tiempo al desarrollar aplicaciones angulares?

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


All Articles