Quelques conseils angulaires

Assez de temps s'est écoulé depuis la sortie de l'Angular mis à jour. Actuellement, de nombreux projets ont été achevés. De la "mise en route", de nombreux développeurs sont déjà passés à l'utilisation significative de ce cadre, de ses capacités et ont appris à contourner les pièges. Chaque développeur et / ou équipe a déjà formé ses propres guides de style et meilleures pratiques, ou en utilise d'autres. Mais en même temps, vous devez souvent faire face à beaucoup de code Angular, qui n'utilise pas beaucoup des fonctionnalités de ce framework et / ou écrit dans le style d'AngularJS.


Cet article présente certaines des fonctionnalités et des fonctionnalités de l'utilisation du framework Angular, qui, selon l'opinion modeste de l'auteur, ne sont pas suffisamment couvertes dans les manuels ou ne sont pas utilisées par les développeurs. L'article traite de l'utilisation des requêtes HTTP "Intercepteurs", de l'utilisation de Route Guards pour limiter l'accès aux utilisateurs. Certaines recommandations pour l'utilisation de RxJS et la gestion de l'état de l'application sont données. Sont également présentées quelques recommandations sur la conception du code du projet, ce qui rendra probablement le code du projet plus propre et plus compréhensible. L'auteur espère que cet article sera utile non seulement aux développeurs qui commencent tout juste à se familiariser avec Angular, mais aussi aux développeurs expérimentés.


Travailler avec HTTP


La construction de toute application Web cliente se fait autour des requêtes HTTP adressées au serveur. Cette partie présente certaines des fonctionnalités du framework Angular pour travailler avec les requêtes HTTP.


Utilisation d'intercepteurs


