📜 ⬆️ ⬇️

A few tips on Angular

Enough time has passed since the release of the updated Angular. Currently, many projects have been completed. From "getting started" a lot of developers have already switched to meaningful use of this framework, its capabilities, have learned to bypass the pitfalls. Each developer and / or team either have already formed their own style guides and best practice or use other people’s. But at the same time, one often comes across a large amount of code on Angular, which does not use many features of this framework and / or written in the style of AngularJS.


This article presents some of the features and features of using the Angular framework, which, in the modest opinion of the author, are not sufficiently covered in the manuals or are not used by the developers. The article discusses the use of "interceptors" (Interceptors) HTTP requests, the use of Route Guards to restrict access to users. Some recommendations are given on using RxJS and managing the state of the application. There are also some recommendations on the design of the project code, which will probably make the project code cleaner and clearer. The author hopes that this article will be useful not only for developers who are just starting to get acquainted with Angular, but also for experienced developers.


Work with HTTP


Building any client Web application is performed around HTTP requests to the server. This section discusses some of the capabilities of the Angular framework for working with HTTP requests.


Using Interceptors


In some cases, it may be necessary to modify the request before it reaches the server. Or you need to change each answer. Starting with version Angular 4.3, a new HttpClient has appeared. It added the ability to intercept a request using interceptors (Yes, they were finally returned only in version 4.3! This was one of the most expected missing features of AngularJs that did not migrate to Angular). This is a kind of middleware between http-api and the actual request.


One common use case might be authentication. To get a response from the server, you often need to add some kind of authentication mechanism to the request. This task with the use of interceptors is solved quite simply:


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

Since an application can have multiple interceptors, they are chained. The first element is invoked by the Angular framework itself. Subsequently, we are responsible for transmitting the request to the next interceptor. To do this, we call the handle method of the next element in the chain as soon as we finish. We connect 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 {} 

As you can see the connection and implementation of interceptors is quite simple.


Progress tracking


One of the features of HttpClient is the ability to track the progress of the request. For example, if you need to upload a large file, then you probably want to report the progress of the download to the user. To get progress, you must set the reportProgress property of the HttpRequest object to true . An example of a service that implements this approach:


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

The post method returns an Observable object representing the loading progress. All that is needed now is to display the download progress in the component.


Routing Use Route Guard


Routing allows you to match requests to the application with specific resources within the application. Quite often, it is necessary to solve the problem of limiting the visibility of the way in which certain components are located, depending on certain conditions. In these cases, Angular has a transition limiting mechanism. As an example, there is a service that will implement route guard. Suppose in a user authentication application is implemented using JWT. A simplified version of the service that checks whether the user is authorized, can be represented as:


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

To implement route guard, you must implement the CanActivate interface, which consists of a single canActivate function.


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

The AuthGuardService implementation uses the AuthGuardService described above to verify the user's authorization. The canActivate method returns a Boolean value that can be used in the condition of route activation.


Now we can apply the created Route Guard to any route or path. For this, when declaring Routes we specify our service, which inherits the CanActivate interface, in the canActivate section:


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

In this case, the /profile route has the additional configuration value canActivate . AuthGuard described earlier is passed as an argument to this canActivate property. Further, the canActivate method will be called each time someone tries to access the /profile path. If the user is authorized he will get access to the /profile path, otherwise he will be redirected to the /login path.


You should know that canActivate still allows you to activate a component along a given path, but does not allow you to switch to it. If you need to protect the activation and loading of the component, then for such a case we can use canLoad . Implementation of CanLoad can be done by analogy.


Cooking RxJS


Angular is based on RxJS. RxJS is a library for working with asynchronous and event-based data streams using observed sequences. RxJS is a JavaScript implementation of the ReactiveX API. For the most part, errors that arise when working with this library are connected with superficial knowledge of the basics of its implementation.


Use async instead of subscribing to events.


A large number of developers who only recently came to use the Angular framework use the subscribe function of Observable to receive and save data in a component:


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

Instead, we can subscribe through a template using the async pipe:


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

By subscribing through the template, we avoid memory leaks, because Angular automatically cancels the Observable subscription when a component collapses. In this case, for HTTP requests, the use of async pipe provides almost no advantages, except one - async will cancel the request if the data is no longer needed, and does not complete the processing of the request.


Many features of the Observables not used when subscribing manually. Observables behavior can be extended by replaying (for example, retry in an http request), timer-based update, or preliminary caching.


Use $ to denote observables


The next item is related to the design of the source code of the application and follows from the previous item. In order to distinguish Observable from simple variables, one can often hear advice to use the “ $ ” sign in the name of a variable or field. This simple trick will eliminate confusion in variables when using 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(); } } 

When to unsubscribe


The most frequent question that arises from the developer with a short acquaintance with Angular is when it’s still necessary to unsubscribe and when not. To answer this question, you first need to decide what type of Observable is currently used. In Angular, there are 2 types of Observable - finite and infinite, some produce a finite, others, respectively, an infinite number of values.


Http Observable is finite, and listeners of DOM events are infinite Observable .


If the subscription to the values ​​of the infinite Observable done manually (without using the async pipe), then a cancellation must be made. If we subscribe in manual mode to the finite Observable, then it is not necessary to unsubscribe, RxJS will take care of this. In the case of finite Observables we can unsubscribe if the Observable has a longer execution time than necessary, for example, a multiple repetitive HTTP request.


