El autor del artículo, cuya primera parte de la traducción estamos publicando, dice que ha estado trabajando en una aplicación angular a gran escala en
Trade Me durante aproximadamente dos años. En los últimos años, el equipo de desarrollo de aplicaciones ha estado mejorando constantemente el proyecto, tanto en términos de calidad de código como de rendimiento.
Esta serie de materiales se centrará en los enfoques de desarrollo utilizados por el equipo de Trade Me, que se expresan en forma de más de dos docenas de recomendaciones sobre tecnologías como Angular, TypeScript, RxJS y @ ngrx / store. Además, se prestará atención a las técnicas de programación universal destinadas a hacer que el código de la aplicación sea más limpio y preciso.
1. Acerca de trackBy
Usando
ngFor
para recorrer matrices en plantillas, use esta construcción con la función
trackBy
, que devuelve un identificador único para cada elemento.
▍ Explicaciones
Cuando la matriz cambia, Angular vuelve a representar todo el árbol DOM. Pero si usa
trackBy
, el sistema sabrá qué elemento ha cambiado y realizará cambios en el DOM que se aplican solo a ese elemento en particular. Los detalles sobre esto se pueden encontrar
aquí .
▍To
<li *ngFor="let item of items;">{{ item }}</li>
▍Después
// <li *ngFor="let item of items; trackBy: trackByFn">{{ item }}</li> // trackByFn(index, item) { return item.id; // id, }
2. Palabras clave const y let
Si va a declarar una variable cuyo valor no planea cambiar, use la palabra clave
const
.
▍ Explicaciones
El uso apropiado de las palabras clave
let
y
const
aclara la intención con respecto al uso de entidades declaradas que las usan. Además, este enfoque facilita el reconocimiento de los problemas causados por sobrescribir accidentalmente valores constantes. En esta situación, se produce un error de compilación. Además, mejora la legibilidad del código.
▍To
let car = 'ludicrous car'; let myCar = `My ${car}`; let yourCar = `Your ${car}; if (iHaveMoreThanOneCar) { myCar = `${myCar}s`; } if (youHaveMoreThanOneCar) { yourCar = `${youCar}s`; }
▍Después
// car , car const car = 'ludicrous car'; let myCar = `My ${car}`; let yourCar = `Your ${car}; if (iHaveMoreThanOneCar) { myCar = `${myCar}s`; } if (youHaveMoreThanOneCar) { yourCar = `${youCar}s`; }
3. Operadores de transportadores
Cuando trabaje con RxJS, use operadores canalizados.
▍ Explicaciones
Los operadores transportados admiten el algoritmo de vibración de árbol, es decir, cuando se importan, solo se incluirá en el proyecto el código que se planea ejecutar. Esto también facilita la identificación de declaraciones no utilizadas en los archivos.
Tenga en cuenta que esta recomendación es relevante para Angular versión 5.5 y superior.
▍To
import 'rxjs/add/operator/map'; import 'rxjs/add/operator/take'; iAmAnObservable .map(value => value.item) .take(1);
▍Después
import { map, take } from 'rxjs/operators'; iAmAnObservable .pipe( map(value => value.item), take(1) );
4. Aislamiento de correcciones de API
No todas las API son completamente estables y sin errores. Por lo tanto, a veces es necesario introducir algo de lógica en el código destinado a solucionar problemas de API. En lugar de colocar esta lógica en componentes donde se usan API parcheadas, sería mejor aislarla en algún lugar, por ejemplo, en un servicio, y referirse desde el componente ya no es a la API problemática, sino al servicio correspondiente.
▍ Explicaciones
El enfoque propuesto permite mantener las correcciones "más cerca" de la API, es decir, lo más cerca posible del código desde el que se realizan las solicitudes de red. Como resultado, se reduce la cantidad de código de aplicación que interactúa con API problemáticas. Además, resulta que todas las correcciones están en un solo lugar, como resultado será más fácil trabajar con ellas. Si tiene que corregir errores en la API, entonces es mucho más fácil hacer esto en un solo archivo que distribuir estas correcciones en toda la aplicación. Esto facilita no solo la creación de correcciones, sino también la búsqueda del código apropiado en el proyecto y su soporte.
Además, puede crear sus propias etiquetas, como
API_FIX
(que se parece a la etiqueta
TODO
), y marcar las correcciones con ellas. Esto facilita encontrar tales soluciones.
5. Suscripción en la plantilla.
Evite suscribirse a observables desde componentes. En cambio, suscríbase a ellos en plantillas.
▍ Explicaciones
Los operadores canalizados asíncronos se anulan automáticamente, lo que simplifica el código y elimina la necesidad de una administración de suscripción manual. Esto, además, reduce el riesgo de que el desarrollador se olvide de darse de baja del componente, lo que puede provocar pérdidas de memoria. Es posible reducir la probabilidad de pérdidas de memoria utilizando reglas de interinterruptor destinadas a identificar objetos observables de los que no se dieron de baja.
Además, la aplicación de este enfoque lleva al hecho de que los componentes dejan de ser componentes con estado, lo que puede generar errores cuando los datos cambian fuera de la suscripción.
▍To
// <p>{{ textToDisplay }}</p> // iAmAnObservable .pipe( map(value => value.item), takeUntil(this._destroyed$) ) .subscribe(item => this.textToDisplay = item);
▍Después
// <p>{{ textToDisplay$ | async }}</p> // this.textToDisplay$ = iAmAnObservable .pipe( map(value => value.item) );
6. Eliminar suscripciones
Al suscribirse a objetos supervisados, asegúrese siempre de que las suscripciones a ellos se eliminen correctamente utilizando operadores como
take
,
takeUntil
etc.
▍ Explicaciones
Si no se da de baja del objeto observado, esto provocará pérdidas de memoria, ya que la secuencia del objeto observado permanecerá abierta, lo que es posible incluso después de que se destruya el componente o después de que el usuario vaya a otra página de la aplicación.
Aún mejor sería crear una regla linter para detectar objetos observados con una suscripción válida para ellos.
▍To
iAmAnObservable .pipe( map(value => value.item) ) .subscribe(item => this.textToDisplay = item);
▍Después
Use el operador
takeUntil
si desea observar los cambios de un objeto hasta que otro objeto observado genere un cierto valor:
private destroyed$ = new Subject(); public ngOnInit (): void { iAmAnObservable .pipe( map(value => value.item)
Usar algo como
this
es un patrón utilizado para controlar la eliminación de suscripciones a muchos objetos observados en un componente.
Use
take
si solo necesita el primer valor devuelto por el objeto observado:
iAmAnObservable .pipe( map(value => value.item), take(1), takeUntil(this._destroyed$) ) .subscribe(item => this.textToDisplay = item);
Tenga en cuenta que aquí usamos
takeUntil
con
take
. Esto se hace para evitar pérdidas de memoria causadas por el hecho de que la suscripción no condujo a obtener el valor hasta que se destruyó el componente. Si la función
takeUntil
no se usara
takeUntil
, la suscripción existiría hasta que se recibiera el primer valor, pero como el componente ya se habría destruido, este valor nunca se habría recibido, lo que provocaría una pérdida de memoria.
7. Utilizando operadores adecuados
Usando operadores de suavizado con objetos observables, aplique aquellos que correspondan a las características del problema que se está resolviendo.
- Use
switchMap
cuando necesite ignorar la acción programada anterior cuando llegue una nueva acción. - Use
mergeMap
en caso de que necesite procesar todas las acciones despachadas en paralelo. - Utilice
concatMap
cuando las acciones deban procesarse una tras otra, en el orden en que se reciben. - Use
exhaustMap
en situaciones en las que, en el proceso de procesamiento de acciones recibidas anteriormente, necesite ignorar otras nuevas.
Los detalles sobre esto se pueden encontrar
aquí .
▍ Explicaciones
Usar, si es posible, un operador, en lugar de lograr el mismo efecto combinando varios operadores en una cadena, reduce la cantidad de código de aplicación que debe enviarse al usuario. El uso de un operador seleccionado incorrectamente puede conducir a un comportamiento incorrecto del programa, ya que diferentes operadores procesan los objetos observados de manera diferente.
8. Carga perezosa
Luego, siempre que sea posible, intente organizar la carga diferida de los módulos de la aplicación angular. Esta técnica se reduce al hecho de que algo se carga solo si se usa. Por ejemplo, un componente se carga solo cuando debe mostrarse.
▍ Explicaciones
La carga diferida reduce el tamaño de los materiales de la aplicación que el usuario tiene que descargar. Esto puede mejorar la velocidad de descarga de la aplicación debido al hecho de que los módulos no utilizados no se transfieren del servidor a los clientes.
▍To
// app.routing.ts { path: 'not-lazy-loaded', component: NotLazyLoadedComponent }
▍Después
// app.routing.ts { path: 'lazy-load', loadChildren: 'lazy-load.module#LazyLoadModule' } // lazy-load.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { LazyLoadComponent } from './lazy-load.component'; @NgModule({ imports: [ CommonModule, RouterModule.forChild([ { path: '', component: LazyLoadComponent } ]) ], declarations: [ LazyLoadComponent ] }) export class LazyModule {}
9. Acerca de las suscripciones dentro de otras suscripciones
A veces, para realizar alguna acción, es posible que necesite datos de varios objetos observables. En tal situación, evite crear suscripciones a dichos objetos dentro de los bloques de
subscribe
de otros objetos observables. En su lugar, utilice operadores adecuados para encadenar comandos. Entre tales operadores, uno puede notar con
withLatestFrom
y
combineLatest
. Considere los ejemplos y luego comente sobre ellos.
▍To
firstObservable$.pipe( take(1) ) .subscribe(firstValue => { secondObservable$.pipe( take(1) ) .subscribe(secondValue => { console.log(`Combined values are: ${firstValue} & ${secondValue}`); }); });
▍Después
firstObservable$.pipe( withLatestFrom(secondObservable$), first() ) .subscribe(([firstValue, secondValue]) => { console.log(`Combined values are: ${firstValue} & ${secondValue}`); });
▍ Explicaciones
Si hablamos de legibilidad, de la complejidad del código o de los signos de código incorrecto, cuando el programa no utiliza las funciones de RxJS en su totalidad, esto indica que el desarrollador no conoce bien la API de RxJS. Si tocamos el tema del rendimiento, resulta que si el objeto observable necesita algo de tiempo para inicializarse, se suscribirá a
firstObservable
, entonces el sistema esperará a que se complete la operación, y solo después de eso comenzará el trabajo con el segundo objeto observable. Si estos objetos son solicitudes de red, se verá como una ejecución síncrona de solicitudes.
10. Sobre escribir
Siempre trate de declarar variables o constantes con un tipo diferente a
any
.
▍ Explicaciones
Si se declara una variable o constante en TypeScript sin especificar un tipo, el tipo se deducirá en función del valor que se le asigne. Esto puede llevar a problemas. Considere un ejemplo clásico de comportamiento del sistema en una situación similar:
const x = 1; const y = 'a'; const z = x + y; console.log(`Value of z is: ${z}` // Value of z is 1a
Se supone que
y
es un número aquí, pero nuestro programa no lo sabe, por lo que muestra algo que parece incorrecto, pero no genera ningún mensaje de error. Se pueden evitar problemas similares asignando tipos apropiados a variables y constantes.
Reescribimos el ejemplo anterior:
const x: number = 1; const y: number = 'a'; const z: number = x + y; // : Type '"a"' is not assignable to type 'number'. const y:number
Esto ayuda a evitar errores de tipo de datos.
Otra ventaja de un enfoque sistemático para escribir es que simplifica la refactorización y reduce la probabilidad de errores durante este proceso.
Considere un ejemplo:
public ngOnInit (): void { let myFlashObject = { name: 'My cool name', age: 'My cool age', loc: 'My cool location' } this.processObject(myFlashObject); } public processObject(myObject: any): void { console.log(`Name: ${myObject.name}`); console.log(`Age: ${myObject.age}`); console.log(`Location: ${myObject.loc}`); } // Name: My cool name Age: My cool age Location: My cool location
Supongamos que queremos cambiar, en
myFlashObject
, el nombre de la propiedad
loc
a la
location
y cometimos un error
myFlashObject
editar el código:
public ngOnInit (): void { let myFlashObject = { name: 'My cool name', age: 'My cool age', location: 'My cool location' } this.processObject(myFlashObject); } public processObject(myObject: any): void { console.log(`Name: ${myObject.name}`); console.log(`Age: ${myObject.age}`); console.log(`Location: ${myObject.loc}`); } // Name: My cool name Age: My cool age Location: undefined
Si no se utiliza la
myFlashObject
al crear el objeto
myFlashObject
, en nuestro caso, el sistema supone que el valor de la propiedad
loc
de
myFlashObject
undefined
está
undefined
. Ella no cree que
loc
pueda ser un nombre de propiedad no válido.
Si se usa la escritura al describir el objeto
myFlashObject
, en una situación similar veremos, al compilar el código, un maravilloso mensaje de error:
type FlashObject = { name: string, age: string, location: string } public ngOnInit (): void { let myFlashObject: FlashObject = { name: 'My cool name', age: 'My cool age', // Type '{ name: string; age: string; loc: string; }' is not assignable to type 'FlashObjectType'. Object literal may only specify known properties, and 'loc' does not exist in type 'FlashObjectType'. loc: 'My cool location' } this.processObject(myFlashObject); } public processObject(myObject: FlashObject): void { console.log(`Name: ${myObject.name}`); console.log(`Age: ${myObject.age}`) // Property 'loc' does not exist on type 'FlashObjectType'. console.log(`Location: ${myObject.loc}`); }
Si está comenzando a trabajar en un nuevo proyecto, será útil establecer, en el archivo
tsconfig.json
, la opción
strict:true
para habilitar la verificación estricta de tipos.
11. Sobre el uso de linter
Tslint tiene varias reglas estándar como
no-any ,
no-magic-numbers ,
no-console . El linter se puede personalizar editando el archivo
tslint.json
para organizar la verificación del código de acuerdo con ciertas reglas.
▍ Explicaciones
Usar un linter para verificar el código significa que si algo aparece en el código que está prohibido por las reglas, recibirá un mensaje de error. Esto contribuye a la uniformidad del código del proyecto, mejora su legibilidad. Vea otras reglas de tslint aquí.
Cabe señalar que algunas reglas incluyen medios para corregir lo que se considera inadmisible de acuerdo con ellas. Si es necesario, puede crear sus propias reglas. Si está interesado, eche un vistazo a
este material, que analiza la creación de reglas personalizadas para el linter utilizando
TSQuery .
▍To
public ngOnInit (): void { console.log('I am a naughty console log message'); console.warn('I am a naughty console warning message'); console.error('I am a naughty console error message'); }
▍Después
// tslint.json { "rules": { ....... "no-console": [ true, "log", // console.log "warn" // console.warn ] } } // ..component.ts public ngOnInit (): void { console.log('I am a naughty console log message'); console.warn('I am a naughty console warning message'); console.error('I am a naughty console error message'); } // . console.log and console.warn console.error, Calls to 'console.log' are not allowed. Calls to 'console.warn' are not allowed.
Resumen
Hoy revisamos 11 recomendaciones que esperamos sean útiles para los desarrolladores de Angular. La próxima vez, espera 11 consejos más.
Estimados lectores! ¿Utiliza el marco angular para desarrollar proyectos web?
