angular-ngrx-data - gestion de l'état et CRUD en cinq minutes

image
À ce jour, pas une seule grande application SPA n'est complète sans gestion d'état . Il existe plusieurs solutions pour Angular dans ce domaine. Le plus populaire d'entre eux est NgRx . Il implémente un modèle Redux à l'aide de la bibliothèque RxJs et dispose de bons outils.

Dans cet article, nous allons brièvement parcourir les principaux modules NgRx et nous concentrer plus en détail sur la bibliothèque angular-ngrx-data , qui vous permet de créer un CRUD complet avec gestion d'état en cinq minutes.

Examen NgRx


Vous pouvez en savoir plus sur NgRx dans les articles suivants:

- Applications réactives sur Angular / NGRX. Partie 1. Introduction
- Applications réactives sur Angular / NGRX. Partie 2. Magasin
- Applications réactives sur Angular / NGRX. Partie 3. Effets

Examinez brièvement les principaux modules de NgRx , ses avantages et ses inconvénients.

NgRx / store - implémente un modèle Redux.

Implémentation simple en magasin
counter.actions.ts
export const INCREMENT = 'INCREMENT'; export const DECREMENT = 'DECREMENT'; export const RESET = 'RESET'; 

counter.reducer.ts

 import { Action } from '@ngrx/store'; const initialState = 0; export function counterReducer(state: number = initialState, action: Action) { switch (action.type) { case INCREMENT: return state + 1; case DECREMENT: return state - 1; case RESET: return 0; default: return state; } } 
.
Connexion au module
 import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; import { counterReducer } from './counter'; @NgModule({ imports: [StoreModule.forRoot({ count: counterReducer })], }) export class AppModule {} 

Utilisation dans le composant
 import { Component } from '@angular/core'; import { Store, select } from '@ngrx/store'; import { Observable } from 'rxjs'; import { INCREMENT, DECREMENT, RESET } from './counter'; interface AppState { count: number; } @Component({ selector: 'app-my-counter', template: ` <button (click)="increment()">Increment</button> <div>Current Count: {{ count$ | async }}</div> <button (click)="decrement()">Decrement</button> <button (click)="reset()">Reset Counter</button> `, }) export class MyCounterComponent { count$: Observable<number>; constructor(private store: Store<AppState>) { this.count$ = store.pipe(select('count')); } increment() { this.store.dispatch({ type: INCREMENT }); } decrement() { this.store.dispatch({ type: DECREMENT }); } reset() { this.store.dispatch({ type: RESET }); } } 


NgRx / store-devtools - vous permet de suivre les changements dans l'application via redux-devtools .

