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.
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.
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.
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 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.
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.
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.
$
to denote observablesThe 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(); } }
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
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(); } }
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 } }
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(); } }
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.
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 }); } }
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.
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";
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.
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"); } } }
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 { }
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 CoreModule
and 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.
Source: https://habr.com/ru/post/425959/
All Articles