
Bisher ist keine einzige große
SPA- Anwendung ohne
Statusverwaltung vollständig. In diesem Bereich gibt es verschiedene Lösungen für
Angular . Das beliebteste davon ist
NgRx . Es implementiert ein
Redux- Muster unter Verwendung der
RxJs- Bibliothek und verfügt über gute Werkzeuge.
In diesem Artikel werden wir kurz auf die wichtigsten
NgRx- Module
eingehen und uns detaillierter auf die
Angular-Ngrx-Datenbibliothek konzentrieren , mit der Sie in fünf Minuten eine vollständige
CRUD mit
Statusverwaltung erstellen können.
NgRx Bewertung
Weitere
Informationen zu
NgRx finden Sie in den folgenden Artikeln:
-
Reaktive Anwendungen auf Angular / NGRX. Teil 1. Einführung-
Reaktive Anwendungen auf Angular / NGRX. Teil 2. Speichern-
Reaktive Anwendungen auf Angular / NGRX. Teil 3. EffekteBetrachten Sie kurz die Hauptmodule von
NgRx , seine Vor- und Nachteile.
NgRx / store - implementiert ein Redux-Muster.
Einfache Store-Implementierungcounter.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; } }
.
Verbindung zum Modul
import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; import { counterReducer } from './counter'; @NgModule({ imports: [StoreModule.forRoot({ count: counterReducer })], }) export class AppModule {}
Verwendung in Komponenten
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 - Ermöglicht das Verfolgen von Änderungen in der Anwendung über
redux-devtools .
Verbindungsbeispiel import { StoreDevtoolsModule } from '@ngrx/store-devtools'; @NgModule({ imports: [ StoreModule.forRoot(reducers), // StoreModule StoreDevtoolsModule.instrument({ maxAge: 25, // 25 }), ], }) export class AppModule {}
NgRx / Effects - Ermöglicht das Hinzufügen von Daten, die in die Anwendung
eingehen , zum Repository, z. B. http-Anforderungen.
Beispiel./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) {} }
Anschließen des Effekts an das Modul
import { EffectsModule } from '@ngrx/effects'; import { AuthEffects } from './effects/auth.effects'; @NgModule({ imports: [EffectsModule.forRoot([AuthEffects])], }) export class AppModule {}
NgRx / entity - bietet die Möglichkeit, mit
Datenarrays zu arbeiten.
Beispieluser.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;
Reduzierer / 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] );
Was ist das Ergebnis?
Wir erhalten ein vollständiges
Staatsmanagement mit einer Reihe von Vorteilen:
- eine einzige Datenquelle für die Anwendung,
- Der Status wird getrennt von der Anwendung gespeichert.
- ein einziger Schreibstil für alle Entwickler im Projekt,
-
changeDetectionStrategy.OnPush in allen Komponenten der Anwendung,
- bequemes Debuggen durch
Redux-Devtools ,
- einfache Prüfung, as
Reduzierstücke sind reine Funktionen.
Es gibt aber auch Nachteile:- eine große Anzahl von scheinbar unverständlichen Modulen,
- viel von der gleichen Art von Code, die Sie nicht ohne Traurigkeit betrachten werden,
- Schwierigkeiten beim Mastering aufgrund all der oben genannten.
CRUD
In der Regel wird ein wesentlicher Teil der Anwendung mit der Arbeit mit Objekten (Erstellen, Lesen, Aktualisieren, Löschen) belegt. Daher wurde zur Vereinfachung der Arbeit das
CRUD- Konzept (Erstellen, Lesen, Aktualisieren, Löschen) erfunden. Somit sind die Grundoperationen für die Arbeit mit allen Arten von Objekten standardisiert. Im Backend boomt es schon lange. Viele Bibliotheken helfen dabei, diese Funktionalität zu implementieren und Routinearbeiten zu vermeiden.
In
NgRx ist das
Entitätsmodul für
CRUD verantwortlich. Wenn Sie sich ein Beispiel für seine Implementierung ansehen, können Sie sofort erkennen, dass dies der größte und komplexeste Teil von
NgRx ist . Deshalb haben
John Papa und
Ward Bell Angular-Ngrx-Daten erstellt .
Angular-Ngrx-Daten
angle-ngrx-data ist eine
NgRx- Add-In-Bibliothek, mit der Sie mit
Datenarrays arbeiten können, ohne zusätzlichen Code schreiben zu müssen.
Neben der Erstellung eines vollwertigen Statusmanagements
erstellt sie Dienste mit
http, um mit dem Server zu interagieren.
Betrachten Sie ein Beispiel
Installation npm install --save @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools ngrx-data
Angular-ngrx-Datenmodul 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 {}
Stellen Sie eine Verbindung zur App her @NgModule({ imports: [ BrowserModule, HttpClientModule, StoreModule.forRoot({}), EffectsModule.forRoot([]), EntityStoreModule, StoreDevtoolsModule.instrument({ maxAge: 25, }), ], declarations: [ AppComponent ], providers: [], bootstrap: [AppComponent] }) export class AppModule {}
Wir haben gerade die generierte API für die Arbeit mit dem Back-End und die Integration der API in NgRx erhalten , ohne einen einzigen Effekt, Reduzierer, Aktion und Selektor zu schreiben.Lassen Sie uns genauer untersuchen, was hier passiert.
Die
Konstante defaultDataServiceConfig legt die Konfiguration für unsere API fest und stellt eine Verbindung zum
Anbietermodul her . Die
root- Eigenschaft gibt an, wohin Anforderungen gestellt werden sollen. Wenn es nicht eingestellt ist, ist die Standardeinstellung "api".
const defaultDataServiceConfig: DefaultDataServiceConfig = { root: 'crud' };
Die
EntityMetadata-Konstante definiert die Namen des
Speichers , der erstellt wird, wenn
NgrxDataModule.forRoot verbunden ist.
export const entityMetadata: EntityMetadataMap = { Hero: {}, User:{} }; ... NgrxDataModule.forRoot({ entityMetadata, pluralNames })
Der Pfad zur API besteht aus dem Basispfad (in unserem Fall "grob") und dem Namen des Geschäfts.
Um beispielsweise einen Benutzer mit einer bestimmten Nummer zu erhalten, lautet der Pfad "crud / user / {userId}".
Um eine vollständige Liste der Benutzer zu erhalten, wird standardmäßig der Buchstabe "s" - "crud / user
s " am Ende des Geschäftsnamens hinzugefügt.
Wenn Sie eine andere Route benötigen, um die vollständige Liste zu erhalten (z. B. "Helden" und nicht "Helden"), können Sie diese ändern, indem Sie
pluralNames festlegen und diese mit
NgrxDataModule.forRoot verbinden .
export const pluralNames = { Hero: 'heroes' }; ... NgrxDataModule.forRoot({ entityMetadata, pluralNames })
Verbindung in Komponente
Um eine Verbindung in der Komponente
herzustellen , müssen Sie den Konstruktor
entityServices an den Konstruktor übergeben und mit der Methode
getEntityCollectionService den Dienst des gewünschten Speichers auswählen
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'); } ... }
Um die Liste an die Komponente zu binden, reicht es aus, die Eigenschaft
entity $ vom Dienst zu übernehmen und die Daten vom Server
abzurufen ,
indem Sie die Methode
getAll () aufrufen .
ngOnInit() { this.heroes$ = this.heroesService.entities$; this.heroesService.getAll(); }
Zusätzlich zu den Basisdaten erhalten Sie außerdem:
-
geladenes $ ,
Laden von $ - Abrufen des Status des Ladens von Daten,
-
Fehler $ - Fehler, wenn der Dienst ausgeführt wird,
-
count $ - Gesamtzahl der Datensätze im Repository.
Die wichtigsten Methoden zur Interaktion mit dem Server:
-
getAll () -
Abrufen der gesamten
Datenliste ,
-
getWithQuery (Abfrage) - eine mit Abfrageparametern gefilterte Liste erhalten,
-
getByKey (id) -
Abrufen eines Datensatzes nach Kennung,
-
add (entity) - Hinzufügen einer neuen Entität mit einer Anforderung zur Sicherung,
-
Löschen (Entität) - Löschen einer Entität mit einer Anforderung zur Sicherung,
-
update (Entität) - Aktualisieren Sie die Entität mit einer Anforderung zur Sicherung.
Lokale Speichermethoden:
-
addManyToCache (Entität) - Hinzufügen eines Arrays neuer Entitäten zum Repository,
-
addOneToCache (Entität) - Hinzufügen einer neuen Entität nur zum Repository,
-
removeOneFromCache (id) - Entfernen Sie eine Entität aus dem Repository.
-
updateOneInCache (Entität) - Aktualisieren Sie die Entität im Repository.
-
upsertOneInCache (Entität) - Wenn eine Entität mit der angegebenen ID vorhanden ist, wird sie aktualisiert. Wenn nicht, wird eine neue erstellt.
- usw.
Beispiel für die Verwendung von Komponenten 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); } }
Alle
Angular-Ngrx-Datenmethoden sind in
lokale Arbeit und Interaktion mit dem Server unterteilt. Auf diese Weise können Sie die Bibliothek verwenden, wenn Sie Daten sowohl auf dem Client als auch auf dem Server bearbeiten.
Protokollierung
Für die Protokollierung müssen Sie
EntityServices in eine Komponente oder einen Dienst
einfügen und die folgenden Eigenschaften verwenden:
-
reduzierteAktionen $ - für Protokollierungsaktionen,
-
entityActionErrors $ - zum Protokollieren von Fehlern.
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); } }); } }
Umzug in das Haupt-NgRx-Repository
Wie auf
ng-conf 2018 angekündigt, werden
Angular -Ngrx-Daten in Kürze in das Haupt-
NgRx- Repository migriert.
Reduzierung der Boilerplate mit NgRx-Talkvideo - Brandon Roberts & Mike Ryan
Referenzen
Die Schöpfer von Anguar-Ngrx-Daten:- John Papa
twitter.com/John_Papa- Ward Bell
twitter.com/wardbellOffizielle Repositories:-
NgRx-
Angular-Ngrx-DatenAnwendungsbeispiel:-
mit NgRx ohne Winkel-ngrx-Daten-
mit NgRx- und Angular-Ngrx-DatenRussischsprachige Angular Community auf Telegramm