
À 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. EffetsExaminez 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 magasincounter.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.
Exempleuser.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/wardbellDépôts officiels:-
NgRx-
angular-ngrx-dataExemple d'application:-
avec NgRx sans angular-ngrx-data-
avec NgRx et angular-ngrx-dataCommunauté angulaire russophone sur Telegram