angular-ngrx-data: gestión de estado y CRUD en cinco minutos

imagen
Hasta la fecha, ni una sola aplicación de SPA grande está completa sin la administración del estado . Hay varias soluciones para Angular en esta área. El más popular de estos es NgRx . Implementa un patrón Redux usando la biblioteca RxJs y tiene buenas herramientas.

En este artículo, repasaremos brevemente los módulos principales de NgRx y nos enfocaremos con más detalle en la biblioteca angular-ngrx-data , que le permite hacer un CRUD completo con administración de estado en cinco minutos.

Revisión de NgRx


Puede leer más sobre NgRx en los siguientes artículos:

- Aplicaciones reactivas en Angular / NGRX. Parte 1. Introducción
- Aplicaciones reactivas en Angular / NGRX. Parte 2. Almacenar
- Aplicaciones reactivas en Angular / NGRX. Parte 3. Efectos

Considere brevemente los módulos principales de NgRx , sus ventajas y desventajas.

NgRx / store : implementa un patrón Redux.

Implementación simple de la tienda
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; } } 
.
Conexión al módulo.
 import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; import { counterReducer } from './counter'; @NgModule({ imports: [StoreModule.forRoot({ count: counterReducer })], }) export class AppModule {} 

Uso en componente
 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 : le permite realizar un seguimiento de los cambios en la aplicación a través de redux-devtools .

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


NgRx / effects : le permite agregar datos que ingresan en la aplicación al repositorio, como las solicitudes http.

Ejemplo
./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) {} } 

Conectando el efecto al módulo

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


NgRx / entity : proporciona la capacidad de trabajar con matrices de datos.

Ejemplo
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; 

reductores / 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] ); 


Cual es el resultado?


Obtenemos una gestión estatal completa con muchas ventajas:

- una única fuente de datos para la aplicación,
- el estado se almacena por separado de la aplicación,
- un estilo de escritura único para todos los desarrolladores del proyecto,
- changeDetectionStrategy.OnPush en todos los componentes de la aplicación,
- depuración conveniente a través de redux-devtools ,
- facilidad de prueba, como Los reductores son funciones puras.

Pero también hay desventajas:

- una gran cantidad de módulos aparentemente incomprensibles,
- mucho del mismo tipo de código que no mirarás sin tristeza,
- Dificultad para dominar debido a todo lo anterior.

CRUDO


Como regla general, una parte importante de la aplicación está ocupada trabajando con objetos (creación, lectura, actualización, eliminación), por lo tanto, para la comodidad del trabajo, se creó el concepto CRUD (Crear, Leer, Actualizar, Eliminar). Por lo tanto, las operaciones básicas para trabajar con todo tipo de objetos están estandarizadas. Ha estado en auge durante mucho tiempo en el backend. Muchas bibliotecas ayudan a implementar esta funcionalidad y deshacerse del trabajo de rutina.

En NgRx , el módulo de entidad es responsable de CRUD , y si observa un ejemplo de su implementación, puede ver de inmediato que esta es la parte más grande y compleja de NgRx . Es por eso que John Papa y Ward Bell crearon angular-ngrx-data .

angular-ngrx-data


angular-ngrx-data es una biblioteca de complementos NgRx que le permite trabajar con matrices de datos sin escribir código adicional.
Además de crear una gestión estatal completa, se compromete a crear servicios con http para interactuar con el servidor.

Considera un ejemplo


Instalación

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

Módulo de datos angulares-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 {} 

Conéctate a la aplicación

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

Acabamos de obtener la API generada para trabajar con el back-end y la integración de la API con NgRx , sin escribir un solo efecto, reductor y acción y selector.

Examinemos con más detalle lo que está sucediendo aquí.


La constante defaultDataServiceConfig establece la configuración de nuestra API y se conecta al módulo de proveedores . La propiedad raíz indica a dónde ir para las solicitudes. Si no está configurado, el valor predeterminado será "api".

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

La constante entityMetadata define los nombres de la tienda que se crearán cuando se conecte NgrxDataModule.forRoot .

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

La ruta a la API consiste en la ruta base (en nuestro caso, "crud") y el nombre de la tienda.
Por ejemplo, para obtener un usuario con un número determinado, la ruta sería "crud / user / {userId}".

Para obtener una lista completa de usuarios, la letra "s" - "crud / user s " se agrega al final del nombre de la tienda de forma predeterminada.

Si necesita una ruta diferente para obtener la lista completa (por ejemplo, "héroes" y no "héroes"), puede cambiarla configurando pluralNames y conectándolos a NgrxDataModule.forRoot .

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

Conexión en componente


Para conectarse en el componente, debe pasar el constructor entityServices al constructor y usar el método getEntityCollectionService para seleccionar el servicio del almacenamiento deseado

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

Para enlazar la lista al componente, es suficiente tomar las entidades $ property del servicio y obtener los datos del servidor, llame al método getAll () .

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

Además de los datos básicos, también puede obtener:

- cargado $ , cargando $ - obteniendo el estado de carga de datos,
- errores $ - errores cuando el servicio se está ejecutando,
- cuenta $ - número total de registros en el repositorio.

Los principales métodos para interactuar con el servidor:

- getAll () - obteniendo la lista completa de datos,
- getWithQuery (consulta) : filtrar una lista utilizando parámetros de consulta,
- getByKey (id) - obteniendo un registro por identificador,
- add (entity) : agrega una nueva entidad con una solicitud de respaldo,
- eliminar (entidad) : eliminar una entidad con una solicitud de respaldo,
- actualizar (entidad) : actualiza la entidad con una solicitud de respaldo.

Métodos de almacenamiento local:

- addManyToCache (entidad) : agrega una matriz de nuevas entidades al repositorio,
- addOneToCache (entidad) : agrega una nueva entidad solo a la tienda,
- removeOneFromCache (id) : elimina una entidad del repositorio,
- updateOneInCache (entidad) : actualiza la entidad en el repositorio,
- upsertOneInCache (entidad) : si existe una entidad con el ID especificado, se actualiza; de lo contrario, se crea una nueva,
- y otros

Ejemplo de uso de componentes

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

Todos los métodos de angular-ngrx-data se dividen en trabajar localmente e interactuar con el servidor. Esto le permite utilizar la biblioteca al manipular datos tanto en el cliente como en el servidor.

Registro


Para iniciar sesión, debe inyectar EntityServices en un componente o servicio y usar las propiedades:

- reduceActions $ - para acciones de registro,
- entityActionErrors $ - para errores de registro.

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

Pasar al repositorio principal de NgRx


Como se anunció en ng-conf 2018 , angular-ngrx-data se migrará al repositorio principal de NgRx pronto .

Reduciendo el Boilerplate con el video de NgRx talk


Referencias


Los creadores de anguar-ngrx-data:
- John Papa twitter.com/John_Papa
- Ward Bell twitter.com/wardbell

Repositorios oficiales:
- NgRx
- angular-ngrx-data

Ejemplo de aplicación:
- con NgRx sin angular-ngrx-data
- con NgRx y angular-ngrx-data

Comunidad angular de habla rusa en Telegram

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


All Articles