
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. EfectosConsidere brevemente los módulos principales de
NgRx , sus ventajas y desventajas.
NgRx / store : implementa un patrón Redux.
Implementación simple de la tiendacounter.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.
Ejemplouser.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/wardbellRepositorios oficiales:-
NgRx-
angular-ngrx-dataEjemplo de aplicación:-
con NgRx sin angular-ngrx-data-
con NgRx y angular-ngrx-dataComunidad angular de habla rusa en Telegram