An example of finite Observables :


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

Example of infinite Observables


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

Below, in more detail the cases in which you need to unsubscribe


  1. It is necessary to unsubscribe from the form and from the individual controls to which you subscribed:

 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. Router. According to the documentation, Angular should unsubscribe itself, but this does not happen . Therefore, in order to avoid further problems, we unsubscribe ourselves:

 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. Endless sequences. Examples include sequences created with interva() or event listeners (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 and takeWhile


To simplify the work with the infinite Observables in RxJS, there are two convenient functions - it is takeUntil and takeWhile . They produce the same action — a reply from the Observable at the end of a condition; the difference is only in the values ​​accepted. takeWhile takes a boolean , and takeUntil a Subject .
Example 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; } } 

In this case, if the alive flag is changed, an unsubscribe from the Observable occurs. In this example, unsubscribe when you destroy a component.
Example 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(); } } 

In this case, to unsubscribe from Observable we report that subject accepts the following value and terminates it.


The use of these functions will avoid leaks and simplify work with replies from data. Which function to use? The answer to this question should be guided by personal preferences and current requirements.


State management in Angular applications, @ ngrx / store


Quite often, when developing complex applications, we are faced with the need to maintain state and respond to changes in it. For applications developed on the ReactJs framework, there are many libraries that allow you to manage the state of the application and respond to changes in it - Flux, Redux, Redux-saga, etc. For Angular applications, there is an RxJS based state container inspired by Redux - @ ngrx / store. Proper management of the state of the application will save the developer from many problems with the further expansion of the application.


Why redux?
Redux positions itself as a predictable state container for JavaScript applications. Redux is inspired by Flux and Elm.


Redux suggests thinking of an application as the initial state of a modifiable sequence of actions (actions), which can be a good approach when building complex web applications.


Redux is not related to any particular framework, and although developed for React, it can be used with Angular or jQuery.


The main postulates of Redux:



An example of a state management function:


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

In the main application module, the Reducer is imported and using the StoreModule.provideStore(reducers) function, we make it available for the Angular Injector:


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

Next is the implementation of the Store service in the necessary components and services. To select the "slice" of the state, use the store.select () function:


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


In some cases it is convenient to associate the state of the application with the current route of the application. For these cases, there is a module @ ngrx / router-store. In order for the application to use the router-store to save the state, just connect the routerReducer and add the RouterStoreModule.connectRoute call in the main application module:


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

Now we add RouterState to the main state of the application:


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

Additionally, we can specify the initial state of the application when declaring store


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

Supported actions:


 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: The comment suggested that the action data will not be available in the new version of @ngrx, for the new version https://github.com/ngrx/platform/blob/master/MIGRATION.md#ngrxrouter-store


Using a state container will eliminate many problems when developing complex applications. However, it is important to make state management as simple as possible. Quite often one has to deal with applications in which there is an excessive nesting of states, which only complicates the understanding of the application operation.


Code organization


We get rid of bulky expressions in import


Many developers know the situation when expressions in import rather cumbersome. This is especially noticeable in large applications, where many reusable libraries.


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

What else is bad in this code? In the case when you need to transfer our component to another directory, the expressions in the import will not be valid.


In this case, the use of pseudonyms will make it possible to get away from cumbersome expressions in import and make our code much cleaner. In order to prepare a project for using aliases, you need to add the baseUrl and path properties in tsconfig.json :


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

With these changes, it’s easy to manage the plugins:


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

In this example, SomeService imported directly from @app/core instead of a cumbersome expression (for example @app/core/some-package/some.service ). This is possible due to the re-export of public components in the main index.ts file. It is advisable to create an index.ts file for each package in which you need to re-export all public modules:


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

Core, Shared and Feature modules


For more flexible management of component parts of an application, it is often recommended in the literature and various Internet resources to distribute the visibility of its components. In this case, managing the components of the application is simplified. The following divisions are most commonly used: Core, Shared, and Feature modules.


Coremodule


The main purpose of CoreModule is a description of services that will have one instance for the entire application (that is, they implement the singleton pattern). These often include an authorization service or a service for obtaining information about a user. CoreModule example:


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

SharedModule


This module describes simple components. These components do not import or embed dependencies from other modules into their constructors. They should receive all the data through the attributes in the component template. SharedModule has no dependency on the rest of our application. It is also an ideal place to import and re-export Angular Material components or other UI libraries.


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

FeatureModule


Here you can repeat the Angular style guide. For each independent function of the application, a separate FeatureModule is created. FeatureModule should import services only from CoreModule . If some module needed to import a service from another module, it is possible that this service needs to be rendered in CoreModule .


In some cases, there is a need to use the service only for some modules and there is no need to put it into CoreModule . In this case, you can create a special SharedModule that will be used only in these modules.
The basic rule used when creating modules is to try to create modules that do not depend on any other modules, but only on the services provided CoreModuleand the components provided SharedModule.


This will allow the code of the developed applications to be cleaner, easier to maintain and extend. It also reduces the effort required for refactoring. If you follow this rule, you can be sure that changes to one module will not affect or destroy the rest of our application.


Bibliography


  1. https://github.com/ngrx/store
  2. http://stepansuvorov.com/blog/2017/06/angular-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/425959/


All Articles