Dans certains cas, il peut être nécessaire de modifier la demande avant qu'elle n'atteigne le serveur. Ou vous devez changer chaque réponse. À partir d'Angular 4.3, un nouveau HttpClient a été publié. Il a ajouté la possibilité d'intercepter une demande à l'aide d'intercepteurs (Oui, ils n'ont finalement été renvoyés que dans la version 4.3 !, C'était l'une des fonctionnalités manquantes les plus attendues d'AngularJs qui n'ont pas migré vers Angular). Il s'agit d'une sorte de middleware entre l'API http et la requête réelle.


Un cas d'utilisation courant peut être l'authentification. Pour obtenir une réponse du serveur, vous devez souvent ajouter une sorte de mécanisme d'authentification à la demande. Cette tâche utilisant des intercepteurs est résolue tout simplement:


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

Puisqu'une application peut avoir plusieurs intercepteurs, ils sont organisés en chaîne. Le premier élément est appelé par le cadre angulaire lui-même. Par la suite, nous sommes responsables de transmettre la demande au prochain intercepteur. Pour ce faire, nous appelons la méthode handle de l'élément suivant de la chaîne dès que nous terminons. Nous connectons l'intercepteur:


 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 {} 

Comme vous pouvez le voir, la connexion et la mise en œuvre des intercepteurs est assez simple.


Suivi des progrès


L'une des fonctionnalités de HttpClient est la possibilité de suivre la progression d'une demande. Par exemple, si vous devez télécharger un fichier volumineux, vous souhaiterez probablement signaler à l'utilisateur la progression du téléchargement. Pour obtenir des progrès, vous devez définir la propriété reportProgress de l'objet HttpRequest sur true . Un exemple de service qui implémente cette approche:


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

La méthode post renvoie un Observable qui représente la progression du téléchargement. Il ne reste plus qu'à afficher la progression du chargement dans le composant.


Acheminement Utilisation de Route Guard


Le routage vous permet de mapper les demandes d'application à des ressources spécifiques au sein de l'application. Très souvent, il est nécessaire de résoudre le problème de la limitation de la visibilité du chemin le long duquel certains composants sont situés, en fonction de certaines conditions. Dans ces cas, Angular a un mécanisme de restriction de transition. À titre d'exemple, il existe un service qui implémentera le routage guard. Supposons que dans une application, l'authentification des utilisateurs soit implémentée à l'aide de JWT. Une version simplifiée du service qui vérifie si l'utilisateur est autorisé peut être représentée comme:


 @Injectable() export class AuthService { constructor(public jwtHelper: JwtHelperService) {} public isAuthenticated(): boolean { const token = localStorage.getItem("token"); //        return !this.jwtHelper.isTokenExpired(token); } } 

Pour implémenter Route Guard, vous devez implémenter l'interface CanActivate , qui consiste en une seule fonction 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; } } 

L'implémentation AuthGuardService utilise AuthGuardService décrit ci-dessus pour vérifier l'autorisation utilisateur. La méthode canActivate renvoie une valeur booléenne qui peut être utilisée dans la condition d'activation de l'itinéraire.


Nous pouvons maintenant appliquer la Route Guard créée à n'importe quel itinéraire ou chemin. Pour ce faire, lors de la déclaration de Routes nous CanActivate notre service, qui hérite de l'interface CanActivate , dans la section canActivate :


 export const ROUTES: Routes = [ { path: "", component: HomeComponent }, { path: "profile", component: UserComponent, canActivate: [AuthGuardService] }, { path: "**", redirectTo: "" } ]; 

Dans ce cas, la route /profile a la valeur de configuration facultative canActivate . AuthGuard décrit précédemment est passé en tant qu'argument à cette propriété canActivate . Ensuite, la méthode canActivate sera appelée chaque fois que quelqu'un essaie d'accéder au chemin /profile . Si l'utilisateur est autorisé, il aura accès au chemin /profile , sinon il sera redirigé vers le chemin /login .


Vous devez savoir que canActivate vous permet toujours d'activer le composant sur ce chemin, mais ne vous permet pas d'y basculer. Si vous devez protéger l'activation et le chargement du composant, alors dans ce cas, nous pouvons utiliser canLoad . CanLoad implémentation de CanLoad peut se faire par analogie.


Cooking RxJS


Angular est construit au-dessus de RxJS. RxJS est une bibliothèque pour travailler avec des flux de données asynchrones et basés sur des événements à l'aide de séquences observables. RxJS est une implémentation JavaScript de l'API ReactiveX. Pour la plupart, les erreurs qui se produisent lors de l'utilisation de cette bibliothèque sont associées à une connaissance superficielle des bases de sa mise en œuvre.


Utiliser async au lieu de s'inscrire à des événements


Un grand nombre de développeurs qui ne sont venus que récemment à utiliser le framework Angular utilisent la fonction d' subscribe d' Observable pour recevoir et enregistrer des données dans le composant:


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

Au lieu de cela, nous pouvons nous abonner via le modèle en utilisant un canal asynchrone:


 @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"); } } 

En vous abonnant via un modèle, nous évitons les fuites de mémoire car Angular se désabonne automatiquement d' Observable lorsqu'un composant se casse. Dans ce cas, pour les demandes HTTP, l'utilisation du canal asynchrone ne présente pratiquement aucun avantage, sauf pour un - async annulera la demande si les données ne sont plus nécessaires et ne terminera pas le traitement de la demande.


De nombreuses fonctionnalités d' Observables ne Observables pas utilisées lors de l'abonnement manuel. Observables comportement Observables peut être étendu en répétant (par exemple, réessayer dans une demande http), en mettant à jour la minuterie ou en mettant en cache.


Utilisez $ pour désigner les observables


Le paragraphe suivant est lié à la conception des codes source de l'application et découle du paragraphe précédent. Afin de distinguer les variables Observable des variables simples, vous pouvez très souvent entendre le conseil d'utiliser le signe « $ » au nom d'une variable ou d'un champ. Cette astuce simple éliminera la confusion dans les variables lors de l'utilisation de l'async.


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

Quand se désinscrire (se désinscrire)


La question la plus fréquemment posée par un développeur lorsqu'il apprend brièvement à connaître Angular est quand vous devez toujours vous désinscrire, et quand ce n'est pas le cas. Pour répondre à cette question, vous devez d'abord décider quel type d' Observable est actuellement utilisé. Dans Angular, il existe 2 types d' Observable - fini et infini, certains produisent un fini, d'autres, respectivement, un nombre infini de valeurs.


Http Observable est compact et les écouteurs / écouteurs des événements DOM sont infinis Observable .


Si l'abonnement aux valeurs d'un Observable infini Observable fait manuellement (sans utiliser de canal asynchrone), alors une réponse doit être faite sans échec. Si nous souscrivons manuellement à un Observable fini, il n'est pas nécessaire de se désinscrire, RxJS s'en chargera. Dans le cas d' Observables compacts Observables nous pouvons nous désinscrire si Observable a un temps d'exécution plus long que nécessaire, par exemple, une requête HTTP multiple.


Un exemple d' Observables compacts:


 export class SomeComponent { constructor(private http: HttpClient) { } ngOnInit() { Observable.timer(1000).subscribe(...); this.http.get("http://api.com").subscribe(...); } } 

Exemple d'observables infinis


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

Ci-dessous, plus en détail, les cas dans lesquels vous devez vous désinscrire


  1. Il est nécessaire de se désinscrire du formulaire et des contrôles individuels auxquels vous avez souscrit:

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

  1. Routeur Selon la documentation, Angular devrait se désinscrire, mais cela ne se produit pas . Par conséquent, afin d'éviter d'autres problèmes, nous écrivons par nous-mêmes:

 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() { //        observables } } 

  1. Des séquences sans fin. Des exemples sont des séquences créées à l'aide d' interva() ou d'écouteurs d'événements (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 et takeWhile


Pour simplifier le travail avec des Observables infinis dans RxJS, il existe deux fonctions pratiques - takeUntil et takeWhile . Ils effectuent la même action - se désinscrire de l' Observable à la fin d'une condition, la différence ne concerne que les valeurs acceptées. takeWhile accepte un boolean et takeUntil un Subject .
Exemple 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; } } 

Dans ce cas, lorsque le drapeau alive est modifié, l' Observable se désabonne. Dans cet exemple, désabonnez-vous lorsque le composant est détruit.
Exemple 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(); } } 

Dans ce cas, pour vous désabonner d' Observable nous signalons que le subject prend la valeur suivante et la complète.


L'utilisation de ces fonctions évitera les fuites et simplifiera le travail de désinscription des données. Quelle fonction utiliser? La réponse à cette question doit être guidée par les préférences personnelles et les exigences actuelles.


Gestion des états dans les applications angulaires, @ ngrx / store


Très souvent, lors du développement d'applications complexes, nous sommes confrontés à la nécessité de stocker l'état et de répondre à ses changements. Il existe de nombreuses bibliothèques d'applications développées sur le framework ReactJs qui vous permettent de contrôler l'état de l'application et de répondre à ses changements - Flux, Redux, Redux-saga, etc. Pour les applications angulaires, il existe un conteneur d'état basé sur RxJS inspiré de Redux - @ ngrx / store. Une bonne gestion de l'état de l'application évitera au développeur de nombreux problèmes liés à l'extension de l'application.


Pourquoi Redux
Redux se positionne comme un conteneur d'état prévisible pour les applications JavaScript. Redux est inspiré par Flux et Elm.


Redux suggère de considérer l'application comme un état initial modifiable par une séquence d'actions, ce qui peut être une bonne approche pour créer des applications Web complexes.


Redux n'est associé à aucun framework spécifique, et bien qu'il ait été développé pour React, il peut être utilisé avec Angular ou jQuery.


Les principaux postulats de Redux:


  • un référentiel pour l'état complet de l'application
  • état en lecture seule
  • les modifications sont effectuées par des fonctions «pures», soumises aux exigences suivantes:
  • ne doit pas effectuer d'appels externes sur un réseau ou une base de données;
  • retourne une valeur qui ne dépend que des paramètres passés;
  • les arguments sont immuables, c'est-à-dire les fonctions ne doivent pas les modifier;
  • appeler une fonction pure avec les mêmes arguments renvoie toujours le même résultat;

Un exemple de fonction de gestion d'état:


 // 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; } } 

Le réducteur est importé dans le module principal de l'application et en utilisant la fonction StoreModule.provideStore(reducers) nous le rendons disponible pour l'injecteur angulaire:


 // 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 { } 

Ensuite, le service Store est introduit dans les composants et services nécessaires. La fonction store.select () est utilisée pour sélectionner l'état "tranche":


 // 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


Dans certains cas, il est pratique d'associer l'état de l'application à l'itinéraire actuel de l'application. Pour ces cas, le module @ ngrx / router-store existe. Pour que l'application utilise le router-store pour enregistrer l'état, connectez simplement routerReducer et ajoutez un appel à RouterStoreModule.connectRoute dans le module d'application 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 { } 

Ajoutez maintenant le RouterState à l'état principal de l'application:


 import { RouterState } from "@ngrx/router-store"; export interface AppState { ... router: RouterState; }; 

De plus, nous pouvons indiquer l'état initial de l'application lors de la déclaration du magasin:


 StoreModule.provideStore( { router: routerReducer }, { router: { path: window.location.pathname + window.location.search } } ); 

Actions prises en charge:


 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: Dans le commentaire, ils ont suggéré que ces actions ne seront pas disponibles dans la nouvelle version @ngrx, pour la nouvelle version https://github.com/ngrx/platform/blob/master/MIGRATION.md#ngrxrouter-store


L'utilisation du conteneur d'état éliminera de nombreux problèmes lors du développement d'applications complexes. Cependant, il est important de rendre la gestion de l'État aussi simple que possible. Très souvent, il faut gérer des applications dans lesquelles l'imbrication d'états est excessive, ce qui ne fait que compliquer la compréhension de l'application.


Organisation du code


Se débarrasser des expressions volumineuses lors de l' import


De nombreux développeurs sont conscients d'une situation où les expressions à l' import plutôt lourdes. Cela est particulièrement visible dans les grandes applications où il existe de nombreuses bibliothèques réutilisables.


 import { SomeService } from "../../../core/subpackage1/subpackage2/some.service"; 

Quoi d'autre est mauvais dans ce code? Si vous devez transférer notre composant dans un autre répertoire, les expressions à l' import ne seront pas valides.


Dans ce cas, l'utilisation d'alias nous permettra de nous éloigner des expressions volumineuses à l' import et de rendre notre code beaucoup plus propre. Pour préparer le projet à l'utilisation des alias, vous devez ajouter les propriétés baseUrl et path dans tsconfig.json :


 / tsconfig.json { "compilerOptions": { ... "baseUrl": "src", "paths": { "@app/*": ["app/*"], "@env/*": ["environments/*"] } } } 

Avec ces changements, il est assez facile de gérer les plug-ins:


 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 ) { } } 

