Managementul statului în Angular folosind Firebase
Publicat: 2022-03-11Managementul statului este o arhitectură foarte importantă de luat în considerare atunci când dezvoltați o aplicație web.
În acest tutorial, vom trece peste o abordare simplă pentru a gestiona starea într-o aplicație Angular care utilizează Firebase ca back-end.
Vom trece peste câteva concepte precum stat, magazine și servicii. Sperăm că acest lucru vă va ajuta să înțelegeți mai bine acești termeni și, de asemenea, să înțelegeți mai bine alte biblioteci de management de stat, cum ar fi NgRx și NgXs.
Vom construi o pagină de administrare a angajaților pentru a acoperi câteva scenarii diferite de management de stat și abordările care le pot gestiona.
Componente, servicii, Firestore și management de stat în Angular
Pe o aplicație tipică Angular avem componente și servicii. De obicei, componentele vor servi ca șablon de vizualizare. Serviciile vor conține logica de afaceri și/sau vor comunica cu API-uri externe sau alte servicii pentru a finaliza acțiuni sau pentru a prelua date.
Componentele vor afișa de obicei date și vor permite utilizatorilor să interacționeze cu aplicația pentru a executa acțiuni. În timp ce faceți acest lucru, datele se pot schimba, iar aplicația reflectă acele modificări prin actualizarea vizualizării.
Motorul de detectare a modificărilor Angular are grijă să verifice când o valoare dintr-o componentă legată de vizualizare s-a schimbat și actualizează vizualizarea în consecință.
Pe măsură ce aplicația crește, vom începe să avem din ce în ce mai multe componente și servicii. Adesea, înțelegerea modului în care se schimbă datele și urmărirea unde se întâmplă acest lucru poate fi dificil.
Angular și Firebase
Când folosim Firebase ca back-end, ni se oferă un API cu adevărat îngrijit, care conține majoritatea operațiunilor și funcționalităților de care avem nevoie pentru a construi o aplicație în timp real.
@angular/fire
este biblioteca oficială Angular Firebase. Este un strat deasupra bibliotecii Firebase JavaScript SDK care simplifică utilizarea SDK-ului Firebase într-o aplicație Angular. Oferă o potrivire bună cu bunele practici Angular, cum ar fi utilizarea Observables pentru obținerea și afișarea datelor de la Firebase către componentele noastre.
Magazine și Stat
Ne putem gândi la „stare” ca fiind valorile afișate în orice moment dat în aplicație. Magazinul este pur și simplu deținătorul acelei stări de aplicație.
Starea poate fi modelată ca un singur obiect simplu sau o serie de ele, reflectând valorile aplicației.
Aplicație de probă Angular/Firebase
Să o construim: În primul rând, vom crea o aplicație de bază folosind CLI Angular și o vom conecta la un proiect Firebase.
$ npm install -g @angular/cli $ ng new employees-admin` Would you like to add Angular routing? Yes Which stylesheet format would you like to use? SCSS $ cd employees-admin/ $ npm install bootstrap # We'll add Bootstrap for the UI
Și, pe styles.scss
:
// ... @import "~bootstrap/scss/bootstrap";
În continuare, vom instala @angular/fire
:
npm install firebase @angular/fire
Acum, vom crea un proiect Firebase la consola Firebase.
Apoi suntem gata să creăm o bază de date Firestore.
Pentru acest tutorial, voi începe în modul de testare. Dacă intenționați să lansați în producție, ar trebui să aplicați reguli pentru a interzice accesul neadecvat.
Accesați Prezentare generală a proiectului → Setări proiect și copiați configurația web Firebase în mediile locale environments/environment.ts
.
export const environment = { production: false, firebase: { apiKey: "<api-key>", authDomain: "<auth-domain>", databaseURL: "<database-url>", projectId: "<project-id>", storageBucket: "<storage-bucket>", messagingSenderId: "<messaging-sender-id>" } };
În acest moment, avem schela de bază pentru aplicația noastră. Dacă ng serve
, vom obține:
Clasele de bază Firestore și Store
Vom crea două clase abstracte generice, pe care apoi le vom tasta și de la care le vom extinde pentru a ne construi serviciile.
Genericurile vă permit să scrieți comportament fără un tip legat. Acest lucru adaugă reutilizabilitate și flexibilitate codului dvs.
Serviciu generic Firestore
Pentru a profita de genericele TypeScript, ceea ce vom face este să creăm un wrapper generic de bază pentru serviciul @angular/fire
firestore
.
Să creăm app/core/services/firestore.service.ts
.
Iată codul:
import { Inject } from "@angular/core"; import { AngularFirestore, QueryFn } from "@angular/fire/firestore"; import { Observable } from "rxjs"; import { tap } from "rxjs/operators"; import { environment } from "src/environments/environment"; export abstract class FirestoreService<T> { protected abstract basePath: string; constructor( @Inject(AngularFirestore) protected firestore: AngularFirestore, ) { } doc$(id: string): Observable<T> { return this.firestore.doc<T>(`${this.basePath}/${id}`).valueChanges().pipe( tap(r => { if (!environment.production) { console.groupCollapsed(`Firestore Streaming [${this.basePath}] [doc$] ${id}`) console.log(r) console.groupEnd() } }), ); } collection$(queryFn?: QueryFn): Observable<T[]> { return this.firestore.collection<T>(`${this.basePath}`, queryFn).valueChanges().pipe( tap(r => { if (!environment.production) { console.groupCollapsed(`Firestore Streaming [${this.basePath}] [collection$]`) console.table(r) console.groupEnd() } }), ); } create(value: T) { const id = this.firestore.createId(); return this.collection.doc(id).set(Object.assign({}, { id }, value)).then(_ => { if (!environment.production) { console.groupCollapsed(`Firestore Service [${this.basePath}] [create]`) console.log('[Id]', id, value) console.groupEnd() } }) } delete(id: string) { return this.collection.doc(id).delete().then(_ => { if (!environment.production) { console.groupCollapsed(`Firestore Service [${this.basePath}] [delete]`) console.log('[Id]', id) console.groupEnd() } }) } private get collection() { return this.firestore.collection(`${this.basePath}`); } }
Această abstract class
va funcționa ca un pachet generic pentru serviciile noastre Firestore.
Acesta ar trebui să fie singurul loc în care ar trebui să injectăm AngularFirestore
. Acest lucru va minimiza impactul atunci când biblioteca @angular/fire
este actualizată. De asemenea, dacă la un moment dat dorim să schimbăm biblioteca, va trebui doar să actualizăm această clasă.
Am adăugat doc$
, collection$
, create
și delete
. Acestea înglobează metodele @angular/fire
și oferă înregistrare în jurnal atunci când Firebase transmite date - acest lucru va deveni foarte util pentru depanare - și după ce un obiect este creat sau șters.
Serviciu de magazin generic
Serviciul nostru de magazin generic va fi creat folosind BehaviorSubject
de la RxJS. BehaviorSubject
le permite abonaților să obțină ultima valoare emisă imediat ce se abonează. În cazul nostru, acest lucru este util, deoarece vom putea începe magazinul cu o valoare inițială pentru toate componentele noastre atunci când se abonează la magazin.
Magazinul va avea două metode, patch
și set
. (Vom crea metode get
mai târziu.)
Să creăm app/core/services/store.service.ts
:
import { BehaviorSubject, Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; export abstract class StoreService<T> { protected bs: BehaviorSubject<T>; state$: Observable<T>; state: T; previous: T; protected abstract store: string; constructor(initialValue: Partial<T>) { this.bs = new BehaviorSubject<T>(initialValue as T); this.state$ = this.bs.asObservable(); this.state = initialValue as T; this.state$.subscribe(s => { this.state = s }) } patch(newValue: Partial<T>, event: string = "Not specified") { this.previous = this.state const newState = Object.assign({}, this.state, newValue); if (!environment.production) { console.groupCollapsed(`[${this.store} store] [patch] [event: ${event}]`) console.log("change", newValue) console.log("prev", this.previous) console.log("next", newState) console.groupEnd() } this.bs.next(newState) } set(newValue: Partial<T>, event: string = "Not specified") { this.previous = this.state const newState = Object.assign({}, newValue) as T; if (!environment.production) { console.groupCollapsed(`[${this.store} store] [set] [event: ${event}]`) console.log("change", newValue) console.log("prev", this.previous) console.log("next", newState) console.groupEnd() } this.bs.next(newState) } }
Ca clasă generică, vom amâna tastarea până când este extinsă corespunzător.
Constructorul va primi valoarea inițială a tipului Partial<T>
. Acest lucru ne va permite să aplicăm valori doar unor proprietăți ale statului. Constructorul se va abona și la emisiile interne BehaviorSubject
și va menține starea internă actualizată după fiecare modificare.
patch()
va primi newValue
de tipul Partial<T>
și o va îmbina cu valoarea actuală this.state
a magazinului. În cele din urmă, urmăm next()
newState
și emitem noua stare tuturor abonaților magazinului.
set()
funcționează foarte asemănător, doar că, în loc să corecteze valoarea stării, o va seta la newValue
pe care a primit-o.
Vom înregistra valorile anterioare și următoare ale stării pe măsură ce apar modificări, ceea ce ne va ajuta să depanăm și să urmărim cu ușurință modificările de stare.
Punând totul laolaltă
Bine, să vedem toate acestea în acțiune. Ceea ce vom face este să creăm o pagină de angajați, care va conține o listă de angajați, plus un formular pentru a adăuga noi angajați.
Să actualizăm app.component.html
pentru a adăuga o bară de navigare simplă:
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-3"> <span class="navbar-brand mb-0 h1">Angular + Firebase + State Management</span> <ul class="navbar-nav mr-auto"> <li class="nav-item" [routerLink]="['/employees']" routerLinkActive="active"> <a class="nav-link">Employees</a> </li> </ul> </nav> <router-outlet></router-outlet>
În continuare, vom crea un modul Core:
ng gm Core
În core/core.module.ts
, vom adăuga modulele necesare pentru aplicația noastră:
// ... import { AngularFireModule } from '@angular/fire' import { AngularFirestoreModule } from '@angular/fire/firestore' import { environment } from 'src/environments/environment'; import { ReactiveFormsModule } from '@angular/forms' @NgModule({ // ... imports: [ // ... AngularFireModule.initializeApp(environment.firebase), AngularFirestoreModule, ReactiveFormsModule, ], exports: [ CommonModule, AngularFireModule, AngularFirestoreModule, ReactiveFormsModule ] }) export class CoreModule { }
Acum, să creăm pagina de angajați, începând cu modulul de angajați:
ng gm Employees --routing
În employees-routing.module.ts
, să adăugăm ruta employees
:
// ... import { EmployeesPageComponent } from './components/employees-page/employees-page.component'; // ... const routes: Routes = [ { path: 'employees', component: EmployeesPageComponent } ]; // ...
Și în employees.module.ts
, vom importa ReactiveFormsModule
:
// ... import { ReactiveFormsModule } from '@angular/forms'; // ... @NgModule({ // ... imports: [ // ... ReactiveFormsModule ] }) export class EmployeesModule { }
Acum, să adăugăm aceste două module în fișierul app.module.ts
:
// ... import { EmployeesModule } from './employees/employees.module'; import { CoreModule } from './core/core.module'; imports: [ // ... CoreModule, EmployeesModule ],
În cele din urmă, să creăm componentele reale ale paginii angajaților noștri, plus modelul, serviciul, magazinul și starea corespunzătoare.
ng gc employees/components/EmployeesPage ng gc employees/components/EmployeesList ng gc employees/components/EmployeesForm
Pentru modelul nostru, vom avea nevoie de un fișier numit models/employee.ts
:
export interface Employee { id: string; name: string; location: string; hasDriverLicense: boolean; }
Serviciul nostru va locui într-un fișier numit employees/services/employee.firestore.ts
. Acest serviciu va extinde FirestoreService<T>
generic creat înainte și vom seta doar calea de bază a colecției basePath
:
import { Injectable } from '@angular/core'; import { FirestoreService } from 'src/app/core/services/firestore.service'; import { Employee } from '../models/employee'; @Injectable({ providedIn: 'root' }) export class EmployeeFirestore extends FirestoreService<Employee> { protected basePath: string = 'employees'; }
Apoi vom crea fișierul employees/states/employees-page.ts
. Aceasta va servi ca pagina de stare a angajaților:
import { Employee } from '../models/employee'; export interface EmployeesPage { loading: boolean; employees: Employee[]; formStatus: string; }
Starea va avea o valoare de loading
care determină dacă se afișează un mesaj de încărcare pe pagină, employees
înșiși și o variabilă formStatus
pentru a gestiona starea formularului (de exemplu, Saving
sau Saved
).
Vom avea nevoie de un fișier la employees/services/employees-page.store.ts
. Aici vom extinde StoreService<T>
creat anterior. Vom seta numele magazinului, care va fi folosit pentru a-l identifica la depanare.
Acest serviciu va inițializa și va menține starea paginii angajaților. Rețineți că constructorul apelează super()
cu starea inițială a paginii. În acest caz, vom inițializa starea cu loading=true
și o matrice goală de angajați.
import { EmployeesPage } from '../states/employees-page'; import { StoreService } from 'src/app/core/services/store.service'; import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class EmployeesPageStore extends StoreService<EmployeesPage> { protected store: string = 'employees-page'; constructor() { super({ loading: true, employees: [], }) } }
Acum să creăm EmployeesService
pentru a integra EmployeeFirestore
și EmployeesPageStore
:
ng gs employees/services/Employees
Rețineți că injectăm EmployeeFirestore
și EmployeesPageStore
în acest serviciu. Aceasta înseamnă că EmployeesService
va conține și va coordona apelurile către Firestore și magazin pentru a actualiza starea. Acest lucru ne va ajuta să creăm un singur API pe care să îl apelăm componentele.
import { EmployeesPageStore } from './employees-page.store'; import { EmployeeFirestore } from './employee.firestore'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Employee } from '../models/employee'; import { tap, map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class EmployeesService { constructor( private firestore: EmployeeFirestore, private store: EmployeesPageStore ) { this.firestore.collection$().pipe( tap(employees => { this.store.patch({ loading: false, employees, }, `employees collection subscription`) }) ).subscribe() } get employees$(): Observable<Employee[]> { return this.store.state$.pipe(map(state => state.loading ? [] : state.employees)) } get loading$(): Observable<boolean> { return this.store.state$.pipe(map(state => state.loading)) } get noResults$(): Observable<boolean> { return this.store.state$.pipe( map(state => { return !state.loading && state.employees && state.employees.length === 0 }) ) } get formStatus$(): Observable<string> { return this.store.state$.pipe(map(state => state.formStatus)) } create(employee: Employee) { this.store.patch({ loading: true, employees: [], formStatus: 'Saving...' }, "employee create") return this.firestore.create(employee).then(_ => { this.store.patch({ formStatus: 'Saved!' }, "employee create SUCCESS") setTimeout(() => this.store.patch({ formStatus: '' }, "employee create timeout reset formStatus"), 2000) }).catch(err => { this.store.patch({ loading: false, formStatus: 'An error ocurred' }, "employee create ERROR") }) } delete(id: string): any { this.store.patch({ loading: true, employees: [] }, "employee delete") return this.firestore.delete(id).catch(err => { this.store.patch({ loading: false, formStatus: 'An error ocurred' }, "employee delete ERROR") }) } }
Să aruncăm o privire la modul în care va funcționa serviciul.

În constructor, ne vom abona la colecția de angajați Firestore. De îndată ce Firestore emite date din colecție, vom actualiza magazinul, setând loading=false
și employees
cu colecția returnată de Firestore. Deoarece am injectat EmployeeFirestore
, obiectele returnate din Firestore sunt tastate în Employee
, ceea ce permite mai multe funcții IntelliSense.
Acest abonament va fi activ cât timp aplicația este activă, ascultând toate modificările și actualizând magazinul de fiecare dată când Firestore transmite date.
this.firestore.collection$().pipe( tap(employees => { this.store.patch({ loading: false, employees, }, `employees collection subscription`) }) ).subscribe()
Funcțiile employees$()
și loading$()
vor selecta partea de stare pe care dorim să o folosim mai târziu pe componentă. employees$()
va returna o matrice goală atunci când starea se încarcă. Acest lucru ne va permite să afișăm mesaje adecvate pe vizualizare.
get employees$(): Observable<Employee[]> { return this.store.state$.pipe(map(state => state.loading ? [] : state.employees)) } get loading$(): Observable<boolean> { return this.store.state$.pipe(map(state => state.loading)) }
Bine, deci acum avem toate serviciile pregătite și putem construi componentele noastre de vizualizare. Dar înainte de a face asta, o reîmprospătare rapidă ar putea fi utilă...
RxJs Observables și conducta async
Observabilele permit abonaților să primească emisii de date ca flux. Acest lucru, în combinație cu conducta async
, poate fi foarte puternic.
Conducta async
are grijă să se aboneze la un Observable și să actualizeze vizualizarea atunci când sunt emise date noi. Mai important, se dezabonează automat când componenta este distrusă, protejându-ne de scurgerile de memorie.
Puteți citi mai multe despre Observables și biblioteca RxJs în general în documentele oficiale.
Crearea componentelor de vizualizare
În employees/components/employees-page/employees-page.component.html
, vom pune acest cod:
<div class="container"> <div class="row"> <div class="col-12 mb-3"> <h4> Employees </h4> </div> </div> <div class="row"> <div class="col-6"> <app-employees-list></app-employees-list> </div> <div class="col-6"> <app-employees-form></app-employees-form> </div> </div> </div>
De asemenea, employees/components/employees-list/employees-list.component.html
vor avea acest lucru, folosind tehnica async
pipe menționată mai sus:
<div *ngIf="loading$ | async"> Loading... </div> <div *ngIf="noResults$ | async"> No results </div> <div class="card bg-light mb-3" *ngFor="let employee of employees$ | async"> <div class="card-header">{{employee.location}}</div> <div class="card-body"> <h5 class="card-title">{{employee.name}}</h5> <p class="card-text">{{employee.hasDriverLicense ? 'Can drive': ''}}</p> <button (click)="delete(employee)" class="btn btn-danger">Delete</button> </div> </div>
Dar în acest caz vom avea nevoie și de un cod TypeScript pentru componentă. Fișierul employees/components/employees-list/employees-list.component.ts
va avea nevoie de acest lucru:
import { Employee } from '../../models/employee'; import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { EmployeesService } from '../../services/employees.service'; @Component({ selector: 'app-employees-list', templateUrl: './employees-list.component.html', styleUrls: ['./employees-list.component.scss'] }) export class EmployeesListComponent implements OnInit { loading$: Observable<boolean>; employees$: Observable<Employee[]>; noResults$: Observable<boolean>; constructor( private employees: EmployeesService ) {} ngOnInit() { this.loading$ = this.employees.loading$; this.noResults$ = this.employees.noResults$; this.employees$ = this.employees.employees$; } delete(employee: Employee) { this.employees.delete(employee.id); } }
Deci, mergând la browser, ceea ce vom avea acum este:
Și consola va avea următoarea ieșire:
Privind acest lucru, putem spune că Firestore a transmis în flux colecția employees
cu valori goale, iar magazinul employees-page
a fost corectat, setând loading
de la true
la false
.
OK, să creăm formularul pentru a adăuga noi angajați la Firestore:
Formularul pentru angajați
În employees/components/employees-form/employees-form.component.html
vom adăuga acest cod:
<form [formGroup]="form" (ngSubmit)="submit()"> <div class="form-group"> <label for="name">Name</label> <input type="string" class="form-control" formControlName="name" [class.is-invalid]="isInvalid('name')"> <div class="invalid-feedback"> Please enter a Name. </div> </div> <div class="form-group"> <select class="custom-select" formControlName="location" [class.is-invalid]="isInvalid('location')"> <option value="" selected>Choose location</option> <option *ngFor="let loc of locations" [ngValue]="loc">{{loc}}</option> </select> <div class="invalid-feedback"> Please select a Location. </div> </div> <div class="form-group form-check"> <input type="checkbox" class="form-check-input" formControlName="hasDriverLicense"> <label class="form-check-label" for="hasDriverLicense">Has driver license</label> </div> <button [disabled]="form.invalid" type="submit" class="btn btn-primary d-inline">Add</button> <span class="ml-2">{{ status$ | async }}</span> </form>
Codul TypeScript corespunzător va locui în employees/components/employees-form/employees-form.component.ts
:
import { EmployeesService } from './../../services/employees.service'; import { AngularFirestore } from '@angular/fire/firestore'; import { Component, OnInit } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; import { Observable } from 'rxjs'; @Component({ selector: 'app-employees-form', templateUrl: './employees-form.component.html', styleUrls: ['./employees-form.component.scss'] }) export class EmployeesFormComponent implements OnInit { form: FormGroup = new FormGroup({ name: new FormControl('', Validators.required), location: new FormControl('', Validators.required), hasDriverLicense: new FormControl(false) }); locations = [ 'Rosario', 'Buenos Aires', 'Bariloche' ] status$: Observable < string > ; constructor( private employees: EmployeesService ) {} ngOnInit() { this.status$ = this.employees.formStatus$; } isInvalid(name) { return this.form.controls[name].invalid && (this.form.controls[name].dirty || this.form.controls[name].touched) } async submit() { this.form.disable() await this.employees.create({ ...this.form.value }) this.form.reset() this.form.enable() } }
Formularul va apela metoda create()
a EmployeesService
. În acest moment pagina arată așa:
Să aruncăm o privire la ce se întâmplă când adăugăm un nou angajat.
Adăugarea unui nou angajat
După adăugarea unui nou angajat, vom vedea următoarele rezultate pe consolă:
Acestea sunt toate evenimentele care se declanșează la adăugarea unui nou angajat. Să aruncăm o privire mai atentă.
Când apelăm create()
vom executa următorul cod, setând loading=true
, formStatus='Saving...'
și matricea de employees
să se golească ( (1)
în imaginea de mai sus).
this.store.patch({ loading: true, employees: [], formStatus: 'Saving...' }, "employee create") return this.firestore.create(employee).then(_ => { this.store.patch({ formStatus: 'Saved!' }, "employee create SUCCESS") setTimeout(() => this.store.patch({ formStatus: '' }, "employee create timeout reset formStatus"), 2000) }).catch(err => { this.store.patch({ loading: false, formStatus: 'An error ocurred' }, "employee create ERROR") })
Apoi, apelăm serviciul de bază Firestore pentru a crea angajatul, care se înregistrează (4)
. În apelul de promisiune, am setat formStatus='Saved!'
și jurnal (5)
. În cele din urmă, am stabilit un timeout pentru a seta formStatus
înapoi la gol, logging (6)
.
Evenimentele de jurnal (2)
și (3)
sunt evenimentele declanșate de abonamentul Firestore la colecția de angajați. Când EmployeesService
este instanțiat, ne abonam la colecție și primim colecția la fiecare modificare care are loc.
Aceasta setează o nouă stare magazinului cu loading=false
, setând matricea de employees
pentru angajații care vin de la Firestore.
Dacă extindem grupurile de jurnal, vom vedea date detaliate ale fiecărui eveniment și actualizare a magazinului, cu valoarea anterioară și următoarea, ceea ce este util pentru depanare.
Iată cum arată pagina după adăugarea unui nou angajat:
Adăugarea unei componente Rezumat
Să presupunem că acum vrem să afișăm câteva date rezumative pe pagina noastră. Să presupunem că vrem numărul total de angajați, câți sunt șoferi și câți sunt din Rosario.
Vom începe prin a adăuga noile proprietăți de stare la modelul de stare a paginii în employees/states/employees-page.ts
:
// ... export interface EmployeesPage { loading: boolean; employees: Employee[]; formStatus: string; totalEmployees: number; totalDrivers: number; totalRosarioEmployees: number; }
Și le vom inițializa în magazin în employees/services/emplyees-page.store.ts
:
// ... constructor() { super({ loading: true, employees: [], totalDrivers: 0, totalEmployees: 0, totalRosarioEmployees: 0 }) } // ...
În continuare, vom calcula valorile pentru noile proprietăți și vom adăuga selectatorii acestora în EmployeesService
:
// ... this.firestore.collection$().pipe( tap(employees => { this.store.patch({ loading: false, employees, totalEmployees: employees.length, totalDrivers: employees.filter(employee => employee.hasDriverLicense).length, totalRosarioEmployees: employees.filter(employee => employee.location === 'Rosario').length, }, `employees collection subscription`) }) ).subscribe() // ... get totalEmployees$(): Observable < number > { return this.store.state$.pipe(map(state => state.totalEmployees)) } get totalDrivers$(): Observable < number > { return this.store.state$.pipe(map(state => state.totalDrivers)) } get totalRosarioEmployees$(): Observable < number > { return this.store.state$.pipe(map(state => state.totalRosarioEmployees)) } // ...
Acum, să creăm componenta rezumat:
ng gc employees/components/EmployeesSummary
Vom pune acest lucru în employees/components/employees-summary/employees-summary.html
:
<p> <span class="font-weight-bold">Total:</span> {{total$ | async}} <br> <span class="font-weight-bold">Drivers:</span> {{drivers$ | async}} <br> <span class="font-weight-bold">Rosario:</span> {{rosario$ | async}} <br> </p>
Și în employees/components/employees-summary/employees-summary.ts
:
import { Component, OnInit } from '@angular/core'; import { EmployeesService } from '../../services/employees.service'; import { Observable } from 'rxjs'; @Component({ selector: 'app-employees-summary', templateUrl: './employees-summary.component.html', styleUrls: ['./employees-summary.component.scss'] }) export class EmployeesSummaryComponent implements OnInit { total$: Observable < number > ; drivers$: Observable < number > ; rosario$: Observable < number > ; constructor( private employees: EmployeesService ) {} ngOnInit() { this.total$ = this.employees.totalEmployees$; this.drivers$ = this.employees.totalDrivers$; this.rosario$ = this.employees.totalRosarioEmployees$; } }
Vom adăuga apoi componenta la employees/employees-page/employees-page.component.html
:
// ... <div class="col-12 mb-3"> <h4> Employees </h4> <app-employees-summary></app-employees-summary> </div> // ...
Rezultatul este următorul:
În consolă avem:
Serviciul pentru angajați calculează totalul total de totalEmployees
, totalDrivers
de șoferi și totalul de angajați totalRosarioEmployees
pentru fiecare emisie și actualizează starea.
Codul complet al acestui tutorial este disponibil pe GitHub și există și o demonstrație live.
Gestionarea stării aplicației Angular folosind observabile... Verificați!
În acest tutorial, am acoperit o abordare simplă pentru gestionarea stării în aplicațiile Angular folosind un back-end Firebase.
Această abordare se potrivește bine cu liniile directoare unghiulare de utilizare a observabilelor. De asemenea, facilitează depanarea prin furnizarea de urmărire pentru toate actualizările stării aplicației.
Serviciul de magazin generic poate fi folosit și pentru a gestiona starea aplicațiilor care nu folosesc funcțiile Firebase, fie pentru a gestiona numai datele aplicației, fie pentru a fi date de la alte API-uri.
Dar înainte de a aplica acest lucru fără discernământ, un lucru de luat în considerare este că EmployeesService
se abonează la Firestore pe constructor și continuă să asculte în timp ce aplicația este activă. Acest lucru ar putea fi util dacă folosim lista de angajați pe mai multe pagini din aplicație, pentru a evita obținerea de date din Firestore atunci când navigăm între pagini.
Dar aceasta ar putea să nu fie cea mai bună opțiune în alte scenarii, cum ar fi dacă trebuie doar să extrageți valorile inițiale o dată și apoi să declanșați manual reîncărcările de date din Firebase. Concluzia este că este întotdeauna important să înțelegeți cerințele aplicației dvs. pentru a alege metode mai bune de implementare.