angular-ngrx-data - gerenciamento de estado e CRUD em cinco minutos

imagem
Até o momento, nem um único aplicativo de SPA grande está completo sem gerenciamento de estado . Existem várias soluções para a Angular nessa área. O mais popular deles é o NgRx . Ele implementa um padrão Redux usando a biblioteca RxJs e possui boas ferramentas.

Neste artigo, examinaremos brevemente os principais módulos NgRx e focaremos em mais detalhes a biblioteca de dados angular-ngrx , que permite fazer um CRUD completo com gerenciamento de estado em cinco minutos.

Revisão NgRx


Você pode ler mais sobre o NgRx nos seguintes artigos:

- Aplicações reativas em Angular / NGRX. Parte 1. Introdução
- Aplicações reativas em Angular / NGRX. Parte 2. Loja
- Aplicações reativas em Angular / NGRX. Parte 3. Efeitos

Considere brevemente os principais módulos do NgRx , seus prós e contras.

NgRx / store - implementa um padrão Redux.

Implementação simples da loja
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; } } 
.
Conexão ao 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 {} 

Use no 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 - permite rastrear alterações no aplicativo através do redux-devtools .

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


NgRx / effects - permite adicionar dados que entram no aplicativo ao repositório, como solicitações http.

Exemplo
./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 o efeito ao módulo

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


NgRx / entity - fornece a capacidade de trabalhar com matrizes de dados.

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

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


Qual é o resultado?


Temos gerenciamento de estado completo com várias vantagens:

- uma única fonte de dados para a aplicação,
- o estado é armazenado separadamente do aplicativo,
- um estilo de escrita único para todos os desenvolvedores do projeto,
- changeDetectionStrategy.OnPush em todos os componentes do aplicativo,
- depuração conveniente através do redux-devtools ,
- facilidade de teste, como redutores são funções puras.

Mas também há desvantagens:

- um grande número de módulos aparentemente incompreensíveis,
- muito do mesmo tipo de código que você não verá sem tristeza,
- dificuldade em dominar por causa de todos os itens acima.

CRUD


Como regra, uma parte significativa do aplicativo é ocupada com o trabalho com objetos (criação, leitura, atualização, exclusão); portanto, para maior comodidade do trabalho, foi criado o conceito CRUD (Criar, Ler, Atualizar, Excluir). Assim, as operações básicas para trabalhar com todos os tipos de objetos são padronizadas. Ele está crescendo há muito tempo no back-end. Muitas bibliotecas ajudam a implementar essa funcionalidade e a se livrar do trabalho de rotina.

No NgRx , o módulo da entidade é responsável pelo CRUD e, se você observar um exemplo de sua implementação, poderá ver imediatamente que essa é a parte maior e mais complexa do NgRx . É por isso que John Papa e Ward Bell criaram angular-ngrx-data .

angular-ngrx-data


angular-ngrx-data é uma biblioteca de suplementos NgRx que permite trabalhar com matrizes de dados sem escrever código extra.
Além de criar um gerenciamento de estado completo, ela realiza a criação de serviços com http para interagir com o servidor.

Considere um exemplo


Instalação

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

Módulo Angular-ngrx-data

 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 {} 

Conecte-se ao aplicativo

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

Acabamos de obter a API gerada para trabalhar com o back-end e a integração da API com o NgRx , sem escrever um único efeito, redutor, ação e seletor.

Vamos examinar em mais detalhes o que está acontecendo aqui.


A constante defaultDataServiceConfig define a configuração da nossa API e se conecta ao módulo de fornecedores . A propriedade raiz indica para onde ir para solicitações. Se não estiver definido, o padrão será "api".

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

A constante entityMetadata define os nomes do armazenamento que será criado quando o NgrxDataModule.forRoot estiver conectado.

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

O caminho para a API consiste no caminho base (no nosso caso "crud") e no nome da loja.
Por exemplo, para obter um usuário com um determinado número, o caminho seria "crud / user / {userId}".

Para obter uma lista completa de usuários, a letra "s" - "crud / user s " é adicionada ao final do nome da loja por padrão.

Se você precisar de uma rota diferente para obter a lista completa (por exemplo, "heróis" e não "heróis"), poderá alterá-la definindo pluralNames e conectando-os ao NgrxDataModule.forRoot .

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

Conexão no componente


Para conectar-se ao componente, você precisa passar o construtor entityServices para o construtor e usar o método getEntityCollectionService para selecionar o serviço do armazenamento desejado

 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 vincular a lista ao componente, basta retirar a propriedade $ entity do serviço e, para obter os dados do servidor, chame o método getAll () .

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

Além disso, além dos dados básicos, você pode obter:

- carregado $ , carregando $ - obtendo o status de carregamento de dados,
- errors $ - erros quando o serviço está sendo executado,
- count $ - número total de registros no repositório.

Os principais métodos de interação com o servidor:

- getAll () - obtendo a lista inteira de dados,
- getWithQuery (query) - filtrando uma lista usando parâmetros de consulta,
- getByKey (id) - obtendo um registro por identificador,
- add (entidade) - adicionando uma nova entidade com uma solicitação de apoio,
- excluir (entidade) - excluir uma entidade com uma solicitação de apoio,
- atualização (entidade) - atualiza a entidade com uma solicitação de apoio.

Métodos de armazenamento local:

- addManyToCache (entidade) - adicionando uma matriz de novas entidades ao repositório,
- addOneToCache (entity) - adicionando uma nova entidade apenas ao repositório,
- removeOneFromCache (id) - remove uma entidade do repositório,
- updateOneInCache (entity) - atualiza a entidade no repositório,
- upsertOneInCache (entity) - se uma entidade com o ID especificado existir, ela será atualizada; caso contrário, um novo será criado,
- e outros

Exemplo de uso de componente

 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 os métodos angular-ngrx-data são divididos em trabalhar localmente e interagir com o servidor. Isso permite que você use a biblioteca ao manipular dados no cliente e no servidor.

Registo


Para o log, você precisa injetar EntityServices em um componente ou serviço e usar as propriedades:

- reduzidaActions $ - para ações de registro,
entityActionErrors $ - para erros de log.

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

Movendo para o repositório principal do NgRx


Conforme anunciado no ng-conf 2018 , o angular-ngrx-data será migrado para o repositório principal do NgRx em breve .

Reduzindo o Boilerplate com NgRx talk video - Brandon Roberts & Mike Ryan


Referências


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

Repositórios oficiais:
- NgRx
- angular-ngrx-data

Exemplo de aplicação:
- com NgRx sem dados angulares-ngrx
- com NgRx e angular-ngrx-data

Comunidade angular de língua russa no telegrama

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


All Articles