Ngrx and Angular 2 Tutorial: Building a Reactive Application
we talk a lot about reactive programming in the Angular realm. Programação Reativa e Angular 2 parecem andar de mãos dadas. No entanto, para qualquer um que não esteja familiarizado com ambas as tecnologias, pode ser uma tarefa bastante assustadora para descobrir o que é tudo sobre.
neste artigo, através da construção de uma aplicação Angular 2 reativa usando Ngrx, você vai aprender qual é o padrão, onde o padrão pode ser útil, e como o padrão pode ser usado para construir aplicações angulares 2 melhores.
Ngrx é um grupo de bibliotecas angulares para extensões reativas. Ngrx / Store implementa o padrão Redux usando os bem conhecidos RxJS observáveis do Angular 2. Ele fornece várias vantagens, simplificando o seu estado de aplicação para objetos simples, impondo fluxo unidirecional de dados, e muito mais. A Biblioteca Ngrx / efeitos permite que a aplicação comunique com o mundo exterior, desencadeando efeitos colaterais.
o que é a Programação Reativa?
Programação Reativa é um termo que você ouve muito hoje em dia, mas o que isso realmente significa?
Programação Reativa é uma forma de as aplicações lidarem com eventos e fluxo de dados em suas aplicações. Na Programação Reativa, você projetar seus componentes e outras peças de seu software, a fim de reagir a essas mudanças em vez de pedir mudanças. Isto pode ser um grande turno.
uma grande ferramenta para Programação Reativa, como você pode saber, é RxJS.
ao fornecer observáveis e um monte de operadores para transformar dados recebidos, esta biblioteca irá ajudá-lo a lidar com eventos em sua aplicação. Na verdade, com observáveis, você pode ver o evento como um fluxo de eventos e não um evento único. Isso permite que você os combine, por exemplo, para criar um novo evento para o qual você vai ouvir.
Programação Reativa é uma mudança na forma como você se comunica entre diferentes partes de uma aplicação. Em vez de empurrar dados diretamente para o componente ou serviço que precisava dele, na Programação Reativa, é o componente ou serviço que reage às mudanças de dados.
uma palavra sobre Ngrx
a fim de compreender a aplicação que irá construir através deste tutorial, você deve fazer um mergulho rápido nos conceitos de Redux core.
Store
A store pode ser vista como a sua base de dados do lado do cliente, mas, mais importante, reflecte o estado da sua aplicação. Você pode vê-lo como a única fonte da verdade.
é a única coisa que você altera quando segue o padrão de Redux e você modifica enviando ações para ele.redutores
redutoresredutores são as funções que sabem o que fazer com uma dada ação e o estado anterior do seu aplicativo.
os redutores irão tomar o estado anterior da sua loja e aplicar uma função pura a ele. Puro significa que a função retorna sempre o mesmo valor para a mesma entrada e que não tem efeitos colaterais. A partir do resultado dessa função pura, você terá um novo estado que será colocado em sua loja.as acções
As acções
são a carga útil que contém a informação necessária para alterar a sua loja. Basicamente, uma ação tem um tipo e uma carga útil que sua função redutora vai tomar para alterar o estado.
Dispatcher
Dispatchers são simplesmente um ponto de entrada para você expedir sua ação. Em Ngrx, há um método de expedição diretamente na loja.
Middleware
Middleware são algumas funções que irão interceptar cada acção que está a ser enviada para criar efeitos secundários, mesmo que não os use neste artigo. Eles são implementados na biblioteca Ngrx/Effect, e há uma grande chance de que você vai precisar deles ao construir aplicações do mundo real.
por que utilizar Ngrx?
complexidade
O fluxo de dados de armazenamento e unidirecional reduzem significativamente o acoplamento entre as partes da sua aplicação. Este acoplamento reduzido reduz a complexidade de sua aplicação, uma vez que cada parte só se preocupa com estados específicos.
Ferramentas
todo o estado de sua aplicação é armazenado em um único lugar, por isso é fácil ter uma visão global do seu estado de aplicação e ajuda durante o desenvolvimento. Além disso, com Redux vem um monte de ferramentas dev nice que tirar proveito da loja e pode ajudar a reproduzir um certo estado da aplicação ou fazer viagem no tempo, por exemplo.
simplicidade arquitetônica
muitos dos benefícios da Ngrx são alcançáveis com outras soluções; afinal, Redux é um padrão arquitetônico. Mas quando você tem que construir uma aplicação que é um grande ajuste para o padrão Redux, como ferramentas de edição colaborativa, você pode facilmente adicionar recursos, seguindo o padrão.
embora você não tenha que pensar sobre o que você está fazendo, adicionar algumas coisas como análise através de todas as suas aplicações torna-se trivial, uma vez que você pode rastrear todas as ações que são enviadas.
curva de aprendizagem pequena
Uma vez que este padrão é tão amplamente adotado e simples, é realmente fácil para as novas pessoas em sua equipe para alcançar rapidamente o que você fez.
Ngrx brilha mais quando você tem um monte de atores externos que podem modificar sua aplicação, como um painel de monitoramento. Nesses casos, é difícil gerenciar todos os dados recebidos que são empurrados para a sua aplicação, e a gestão do Estado torna-se difícil. É por isso que você quer simplificá-lo com um estado imutável, e esta é uma coisa que a Loja Ngrx nos fornece.
construindo uma aplicação com Ngrx
a potência do Ngrx brilha mais quando você tem dados externos que estão sendo empurrados para a nossa aplicação em tempo real. Com isso em mente, vamos construir uma grade freelancer simples que mostra freelancers on-line e permite que você filtre através deles.
configurar o projecto
o CLI Angular é uma ferramenta impressionante que simplifica grandemente o processo de configuração. Você pode querer não usá-lo, mas tenha em mente que o resto deste artigo irá usá-lo.
npm install -g @angular/cli
em seguida, você deseja criar um novo aplicativo e instalar todos os Ngrx bibliotecas:
ng new toptal-freelancersnpm install ngrx --save
Freelancers Redutor
Redutores são uma parte da essência do Redux arquitetura, então por que não começar com o primeiro, enquanto a construção do aplicativo?primeiro, crie um redutor “freelancers” que será responsável pela criação de nosso novo estado cada vez que uma ação é enviada para a loja.freelancer-grid / freelancers.redutor.ts
import { Action } from '@ngrx/store';export interface AppState { freelancers : Array<IFreelancer>}export interface IFreelancer { name: string, email: string, thumbnail: string}export const ACTIONS = { FREELANCERS_LOADED: 'FREELANCERS_LOADED',}export function freelancersReducer( state: Array<IFreelancer> = , action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); default: return state; }}
então aqui está o nosso freelancers reducer.
Esta função será chamada cada vez que uma ação é despachada através da loja. If the action is FREELANCERS_LOADED
, it will create a new array from the action payload. Se não for, devolverá a antiga referência do estado e nada será adicionado.
é importante notar aqui que, se a referência do estado antigo for devolvida, o estado será considerado inalterado. Isto significa que se você chamar um state.push(something)
, o estado não será considerado como tendo mudado. Tenha isso em mente enquanto faz suas funções redutoras.os estados são imutáveis. Um novo Estado deve ser devolvido cada vez que muda.
Freelancer Grid Component
Create a grid component to show our online freelancers. No início, ele só vai refletir o que está na loja.
ng generate component freelancer-grid
coloque o seguinte na grelha freelancer.componente.ts
import { Component, OnInit } from '@angular/core';import { Store } from '@ngrx/store';import { AppState, IFreelancer, ACTIONS } from './freelancer-reducer';import * as Rx from 'RxJS';@Component({ selector: 'app-freelancer-grid', templateUrl: './freelancer-grid.component.html', styleUrls: ,})export class FreelancerGridComponent implements OnInit { public freelancers: Rx.Observable<Array<IFreelancer>>; constructor(private store: Store<AppState>) { this.freelancers = store.select('freelancers'); }}
e o seguinte em freelancer-grid.componente.html:
<span class="count">Number of freelancers online: {{(freelancers | async).length}}</span><div class="freelancer fade thumbail" *ngFor="let freelancer of freelancers | async"> <button type="button" class="close" aria-label="Close" (click)="delete(freelancer)"><span aria-hidden="true">×</span></button><br> <img class="img-circle center-block" src="{{freelancer.thumbnail}}" /><br> <div class="info"><span><strong>Name: </strong>{{freelancer.name}}</span> <span><strong>Email: </strong>{{freelancer.email}}</span></div> <a class="btn btn-default">Hire {{freelancer.name}}</a></div>
então o que você acabou de fazer?
primeiro, você criou um novo componente chamado freelancer-grid
.
o componente contém uma propriedade chamada freelancers
que é uma parte do Estado de Aplicação contido na loja Ngrx. Ao usar o operador select, você escolhe ser notificado apenas pelofreelancers
propriedade do Estado de aplicação global. Então agora cada vez que o freelancers
propriedade das alterações do estado da aplicação, o seu observável será notificado.
Uma coisa que é bonita com esta solução é que o seu componente tem apenas uma dependência, e é a loja que torna o seu componente muito menos complexo e facilmente reutilizável.
na parte do modelo, você não fez nada muito complexo. Observe a utilização do tubo async no *ngFor
. Ofreelancers
observável não é diretamente iterável, mas graças ao Angular, temos as ferramentas para desembrulhá-lo e ligar o dom ao seu valor usando o tubo async. Isso torna o trabalho com o observável muito mais fácil.
adicionando a funcionalidade de remover Freelancers
Agora que você tem uma base funcional, vamos adicionar algumas ações à aplicação.você quer ser capaz de remover um freelancer do estado. De acordo com como Redux funciona, você precisa primeiro definir essa ação em cada Estado que são afetados por ele.
neste caso, é apenas o freelancers
redutor de:
export const ACTIONS = { FREELANCERS_LOADED: 'FREELANCERS_LOADED', DELETE_FREELANCER: 'DELETE_FREELANCER',}export function freelancersReducer( state: Array<IFreelancer> = , action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); case ACTIONS.DELETE_FREELANCER: // Remove the element from the array state.splice(state.indexOf(action.payload), 1); // We need to create another reference return Array.prototype.concat(state); default: return state; }}
é realmente importante aqui para criar uma nova matriz da antiga, a fim de ter um novo estado imutável.
Agora, você pode adicionar uma função delete freelancers ao seu componente que irá enviar esta ação para a loja:
delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) }
isso não parece simples?
Agora você pode remover um freelancer específico do estado, e essa mudança irá se propagar através de sua aplicação.e se adicionar outro componente à aplicação para ver como podem interagir entre si através da loja?como sempre, vamos começar com o redutor. Para esse componente, é bastante simples. Quer que o redutor regresse sempre um novo estado com apenas a propriedade que enviámos. Ele deve se parece com isso:
import { Action } from '@ngrx/store';export interface IFilter { name: string, email: string,}export const ACTIONS = { UPDATE_FITLER: 'UPDATE_FITLER', CLEAR_FITLER: 'CLEAR_FITLER',}const initialState = { name: '', email: '' };export function filterReducer( state: IFilter = initialState, action: Action): IFilter { switch (action.type) { case ACTIONS.UPDATE_FITLER: // Create a new state from payload return Object.assign({}, action.payload); case ACTIONS.CLEAR_FITLER: // Create a new state from initial state return Object.assign({}, initialState); default: return state; }}
Componente de Filtro
import { Component, OnInit } from '@angular/core';import { IFilter, ACTIONS as FilterACTIONS } from './filter-reducer';import { Store } from '@ngrx/store';import { FormGroup, FormControl } from '@angular/forms';import * as Rx from 'RxJS';@Component({ selector: 'app-filter', template: '<form class="filter">'+ '<label>Name</label>'+ '<input type="text" ="name" name="name"/>'+ '<label>Email</label>'+ '<input type="text" ="email" name="email"/>'+ '<a (click)="clearFilter()" class="btn btn-default">Clear Filter</a>'+ '</form>', styleUrls: ,})export class FilterComponent implements OnInit { public name = new FormControl(); public email = new FormControl(); constructor(private store: Store<any>) { store.select('filter').subscribe((filter: IFilter) => { this.name.setValue(filter.name); this.email.setValue(filter.email); }) Rx.Observable.merge(this.name.valueChanges, this.email.valueChanges).debounceTime(1000).subscribe(() => this.filter()); } ngOnInit() { } filter() { this.store.dispatch({ type: FilterACTIONS.UPDATE_FITLER, payload: { name: this.name.value, email: this.email.value, } }); } clearFilter() { this.store.dispatch({ type: FilterACTIONS.CLEAR_FITLER, }) }}
Primeiro, você tem feito um modelo simples, que inclui um formulário com dois campos (nome e e-mail), que reflete o nosso estado.
Você mantém esses campos em sincronia com o estado um pouco diferente do que fez com o estadofreelancers
. De facto, como já viu, subscreveu o estado do filtro, e de cada vez, ele despoleta-o a atribuir o novo valor ao formControl
.
Uma coisa que é bom com o Angular 2 é que ele lhe fornece um monte de ferramentas para interagir com observáveis.
Você viu o pipe async mais cedo, e agora você vê a classe formControl
que lhe permite ter um observável sobre o valor de uma entrada. Isto permite coisas extravagantes como o que você fez no componente filtro.
Como você pode ver, você pode usar Rx.observable.merge
combinar as duas observáveis dadas pelo seu formControls
e, em seguida, você debounce que novos observáveis antes de acionar o filter
função.
em palavras mais simples, você espera um segundo após o nome ou e-mail formControl
ter mudado e, em seguida, chamar a funçãofilter
.não é fantástico?
tudo isso é feito em algumas linhas de código. Esta é uma das razões pelas quais você vai amar RxJS. Ele permite que você faça muitas dessas coisas extravagantes facilmente que teria sido mais complicado de outra forma.
Agora vamos avançar para essa função de filtro. O que faz?
simplesmente envia oUPDATE_FILTER
ação com o valor do nome e do E-mail, e o redutor cuida de alterar o estado com essa informação.vamos avançar para algo mais interessante.como faz com que esse filtro interaja com a sua grelha freelancer criada anteriormente?
simples. Só tens de ouvir a parte filtrante da loja. Vamos ver como é o código.
import { Component, OnInit } from '@angular/core';import { Store } from '@ngrx/store';import { AppState, IFreelancer, ACTIONS } from './freelancer-reducer';import { IFilter, ACTIONS as FilterACTIONS } from './../filter/filter-reducer';import * as Rx from 'RxJS';@Component({ selector: 'app-freelancer-grid', templateUrl: './freelancer-grid.component', styleUrls: ,})export class FreelancerGridComponent implements OnInit { public freelancers: Rx.Observable<Array<IFreelancer>>; public filter: Rx.Observable<IFilter>; constructor(private store: Store<AppState>) { this.freelancers = Rx.Observable.combineLatest(store.select('freelancers'), store.select('filter'), this.applyFilter); } applyFilter(freelancers: Array<IFreelancer>, filter: IFilter): Array<IFreelancer> { return freelancers .filter(x => !filter.name || x.name.toLowerCase().indexOf(filter.name.toLowerCase()) !== -1) .filter(x => !filter.email || x.email.toLowerCase().indexOf(filter.email.toLowerCase()) !== -1) } ngOnInit() { } delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) }}
não é mais complicado do que isso.mais uma vez, usou o poder dos RxJS para combinar o estado de filtragem e freelancers.
In fact, combineLatest
will fire if one of the two observables fire and then combine each state using theapplyFilter
function. Ele retorna um novo observável que o faz. Não temos de mudar outras linhas de código.
Notice how the component does not care about how the filter is obtained, modified, or stored; it only listens to it as it as it would do for any other state. Nós apenas adicionamos a funcionalidade do filtro e não adicionamos nenhuma dependências novas.lembra-se que o uso de Ngrx realmente brilha quando temos que lidar com dados em tempo real? Vamos adicionar essa parte à nossa aplicação e ver como corre.introdução dofreelancers-service
.
ng generate service freelancer
The freelancer service will simulate real time operation on data and should looks like this.
import { Injectable } from '@angular/core';import { Store } from '@ngrx/store';import { AppState, IFreelancer, ACTIONS } from './freelancer-grid/freelancer-reducer';import { Http, Response } from '@angular/http';@Injectable()export class RealtimeFreelancersService { private USER_API_URL = 'https://randomuser.me/api/?results=' constructor(private store: Store<AppState>, private http: Http) { } private toFreelancer(value: any) { return { name: value.name.first + ' ' + value.name.last, email: value.email, thumbail: value.picture.large, } } private random(y) { return Math.floor(Math.random() * y); } public run() { this.http.get(`${this.USER_API_URL}51`).subscribe((response) => { this.store.dispatch({ type: ACTIONS.FREELANCERS_LOADED, payload: response.json().results.map(this.toFreelancer) }) }) setInterval(() => { this.store.select('freelancers').first().subscribe((freelancers: Array<IFreelancer>) => { let getDeletedIndex = () => { return this.random(freelancers.length - 1) } this.http.get(`${this.USER_API_URL}${this.random(10)}`).subscribe((response) => { this.store.dispatch({ type: ACTIONS.INCOMMING_DATA, payload: { ADD: response.json().results.map(this.toFreelancer), DELETE: new Array(this.random(6)).fill(0).map(() => getDeletedIndex()), } }); this.addFadeClassToNewElements(); }); }); }, 10000); } private addFadeClassToNewElements() { let elements = window.document.getElementsByClassName('freelancer'); for (let i = 0; i < elements.length; i++) { if (elements.item(i).className.indexOf('fade') === -1) { elements.item(i).classList.add('fade'); } } }}
Este serviço não é perfeito, mas ele faz o que faz e, para fins de demonstração, permite-nos demonstrar algumas coisas.
primeiro, este serviço é bastante simples. Ele consulta uma API do Usuário e empurra os resultados para a loja. É fácil, e você não precisa pensar para onde os dados vão. Vai para a loja, que é algo que torna Redux tão útil e perigoso ao mesmo tempo—mas voltaremos a isso mais tarde. Depois de cada dez segundos, o serviço escolhe alguns freelancers e envia uma operação para eliminá-los, juntamente com uma operação para alguns outros freelancers.
Se queremos que o nosso redutor seja capaz de lidar com ele, precisamos modificá-lo:
import { Action } from '@ngrx/store';export interface AppState { freelancers : Array<IFreelancer>}export interface IFreelancer { name: string, email: string,}export const ACTIONS = { LOAD_FREELANCERS: 'LOAD_FREELANCERS', INCOMMING_DATA: 'INCOMMING_DATA', DELETE_FREELANCER: 'DELETE_FREELANCER',}export function freelancersReducer( state: Array<IFreelancer> = , action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.INCOMMING_DATA: action.payload.DELETE.forEach((index) => { state.splice(state.indexOf(action.payload), 1); }) return Array.prototype.concat(action.payload.ADD, state); case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); case ACTIONS.DELETE_FREELANCER: // Remove the element from the array state.splice(state.indexOf(action.payload), 1); // We need to create another reference return Array.prototype.concat(state); default: return state; }}
Agora somos capazes de lidar com tais operações.
uma coisa que é demonstrada nesse serviço é que, de todo o processo de mudanças de Estado sendo feito sincronicamente, é muito importante notar isso. Se a aplicação do estado fosse async, a chamada em this.addFadeClassToNewElements();
não funcionaria como o elemento DOM não seria criado quando esta função é chamada.pessoalmente, acho isso muito útil, uma vez que melhora a previsibilidade.
aplicações de construção, a forma reativa
através deste tutorial, você construiu uma aplicação reativa usando Ngrx, RxJS e Angular 2.como já viu, estas são ferramentas poderosas. O que você construiu aqui também pode ser visto como a implementação de uma arquitetura Redux, E Redux é poderoso em si mesmo. No entanto, também tem algumas limitações. Enquanto usamos o Ngrx, essas restrições inevitavelmente refletem na parte da nossa aplicação que usamos.
O diagrama acima é uma grosseira da arquitetura que você acabou de fazer.
Você pode notar que, mesmo que alguns componentes estejam influenciando uns aos outros, eles são independentes uns dos outros. Esta é uma peculiaridade desta arquitetura: componentes compartilham uma dependência comum, que é a loja.
outra coisa em particular sobre esta arquitetura é que nós não chamamos de funções, mas de ações de envio. Uma alternativa ao Ngrx poderia ser apenas fazer um serviço que gerencia um estado em particular com observáveis de suas aplicações e funções de chamada nesse serviço em vez de ações. Desta forma, você poderia obter centralização e reactividade do Estado ao isolar o estado problemático. Esta abordagem pode ajudá-lo a reduzir a sobrecarga da criação de um redutor e descrever as ações como objetos simples.