Dans cet exemple, SomeService importé directement de @app/core au lieu d'une expression volumineuse (par exemple @app/core/some-package/some.service ). Ceci est possible grâce à la réexportation des composants publics dans le fichier principal index.ts . Il est conseillé de créer un fichier index.ts pour chaque package dans lequel vous devez réexporter tous les modules publics:


 // index.ts export * from "./core.module"; export * from "./auth/auth.service"; export * from "./user/user.service"; export * from "./some-service/some.service"; 

Modules de base, partagés et de fonctionnalités


Pour une gestion plus souple des composants applicatifs, il est assez souvent recommandé dans la littérature et les différentes ressources Internet de diffuser la visibilité de ses composants. Dans ce cas, la gestion des composants de l'application est simplifiée. La séparation suivante est la plus couramment utilisée: modules Core, Shared et Feature.


Coremodule


L'objectif principal de CoreModule est de décrire les services qui auront une instance pour l'ensemble de l'application (c'est-à-dire implémenter le modèle singleton). Il s'agit souvent d'un service d'autorisation ou d'un service d'obtention d'informations utilisateur. Exemple 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 { /*   CoreModule    NgModule the AppModule */ constructor( @Optional() @SkipSelf() parentModule: CoreModule ) { if (parentModule) { throw new Error("CoreModule is already loaded. Import only in AppModule"); } } } 