Exemple de connexion
 import { StoreDevtoolsModule } from '@ngrx/store-devtools'; @NgModule({ imports: [ StoreModule.forRoot(reducers), //      StoreModule StoreDevtoolsModule.instrument({ maxAge: 25, //   25  }), ], }) export class AppModule {} 


NgRx / effects - vous permet d'ajouter des données entrant dans l'application au référentiel, telles que les requêtes http.

Exemple
./effects/auth.effects.ts
 import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Action } from '@ngrx/store'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { Observable, of } from 'rxjs'; import { catchError, map, mergeMap } from 'rxjs/operators'; @Injectable() export class AuthEffects { // Listen for the 'LOGIN' action @Effect() login$: Observable<Action> = this.actions$.pipe( ofType('LOGIN'), mergeMap(action => this.http.post('/auth', action.payload).pipe( // If successful, dispatch success action with result map(data => ({ type: 'LOGIN_SUCCESS', payload: data })), // If request fails, dispatch failed action catchError(() => of({ type: 'LOGIN_FAILED' })) ) ) ); constructor(private http: HttpClient, private actions$: Actions) {} } 

Connexion de l'effet au module

 import { EffectsModule } from '@ngrx/effects'; import { AuthEffects } from './effects/auth.effects'; @NgModule({ imports: [EffectsModule.forRoot([AuthEffects])], }) export class AppModule {} 


NgRx / entity - offre la possibilité de travailler avec des tableaux de données.

Exemple
user.model.ts

 export interface User { id: string; name: string; } 

user.actions.ts

 import { Action } from '@ngrx/store'; import { Update } from '@ngrx/entity'; import { User } from './user.model'; export enum UserActionTypes { LOAD_USERS = '[User] Load Users', ADD_USER = '[User] Add User', UPSERT_USER = '[User] Upsert User', ADD_USERS = '[User] Add Users', UPSERT_USERS = '[User] Upsert Users', UPDATE_USER = '[User] Update User', UPDATE_USERS = '[User] Update Users', DELETE_USER = '[User] Delete User', DELETE_USERS = '[User] Delete Users', CLEAR_USERS = '[User] Clear Users', } export class LoadUsers implements Action { readonly type = UserActionTypes.LOAD_USERS; constructor(public payload: { users: User[] }) {} } export class AddUser implements Action { readonly type = UserActionTypes.ADD_USER; constructor(public payload: { user: User }) {} } export class UpsertUser implements Action { readonly type = UserActionTypes.UPSERT_USER; constructor(public payload: { user: User }) {} } export class AddUsers implements Action { readonly type = UserActionTypes.ADD_USERS; constructor(public payload: { users: User[] }) {} } export class UpsertUsers implements Action { readonly type = UserActionTypes.UPSERT_USERS; constructor(public payload: { users: User[] }) {} } export class UpdateUser implements Action { readonly type = UserActionTypes.UPDATE_USER; constructor(public payload: { user: Update<User> }) {} } export class UpdateUsers implements Action { readonly type = UserActionTypes.UPDATE_USERS; constructor(public payload: { users: Update<User>[] }) {} } export class DeleteUser implements Action { readonly type = UserActionTypes.DELETE_USER; constructor(public payload: { id: string }) {} } export class DeleteUsers implements Action { readonly type = UserActionTypes.DELETE_USERS; constructor(public payload: { ids: string[] }) {} } export class ClearUsers implements Action { readonly type = UserActionTypes.CLEAR_USERS; } export type UserActionsUnion = | LoadUsers | AddUser | UpsertUser | AddUsers | UpsertUsers | UpdateUser | UpdateUsers | DeleteUser | DeleteUsers | ClearUsers; 

user.reducer.ts
 import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; import { User } from './user.model'; import { UserActionsUnion, UserActionTypes } from './user.actions'; export interface State extends EntityState<User> { // additional entities state properties selectedUserId: number | null; } export const adapter: EntityAdapter<User> = createEntityAdapter<User>(); export const initialState: State = adapter.getInitialState({ // additional entity state properties selectedUserId: null, }); export function reducer(state = initialState, action: UserActionsUnion): State { switch (action.type) { case UserActionTypes.ADD_USER: { return adapter.addOne(action.payload.user, state); } case UserActionTypes.UPSERT_USER: { return adapter.upsertOne(action.payload.user, state); } case UserActionTypes.ADD_USERS: { return adapter.addMany(action.payload.users, state); } case UserActionTypes.UPSERT_USERS: { return adapter.upsertMany(action.payload.users, state); } case UserActionTypes.UPDATE_USER: { return adapter.updateOne(action.payload.user, state); } case UserActionTypes.UPDATE_USERS: { return adapter.updateMany(action.payload.users, state); } case UserActionTypes.DELETE_USER: { return adapter.removeOne(action.payload.id, state); } case UserActionTypes.DELETE_USERS: { return adapter.removeMany(action.payload.ids, state); } case UserActionTypes.LOAD_USERS: { return adapter.addAll(action.payload.users, state); } case UserActionTypes.CLEAR_USERS: { return adapter.removeAll({ ...state, selectedUserId: null }); } default: { return state; } } } export const getSelectedUserId = (state: State) => state.selectedUserId; // get the selectors const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); // select the array of user ids export const selectUserIds = selectIds; // select the dictionary of user entities export const selectUserEntities = selectEntities; // select the array of users export const selectAllUsers = selectAll; // select the total user count export const selectUserTotal = selectTotal; 

réducteurs / index.ts

 import { createSelector, createFeatureSelector, ActionReducerMap, } from '@ngrx/store'; import * as fromUser from './user.reducer'; export interface State { users: fromUser.State; } export const reducers: ActionReducerMap<State> = { users: fromUser.reducer, }; export const selectUserState = createFeatureSelector<fromUser.State>('users'); export const selectUserIds = createSelector( selectUserState, fromUser.selectUserIds ); export const selectUserEntities = createSelector( selectUserState, fromUser.selectUserEntities ); export const selectAllUsers = createSelector( selectUserState, fromUser.selectAllUsers ); export const selectUserTotal = createSelector( selectUserState, fromUser.selectUserTotal ); export const selectCurrentUserId = createSelector( selectUserState, fromUser.getSelectedUserId ); export const selectCurrentUser = createSelector( selectUserEntities, selectCurrentUserId, (userEntities, userId) => userEntities[userId] ); 


Quel est le résultat?


Nous obtenons une gestion complète de l' État avec de nombreux avantages:

- une seule source de données pour l'application,
- l'état est stocké séparément de l'application,
- un style d'écriture unique pour tous les développeurs du projet,
- changeDetectionStrategy.OnPush dans tous les composants de l'application,
- débogage pratique via redux-devtools ,
- facilité de test, comme les réducteurs sont de simples fonctions.

Mais il y a aussi des inconvénients:

- un grand nombre de modules apparemment incompréhensibles,
- beaucoup du même type de code que vous ne regarderez pas sans tristesse,
- difficulté à maîtriser en raison de tout ce qui précède.

CRUD


En règle générale, une partie importante de l'application est occupée par le travail avec des objets (création, lecture, mise à jour, suppression), par conséquent, pour la commodité du travail, le concept CRUD (Créer, Lire, Mettre à jour, Supprimer) a été inventé. Ainsi, les opérations de base pour travailler avec tous les types d'objets sont standardisées. Il est en plein essor depuis longtemps sur le backend. De nombreuses bibliothèques aident à implémenter cette fonctionnalité et à se débarrasser du travail de routine.

Dans NgRx , le module d' entité est responsable de CRUD , et si vous regardez un exemple de sa mise en œuvre, vous pouvez immédiatement voir qu'il s'agit de la partie la plus grande et la plus complexe de NgRx . C'est pourquoi John Papa et Ward Bell ont créé des données angular-ngrx .

angular-ngrx-data


angular-ngrx-data est une bibliothèque de compléments NgRx qui vous permet de travailler avec des tableaux de données sans écrire de code supplémentaire.
En plus de créer une gestion d'état à part entière, elle entreprend la création de services avec http pour interagir avec le serveur.

Prenons un exemple


L'installation

 npm install --save @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools ngrx-data 

Module de données angulaire-ngrx

 import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { EntityMetadataMap, NgrxDataModule, DefaultDataServiceConfig } from 'ngrx-data'; const defaultDataServiceConfig: DefaultDataServiceConfig = { root: 'crud' }; export const entityMetadata: EntityMetadataMap = { Hero: {}, User:{} }; export const pluralNames = { Hero: 'heroes' }; @NgModule({ imports: [ CommonModule, NgrxDataModule.forRoot({ entityMetadata, pluralNames }) ], declarations: [], providers: [ { provide: DefaultDataServiceConfig, useValue: defaultDataServiceConfig } ] }) export class EntityStoreModule {} 

Connectez-vous à l'application

 @NgModule({ imports: [ BrowserModule, HttpClientModule, StoreModule.forRoot({}), EffectsModule.forRoot([]), EntityStoreModule, StoreDevtoolsModule.instrument({ maxAge: 25, }), ], declarations: [ AppComponent ], providers: [], bootstrap: [AppComponent] }) export class AppModule {} 

Nous venons de recevoir l' API générée pour travailler avec le back-end et l'intégration de l' API avec NgRx , sans écrire un seul effet, réducteur et action et sélecteur.

Examinons plus en détail ce qui se passe ici.


La constante defaultDataServiceConfig définit la configuration de notre API et se connecte au module des fournisseurs . La propriété racine indique où aller pour les demandes. S'il n'est pas défini, la valeur par défaut sera "api".

 const defaultDataServiceConfig: DefaultDataServiceConfig = { root: 'crud' }; 

La constante entityMetadata définit les noms du magasin qui sera créé lorsque NgrxDataModule.forRoot est connecté.

 export const entityMetadata: EntityMetadataMap = { Hero: {}, User:{} }; ... NgrxDataModule.forRoot({ entityMetadata, pluralNames }) 

Le chemin vers l'API se compose du chemin de base (dans notre cas «crud») et du nom du magasin.
Par exemple, pour obtenir un utilisateur avec un certain nombre, le chemin serait "crud / user / {userId}".

Pour obtenir une liste complète des utilisateurs, la lettre «s» - «crud / user s » est ajoutée par défaut à la fin du nom du magasin.

Si vous avez besoin d'un itinéraire différent pour obtenir la liste complète (par exemple, "héros" et non "héros"), vous pouvez le changer en définissant pluralNames et en les connectant à NgrxDataModule.forRoot .

 export const pluralNames = { Hero: 'heroes' }; ... NgrxDataModule.forRoot({ entityMetadata, pluralNames }) 

Connexion dans le composant


Pour vous connecter au composant, vous devez passer le constructeur entityServices au constructeur et utiliser la méthode getEntityCollectionService pour sélectionner le service du stockage souhaité

 import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { Observable } from 'rxjs'; import { Hero } from '@appModels/hero'; import { EntityServices, EntityCollectionService } from 'ngrx-data'; @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.css'], changeDetection: ChangeDetectionStrategy.OnPush }) export class HeroesComponent implements OnInit { heroes$: Observable<Hero[]>; heroesService: EntityCollectionService<Hero>; constructor(entityServices: EntityServices) { this.heroesService = entityServices.getEntityCollectionService('Hero'); } ... } 

Pour lier la liste au composant, il suffit de prendre la propriété entity $ du service, et pour obtenir les données du serveur, appelez la méthode getAll () .

 ngOnInit() { this.heroes$ = this.heroesService.entities$; this.heroesService.getAll(); } 

De plus, en plus des données de base, vous pouvez obtenir:

- chargé $ , chargement $ - obtenir l'état de chargement des données,
- erreurs $ - erreurs lors de l'exécution du service,
- count $ - nombre total d'enregistrements dans le référentiel.

Les principales méthodes d'interaction avec le serveur:

- getAll () - obtenir la liste complète des données,
- getWithQuery (query) - obtenir une liste filtrée à l'aide des paramètres de requête,
- getByKey (id) - obtenir un enregistrement par identifiant,
- ajouter (entité) - ajouter une nouvelle entité avec une demande de support,
- supprimer (entité) - supprimer une entité avec une demande de sauvegarde,
- update (entity) - met à jour l'entité avec une demande de sauvegarde.

Méthodes de stockage local:

- addManyToCache (entité) - ajout d'un tableau de nouvelles entités au référentiel,
- addOneToCache (entité) - ajout d'une nouvelle entité uniquement au référentiel,
- removeOneFromCache (id) - supprime une entité du référentiel,
- updateOneInCache (entity) - met à jour l'entité dans le référentiel,
- upsertOneInCache (entité) - si une entité avec l'ID spécifié existe, elle est mise à jour, sinon, une nouvelle est créée,
- et autres

Exemple d'utilisation des composants

 import { EntityCollectionService, EntityServices } from 'ngrx-data'; import { Hero } from '../../core'; @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) export class HeroesComponent implements OnInit { heroes$: Observable<Hero[]>; heroesService: EntityCollectionService<Hero>; constructor(entityServices: EntityServices) { this.heroesService = entityServices.getEntityCollectionService('Hero'); } ngOnInit() { this.heroes$ = this.heroesService.entities$; this.getHeroes(); } getHeroes() { this.heroesService.getAll(); } addHero(hero: Hero) { this.heroesService.add(hero); } deleteHero(hero: Hero) { this.heroesService.delete(hero.id); } updateHero(hero: Hero) { this.heroesService.update(hero); } } 

Toutes les méthodes angular-ngrx-data sont divisées en travail local et interaction avec le serveur. Cela vous permet d'utiliser la bibliothèque lors de la manipulation de données à la fois sur le client et sur le serveur.

Journalisation


Pour la journalisation, vous devez injecter EntityServices dans un composant ou un service et utiliser les propriétés:

- réduitActions $ - pour les actions de journalisation,
- entityActionErrors $ - pour les erreurs de journalisation.

 import { Component, OnInit } from '@angular/core'; import { MessageService } from '@appServices/message.service'; import { EntityServices } from 'ngrx-data'; @Component({ selector: 'app-messages', templateUrl: './messages.component.html', styleUrls: ['./messages.component.css'] }) export class MessagesComponent implements OnInit { constructor( public messageService: MessageService, private entityServices: EntityServices ) {} ngOnInit() { this.entityServices.reducedActions$.subscribe(res => { if (res && res.type) { this.messageService.add(res.type); } }); } } 

Déplacement vers le référentiel NgRx principal


Comme annoncé lors de la conférence ng-conf 2018 , les données angular-ngrx seront bientôt migrées vers le référentiel NgRx principal.

Réduire la plaque de chaudière avec la vidéo de discussion NgRx - Brandon Roberts & Mike Ryan


Les références


Les créateurs de anguar-ngrx-data:
- John Papa twitter.com/John_Papa
- Ward Bell twitter.com/wardbell

Dépôts officiels:
- NgRx
- angular-ngrx-data

Exemple d'application:
- avec NgRx sans angular-ngrx-data
- avec NgRx et angular-ngrx-data

Communauté angulaire russophone sur Telegram

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


All Articles