Ha pasado suficiente tiempo desde el lanzamiento de Angular actualizado. Actualmente, se han completado muchos proyectos. Desde el "comienzo", muchos desarrolladores ya se han movido al uso significativo de este marco, sus capacidades y han aprendido a sortear las trampas. Cada desarrollador y / o equipo ya ha formado sus propias guías de estilo y mejores prácticas, o utilizan otras. Pero al mismo tiempo, a menudo tiene que lidiar con una gran cantidad de código Angular, que no utiliza muchas de las características de este marco y / o está escrito al estilo de AngularJS.
Este artículo presenta algunas de las características y características del uso del marco angular, que, según la modesta opinión del autor, no están cubiertos adecuadamente en los manuales o no son utilizados por los desarrolladores. El artículo analiza el uso de solicitudes HTTP "Interceptores", el uso de Guardias de ruta para limitar el acceso a los usuarios. Se dan algunas recomendaciones para usar RxJS y administrar el estado de la aplicación. También se presentan algunas recomendaciones sobre el diseño del código del proyecto, lo que probablemente hará que el código del proyecto sea más limpio y comprensible. El autor espera que este artículo sea útil no solo para desarrolladores que recién están comenzando a familiarizarse con Angular, sino también para desarrolladores experimentados.
Trabajar con HTTP
La construcción de cualquier aplicación web cliente se realiza alrededor de las solicitudes HTTP al servidor. Esta parte discute algunas de las características del marco angular para trabajar con solicitudes HTTP.
Usando interceptores
En algunos casos, puede ser necesario modificar la solicitud antes de que llegue al servidor. O necesita cambiar cada respuesta. Comenzando con Angular 4.3, se ha lanzado un nuevo HttpClient. Agregó la capacidad de interceptar una solicitud usando interceptores (¡Sí, finalmente fueron devueltos solo en la versión 4.3! Esta fue una de las características faltantes más esperadas de AngularJs que no migró a Angular). Este es un tipo de middleware entre la http-api y la solicitud real.
Un caso de uso común puede ser la autenticación. Para obtener una respuesta del servidor, a menudo necesita agregar algún tipo de mecanismo de autenticación a la solicitud. Esta tarea utilizando interceptores se resuelve de manera bastante simple:
import { Injectable } from "@angular/core"; import { Observable } from "rxjs/Observable"; import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from @angular/common/http"; @Injectable() export class JWTInterceptor implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { req = req.clone({ setHeaders: { authorization: localStorage.getItem("token") } }); return next.handle(req); } }
Debido a que una aplicación puede tener múltiples interceptores, están organizados en una cadena. El primer elemento es llamado por el propio marco angular. Posteriormente, somos responsables de transmitir la solicitud al siguiente interceptor. Para hacer esto, llamamos al método de manejo del siguiente elemento en la cadena tan pronto como terminemos. Conectamos el interceptor:
import { BrowserModule } from "@angular/platform-browser"; import { NgModule } from "@angular/core"; import { AppComponent } from "./app.component"; import { HttpClientModule } from "@angular/common/http"; import { HTTP_INTERCEPTORS } from "@angular/common/http"; @NgModule({ declarations: [AppComponent], imports: [BrowserModule, HttpClientModule], providers: [ { provide: HTTP_INTERCEPTORS, useClass: JWTInterceptor, multi: true } ], bootstrap: [AppComponent] }) export class AppModule {}
Como puede ver, la conexión e implementación de interceptores es bastante simple.
Seguimiento del progreso
Una de las características de HttpClient
es la capacidad de rastrear el progreso de una solicitud. Por ejemplo, si necesita descargar un archivo grande, probablemente quiera informar al usuario sobre el progreso de la descarga. Para obtener progreso, debe establecer la propiedad HttpRequest
objeto HttpRequest
en true
. Un ejemplo de un servicio que implementa este enfoque:
import { Observable } from "rxjs/Observable"; import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { HttpRequest } from "@angular/common/http"; import { Subject } from "rxjs/Subject"; import { HttpEventType } from "@angular/common/http"; import { HttpResponse } from "@angular/common/http"; @Injectable() export class FileUploadService { constructor(private http: HttpClient) {} public post(url: string, file: File): Observable<number> { var subject = new Subject<number>(); const req = new HttpRequest("POST", url, file, { reportProgress: true }); this.httpClient.request(req).subscribe(event => { if (event.type === HttpEventType.UploadProgress) { const percent = Math.round((100 * event.loaded) / event.total); subject.next(percent); } else if (event instanceof HttpResponse) { subject.complete(); } }); return subject.asObservable(); } }
El método de publicación devuelve un Observable
que representa el progreso de la descarga. Todo lo que se necesita ahora es mostrar el progreso de carga en el componente.
Enrutamiento Usando Guardia de ruta
El enrutamiento le permite asignar solicitudes de aplicaciones a recursos específicos dentro de la aplicación. Muy a menudo es necesario resolver el problema de limitar la visibilidad de la ruta a lo largo de la cual se ubican ciertos componentes, dependiendo de algunas condiciones. En estos casos, Angular tiene un mecanismo de restricción de transición. Como ejemplo, hay un servicio que implementará guardia de ruta. Supongamos que en una aplicación la autenticación de usuario se implementa utilizando JWT. Una versión simplificada del servicio que verifica si el usuario está autorizado puede representarse como:
@Injectable() export class AuthService { constructor(public jwtHelper: JwtHelperService) {} public isAuthenticated(): boolean { const token = localStorage.getItem("token");
Para implementar la protección de ruta, debe implementar la interfaz CanActivate
, que consiste en una sola función canActivate
.
@Injectable() export class AuthGuardService implements CanActivate { constructor(public auth: AuthService, public router: Router) {} canActivate(): boolean { if (!this.auth.isAuthenticated()) { this.router.navigate(["login"]); return false; } return true; } }
La implementación de AuthGuardService
utiliza el AuthGuardService
descrito anteriormente para verificar la autorización del usuario. El método canActivate
devuelve un valor booleano que se puede usar en la condición de activación de ruta.
Ahora podemos aplicar el protector de ruta creado a cualquier ruta o ruta. Para hacer esto, al declarar Routes
especificamos nuestro servicio, que hereda la interfaz CanActivate
, en la sección canActivate
:
export const ROUTES: Routes = [ { path: "", component: HomeComponent }, { path: "profile", component: UserComponent, canActivate: [AuthGuardService] }, { path: "**", redirectTo: "" } ];
En este caso, la ruta /profile
tiene el valor de configuración opcional canActivate
. AuthGuard
descrito anteriormente se pasa como un argumento a esta propiedad canActivate
. A continuación, se canActivate
método canActivate
cada vez que alguien intente acceder a la ruta /profile
. Si el usuario está autorizado, obtendrá acceso a la ruta /profile
, de lo contrario será redirigido a la ruta /login
.
Debe tener en cuenta que canActivate
aún le permite activar el componente en esta ruta, pero no le permite cambiar a él. Si necesita proteger la activación y carga del componente, entonces para este caso podemos usar canLoad
. CanLoad
implementación de CanLoad
se puede hacer por analogía.
Cocinar RxJS
Angular está construido sobre RxJS. RxJS es una biblioteca para trabajar con flujos de datos asíncronos y basados en eventos utilizando secuencias observables. RxJS es una implementación de JavaScript de la API ReactiveX. En su mayor parte, los errores que ocurren al trabajar con esta biblioteca están asociados con un conocimiento superficial de los conceptos básicos de su implementación.
Usar asíncrono en lugar de suscribirse a eventos
Un gran número de desarrolladores que recientemente han venido a usar el marco Angular usan la función de subscribe
de Observable
para recibir y guardar datos en el componente:
@Component({ selector: "my-component", template: ` <span>{{localData.name}} : {{localData.value}}</span>` }) export class MyComponent { localData; constructor(http: HttpClient) { http.get("api/data").subscribe(data => { this.localData = data; }); } }
En cambio, podemos suscribirnos a través de la plantilla usando una tubería asíncrona:
@Component({ selector: "my-component", template: ` <p>{{data.name | async}} : {{data.value | async}}</p>` }) export class MyComponent { data; constructor(http: HttpClient) { this.data = http.get("api/data"); } }
Al suscribirse a través de una plantilla, evitamos pérdidas de memoria porque Angular cancela automáticamente la suscripción de Observable
cuando se rompe un componente. En este caso, para las solicitudes HTTP, el uso de la tubería asíncrona prácticamente no proporciona ningún beneficio, excepto por una cosa: la asíncrona cancelará la solicitud si los datos ya no son necesarios y no completará el procesamiento de la solicitud.
Muchas características de los Observables
no se utilizan al suscribirse manualmente. Observables
comportamiento de los Observables
se puede extender repitiendo (por ejemplo, reintentando en una solicitud http), actualización basada en temporizador o almacenamiento en caché previo.
Use $
para denotar observables
El siguiente párrafo está relacionado con el diseño de los códigos fuente de la aplicación y se deduce del párrafo anterior. Para distinguir las variables Observable
de las simples, con frecuencia puede escuchar los consejos para usar el signo " $
" en el nombre de una variable o campo. Este simple truco eliminará la confusión en las variables cuando se usa asíncrono.
import { Component } from "@angular/core"; import { Observable } from "rxjs/Rx"; import { UserClient } from "../services/user.client"; import { User } from "../services/user"; @Component({ selector: "user-list", template: ` <ul class="user_list" *ngIf="(users$ | async).length"> <li class="user" *ngFor="let user of users$ | async"> {{ user.name }} - {{ user.birth_date }} </li> </ul>` }) export class UserList { public users$: Observable<User[]>; constructor(public userClient: UserClient) {} public ngOnInit() { this.users$ = this.client.getUsers(); } }
Cuándo darse de baja (darse de baja)
La pregunta más común que tiene un desarrollador cuando conoce brevemente Angular es cuándo aún necesita darse de baja y cuándo no. Para responder a esta pregunta, primero debe decidir qué tipo de Observable
se está utilizando actualmente. En Angular hay 2 tipos de Observable
: finito e infinito, algunos producen un finito, otros, respectivamente, un número infinito de valores.
Http
Observable
es compacto, y los oyentes / oyentes de eventos DOM son Observable
infinitos.
Si la suscripción a los valores de un Observable
infinito Observable
realiza manualmente (sin utilizar una tubería asíncrona), se debe responder sin fallar. Si nos suscribimos manualmente a un Observable finito, entonces no es necesario darse de baja, RxJS se encargará de esto. En el caso de los Observables
compactos Observables
podemos cancelar la suscripción si Observable
tiene un tiempo de ejecución más largo de lo necesario, por ejemplo, una solicitud HTTP múltiple.
Un ejemplo de Observables
compactos:
export class SomeComponent { constructor(private http: HttpClient) { } ngOnInit() { Observable.timer(1000).subscribe(...); this.http.get("http://api.com").subscribe(...); } }
Ejemplo de observables infinitos
export class SomeComponent { constructor(private element : ElementRef) { } interval: Subscription; click: Subscription; ngOnInit() { this.interval = Observable.interval(1000).subscribe(...); this.click = Observable.fromEvent(this.element.nativeElement, "click").subscribe(...); } ngOnDestroy() { this.interval.unsubscribe(); this.click.unsubscribe(); } }
A continuación, en más detalle están los casos en los que necesita darse de baja
- Es necesario darse de baja del formulario y de los controles individuales a los que se ha suscrito:
export class SomeComponent { ngOnInit() { this.form = new FormGroup({...}); this.valueChangesSubs = this.form.valueChanges.subscribe(...); this.statusChangesSubs = this.form.statusChanges.subscribe(...); } ngOnDestroy() { this.valueChangesSubs.unsubscribe(); this.statusChangesSubs.unsubscribe(); } }
- Enrutador De acuerdo con la documentación, Angular debería darse de baja, sin embargo , esto no sucede . Por lo tanto, para evitar más problemas, escribimos nosotros mismos:
export class SomeComponent { constructor(private route: ActivatedRoute, private router: Router) { } ngOnInit() { this.route.params.subscribe(..); this.route.queryParams.subscribe(...); this.route.fragment.subscribe(...); this.route.data.subscribe(...); this.route.url.subscribe(..); this.router.events.subscribe(...); } ngOnDestroy() {
- Secuencias sin fin. Los ejemplos son secuencias creadas usando
interva()
u oyentes de eventos (fromEvent())
:
export class SomeComponent { constructor(private element : ElementRef) { } interval: Subscription; click: Subscription; ngOnInit() { this.intervalSubs = Observable.interval(1000).subscribe(...); this.clickSubs = Observable.fromEvent(this.element.nativeElement, "click").subscribe(...); } ngOnDestroy() { this.intervalSubs.unsubscribe(); this.clickSubs.unsubscribe(); } }
takeUntil y takeWhile
Para simplificar el trabajo con Observables
infinitos en RxJS, hay dos funciones convenientes: takeUntil
y takeWhile
. Realizan la misma acción: al darse de baja del Observable
al final de alguna condición, la diferencia es solo en los valores aceptados. takeWhile
acepta un boolean
y takeUntil
un Subject
.
Ejemplo de takeWhile
:
export class SomeComponent implements OnDestroy, OnInit { public user: User; private alive: boolean = true; public ngOnInit() { this.userService .authenticate(email, password) .takeWhile(() => this.alive) .subscribe(user => { this.user = user; }); } public ngOnDestroy() { this.alive = false; } }
En este caso, cuando se cambia la bandera alive
, el Observable
dará de baja. En este ejemplo, cancele la suscripción cuando se destruye el componente.
Ejemplo de takeUntil
:
export class SomeComponent implements OnDestroy, OnInit { public user: User; private unsubscribe: Subject<void> = new Subject(void); public ngOnInit() { this.userService.authenticate(email, password) .takeUntil(this.unsubscribe) .subscribe(user => { this.user = user; }); } public ngOnDestroy() { this.unsubscribe.next(); this.unsubscribe.complete(); } }
En este caso, para darse de baja de Observable
informamos que el subject
toma el siguiente valor y lo completa.
El uso de estas funciones evitará fugas y simplificará el trabajo al darse de baja de los datos. ¿Qué función usar? La respuesta a esta pregunta debe guiarse por las preferencias personales y los requisitos actuales.
Gestión de estado en aplicaciones angulares, @ ngrx / store
Muy a menudo, al desarrollar aplicaciones complejas, nos enfrentamos a la necesidad de almacenar el estado y responder a sus cambios. Existen muchas bibliotecas para aplicaciones desarrolladas en el marco ReactJs que le permiten controlar el estado de la aplicación y responder a sus cambios: Flux, Redux, Redux-saga, etc. Para aplicaciones angulares, hay un contenedor de estado basado en RxJS inspirado en Redux - @ ngrx / store. La gestión adecuada del estado de la aplicación salvará al desarrollador de muchos problemas con la expansión adicional de la aplicación.
Por que Redux
Redux se posiciona como un contenedor de estado predecible para aplicaciones JavaScript. Redux está inspirado en Flux y Elm.
Redux sugiere pensar en la aplicación como un estado inicial modificable por una secuencia de acciones, que puede ser un buen enfoque para crear aplicaciones web complejas.
Redux no está asociado con ningún marco específico, y aunque fue desarrollado para React, puede usarse con Angular o jQuery.
Los principales postulados de Redux:
- un repositorio para todo el estado de la aplicación
- estado de solo lectura
- los cambios se realizan mediante funciones "puras", que están sujetas a los siguientes requisitos:
- no debe hacer llamadas externas a través de una red o base de datos;
- devuelve un valor que depende solo de los parámetros pasados;
- los argumentos son inmutables, es decir las funciones no deberían cambiarlos;
- llamar a una función pura con los mismos argumentos siempre devuelve el mismo resultado;
Un ejemplo de una función de gestión del estado:
// counter.ts import { ActionReducer, Action } from "@ngrx/store"; export const INCREMENT = "INCREMENT"; export const DECREMENT = "DECREMENT"; export const RESET = "RESET"; export function counterReducer(state: number = 0, action: Action) { switch (action.type) { case INCREMENT: return state + 1; case DECREMENT: return state - 1; case RESET: return 0; default: return state; } }
El reductor se importa en el módulo principal de la aplicación y, mediante la función StoreModule.provideStore(reducers)
, lo ponemos a disposición del inyector angular:
// app.module.ts import { NgModule } from "@angular/core"; import { StoreModule } from "@ngrx/store"; import { counterReducer } from "./counter"; @NgModule({ imports: [ BrowserModule, StoreModule.provideStore({ counter: counterReducer }) ] }) export class AppModule { }
A continuación, se introduce el servicio de la Store
en los componentes y servicios necesarios. La función store.select () se utiliza para seleccionar el estado "sector":
// app.component.ts ... interface AppState { counter: number; } @Component({ selector: "my-app", template: ` <button (click)="increment()">Increment</button> <div>Current Count: {{ counter | async }}</div> <button (click)="decrement()">Decrement</button> <button (click)="reset()">Reset Counter</button>` }) class AppComponent { counter: Observable<number>; constructor(private store: Store<AppState>) { this.counter = store.select("counter"); } increment() { this.store.dispatch({ type: INCREMENT }); } decrement() { this.store.dispatch({ type: DECREMENT }); } reset() { this.store.dispatch({ type: RESET }); } }
@ ngrx / router-store
En algunos casos, es conveniente asociar el estado de la aplicación con la ruta actual de la aplicación. Para estos casos, existe el módulo @ ngrx / router-store. Para que la aplicación use el router-store
para guardar el estado, simplemente conecte routerReducer
y agregue una llamada a RouterStoreModule.connectRoute
en el módulo de aplicación principal:
import { StoreModule } from "@ngrx/store"; import { routerReducer, RouterStoreModule } from "@ngrx/router-store"; @NgModule({ imports: [ BrowserModule, StoreModule.provideStore({ router: routerReducer }), RouterStoreModule.connectRouter() ], bootstrap: [AppComponent] }) export class AppModule { }
Ahora agregue el RouterState
al estado principal de la aplicación:
import { RouterState } from "@ngrx/router-store"; export interface AppState { ... router: RouterState; };
Además, podemos indicar el estado inicial de la aplicación al declarar tienda:
StoreModule.provideStore( { router: routerReducer }, { router: { path: window.location.pathname + window.location.search } } );
Acciones admitidas:
import { go, replace, search, show, back, forward } from "@ngrx/router-store"; // store.dispatch(go(["/path", { routeParam: 1 }], { query: "string" })); // store.dispatch(replace(["/path"], { query: "string" })); // store.dispatch(show(["/path"], { query: "string" })); // store.dispatch(search({ query: "string" })); // store.dispatch(back()); // store.dispatch(forward());
UPD: El comentario sugirió que estas acciones no estarán disponibles en la nueva versión @ngrx, para la nueva versión https://github.com/ngrx/platform/blob/master/MIGRATION.md#ngrxrouter-store
El uso del contenedor de estado eliminará muchos problemas al desarrollar aplicaciones complejas. Sin embargo, es importante hacer que la gestión del estado sea lo más simple posible. Muy a menudo, uno tiene que lidiar con aplicaciones en las que hay un anidamiento excesivo de estados, lo que solo complica la comprensión de la aplicación.
Organización del código
Deshacerse de las expresiones voluminosas en la import
Muchos desarrolladores son conscientes de una situación en la que las expresiones en la import
bastante engorrosas. Esto es especialmente notable en aplicaciones grandes donde hay muchas bibliotecas reutilizables.
import { SomeService } from "../../../core/subpackage1/subpackage2/some.service";
¿Qué más hay de malo en este código? En caso de que necesite transferir nuestro componente a otro directorio, las expresiones en import
no serán válidas.
En este caso, el uso de alias nos permitirá alejarnos de las expresiones voluminosas en la import
y hacer que nuestro código sea mucho más limpio. Para preparar el proyecto para usar alias, debe agregar las propiedades baseUrl y path en tsconfig.json
:
/ tsconfig.json { "compilerOptions": { ... "baseUrl": "src", "paths": { "@app/*": ["app/*"], "@env/*": ["environments/*"] } } }
Con estos cambios, es bastante fácil administrar complementos:
import { Component, OnInit } from "@angular/core"; import { Observable } from "rxjs/Observable"; import { SomeService } from "@app/core"; import { environment } from "@env/environment"; import { LocalService } from "./local.service"; @Component({ }) export class ExampleComponent implements OnInit { constructor( private someService: SomeService, private localService: LocalService ) { } }
En este ejemplo, SomeService
importa directamente desde @app/core
lugar de una expresión voluminosa (por ejemplo, @app/core/some-package/some.service
). Esto es posible gracias a la reexportación de componentes públicos en el archivo index.ts
principal. Es recomendable crear un archivo index.ts
para cada paquete en el que necesite reexportar todos los módulos públicos:
// index.ts export * from "./core.module"; export * from "./auth/auth.service"; export * from "./user/user.service"; export * from "./some-service/some.service";
Módulos principales, compartidos y de funciones
Para una gestión más flexible de los componentes de la aplicación, a menudo se recomienda en la literatura y varios recursos de Internet para difundir la visibilidad de sus componentes. En este caso, la administración de los componentes de la aplicación se simplifica. La siguiente separación se usa más comúnmente: módulos principales, compartidos y de funciones.
Coremodule
El objetivo principal de CoreModule es describir los servicios que tendrán una instancia para toda la aplicación (es decir, implementar el patrón singleton). Estos a menudo incluyen un servicio de autorización o un servicio para obtener información del usuario. Ejemplo de CoreModule:
import { NgModule, Optional, SkipSelf } from "@angular/core"; import { CommonModule } from "@angular/common"; import { HttpClientModule } from "@angular/common/http"; import { SomeSingletonService } from "./some-singleton/some-singleton.service"; @NgModule({ imports: [CommonModule, HttpClientModule], declarations: [], providers: [SomeSingletonService] }) export class CoreModule { constructor( @Optional() @SkipSelf() parentModule: CoreModule ) { if (parentModule) { throw new Error("CoreModule is already loaded. Import only in AppModule"); } } }
Módulo compartido
Este módulo describe componentes simples. Estos componentes no importan ni inyectan dependencias de otros módulos en sus constructores. Deben recibir todos los datos a través de los atributos en la plantilla del componente. SharedModule
no depende del resto de nuestra aplicación, también es un lugar ideal para importar y reexportar componentes de material angular u otras bibliotecas de interfaz de usuario.
import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { FormsModule } from "@angular/forms"; import { MdButtonModule } from "@angular/material"; import { SomeCustomComponent } from "./some-custom/some-custom.component"; @NgModule({ imports: [CommonModule, FormsModule, MdButtonModule], declarations: [SomeCustomComponent], exports: [ CommonModule, FormsModule, MdButtonModule, SomeCustomComponent ] }) export class SharedModule { }
Módulo de funciones
Aquí puede repetir la guía de estilo angular. Se crea un FeatureModule separado para cada función de aplicación independiente. FeatureModule debe importar servicios solo desde CoreModule
. Si algún módulo necesitaba importar un servicio desde otro módulo, es posible que este servicio se mueva a CoreModule
.
En algunos casos, es necesario usar el servicio solo por algunos módulos y no es necesario exportarlo a CoreModule
. En este caso, puede crear un SharedModule
especial, que se usará solo en estos módulos.
, — , - , , CoreModule
, SharedModule
.
, . , . , , .
Referencias
- https://github.com/ngrx/store
- http://stepansuvorov.com/blog/2017/06/angular-rxjs-unsubscribe-or-not-unsubscribe/
- https://medium.com/@tomastrajan/6-best-practices-pro-tips-for-angular-cli-better-developer-experience-7b328bc9db81
- https://habr.com/post/336280/
- https://angular.io/docs