Module partagé


Ce module décrit des composants simples. Ces composants n'importent pas ou n'injectent pas de dépendances d'autres modules dans leurs constructeurs. Ils doivent recevoir toutes les données via les attributs du modèle de composant. SharedModule ne dépend pas du reste de notre application. C'est également un endroit idéal pour importer et réexporter des composants de matériaux angulaires ou d'autres bibliothèques d'interface utilisateur.


 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: [ /*  Angular Material*/ CommonModule, FormsModule, MdButtonModule, /*   */ SomeCustomComponent ] }) export class SharedModule { } 

Module de fonctionnalités


Ici, vous pouvez répéter le guide de style angulaire. Un FeatureModule distinct est créé pour chaque fonction d'application indépendante. FeatureModule doit importer des services uniquement à partir de CoreModule . Si un module devait importer un service à partir d'un autre module, il est possible que ce service soit déplacé vers CoreModule .


Dans certains cas, il est nécessaire d'utiliser le service uniquement par certains modules et il n'est pas nécessaire de l'exporter vers CoreModule . Dans ce cas, vous pouvez créer un SharedModule spécial, qui ne sera utilisé que dans ces modules.
, — , - , , CoreModule , SharedModule .


, . , . , , .


Les références


  1. https://github.com/ngrx/store
  2. http://stepansuvorov.com/blog/2017/06/angular-rxjs-unsubscribe-or-not-unsubscribe/
  3. https://medium.com/@tomastrajan/6-best-practices-pro-tips-for-angular-cli-better-developer-experience-7b328bc9db81
  4. https://habr.com/post/336280/
  5. https://angular.io/docs

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


All Articles