Zarządzanie stanem w Angular za pomocą Firebase
Opublikowany: 2022-03-11Zarządzanie stanem jest bardzo ważnym elementem architektury, który należy wziąć pod uwagę podczas tworzenia aplikacji internetowej.
W tym samouczku omówimy proste podejście do zarządzania stanem w aplikacji Angular, która używa Firebase jako swojego zaplecza.
Omówimy niektóre koncepcje, takie jak stan, sklepy i usługi. Mamy nadzieję, że pomoże to lepiej zrozumieć te terminy, a także lepiej zrozumieć inne biblioteki zarządzania stanem, takie jak NgRx i NgXs.
Zbudujemy stronę administracyjną pracownika, aby omówić różne scenariusze zarządzania stanem i podejścia, które mogą je obsłużyć.
Komponenty, usługi, Firestore i zarządzanie stanem w Angular
W typowej aplikacji Angulara mamy komponenty i usługi. Zwykle komponenty będą służyć jako szablon widoku. Usługi będą zawierać logikę biznesową i/lub komunikować się z zewnętrznymi interfejsami API lub innymi usługami w celu wykonania czynności lub pobrania danych.
Komponenty zazwyczaj wyświetlają dane i umożliwiają użytkownikom interakcję z aplikacją w celu wykonywania działań. W tym czasie dane mogą ulec zmianie, a aplikacja odzwierciedla te zmiany, aktualizując widok.
Silnik wykrywania zmian Angulara zajmuje się sprawdzaniem, kiedy wartość w komponencie powiązanym z widokiem uległa zmianie i odpowiednio aktualizuje widok.
Wraz z rozwojem aplikacji zaczniemy mieć coraz więcej komponentów i usług. Często zrozumienie, jak zmieniają się dane i śledzenie, gdzie to się dzieje, może być trudne.
Angular i Firebase
Kiedy używamy Firebase jako naszego zaplecza, otrzymujemy naprawdę zgrabny interfejs API, który zawiera większość operacji i funkcji potrzebnych do zbudowania aplikacji czasu rzeczywistego.
@angular/fire
to oficjalna biblioteka Angular Firebase. Jest to warstwa nałożona na bibliotekę Firebase JavaScript SDK, która upraszcza korzystanie z Firebase SDK w aplikacji Angular. Zapewnia dobre dopasowanie do dobrych praktyk Angulara, takich jak używanie Observables do pobierania i wyświetlania danych z Firebase w naszych komponentach.
Sklepy i stan
Możemy myśleć o „stanie” jako o wartościach wyświetlanych w dowolnym momencie w aplikacji. Sklep jest po prostu właścicielem tego stanu aplikacji.
Stan może być modelowany jako pojedynczy zwykły obiekt lub ich seria, odzwierciedlająca wartości aplikacji.
Przykładowa aplikacja kątowa/Firebase
Zbudujmy to: najpierw utworzymy podstawowe szkieletowe aplikacje przy użyciu interfejsu Angular CLI i połączymy je z projektem 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
A na styles.scss
:
// ... @import "~bootstrap/scss/bootstrap";
Następnie zainstalujemy @angular/fire
:
npm install firebase @angular/fire
Teraz utworzymy projekt Firebase w konsoli Firebase.
Następnie jesteśmy gotowi do stworzenia bazy danych Firestore.
W tym samouczku zacznę w trybie testowym. Jeśli planujesz wydać na produkcję, powinieneś wymusić reguły zabraniające nieodpowiedniego dostępu.
Przejdź do Przegląd projektu → Ustawienia projektu i skopiuj konfigurację internetową Firebase do lokalnych 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>" } };
W tym momencie mamy już podstawowe rusztowanie dla naszej aplikacji. Jeśli ng serve
, otrzymamy:
Klasy bazowe Firestore i Store
Stworzymy dwie ogólne klasy abstrakcyjne, które następnie będziemy pisać i rozszerzać, aby zbudować nasze usługi.
Ogólne pozwalają pisać zachowanie bez typu powiązanego. Zwiększa to możliwość ponownego wykorzystania i elastyczność kodu.
Ogólna usługa Firestore
Aby skorzystać z generyków TypeScript, stworzymy podstawowe opakowanie generyczne dla usługi firestore
@angular/fire
.
Utwórzmy app/core/services/firestore.service.ts
.
Oto kod:
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}`); } }
Ta abstract class
będzie działać jako ogólne opakowanie dla naszych usług Firestore.
To powinno być jedyne miejsce, do którego powinniśmy wstrzyknąć AngularFirestore
. Zminimalizuje to wpływ aktualizacji biblioteki @angular/fire
. Ponadto, jeśli w pewnym momencie będziemy chcieli zmienić bibliotekę, będziemy musieli tylko zaktualizować tę klasę.
Dodałem doc$
, collection$
, create
i delete
. Opakowują metody @angular/fire
i zapewniają rejestrowanie, gdy Firebase przesyła dane strumieniowo — będzie to bardzo przydatne podczas debugowania — oraz po utworzeniu lub usunięciu obiektu.
Ogólna usługa sklepu
Nasza ogólna usługa sklepu zostanie zbudowana przy użyciu BehaviorSubject
RxJS. BehaviorSubject
pozwala subskrybentom uzyskać ostatnią wyemitowaną wartość, gdy tylko zasubskrybują. W naszym przypadku jest to pomocne, ponieważ będziemy mogli rozpocząć sklep z początkową wartością dla wszystkich naszych komponentów, gdy zasubskrybują sklep.
Sklep będzie miał dwie metody, patch
i set
. (Stworzymy metody get
później.)
Stwórzmy 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) } }
Jako klasa generyczna odłożymy pisanie, dopóki nie zostanie odpowiednio rozszerzona.
Konstruktor otrzyma początkową wartość typu Partial<T>
. To pozwoli nam zastosować wartości tylko do niektórych właściwości stanu. Konstruktor zasubskrybuje również wewnętrzne emisje BehaviorSubject
i będzie aktualizował stan wewnętrzny po każdej zmianie.
patch()
otrzyma newValue
typu Partial<T>
i połączy ją z bieżącą wartością this.state
sklepu. Na koniec wykonujemy next()
newState
i emitujemy nowy stan do wszystkich subskrybentów sklepu.
set()
działa bardzo podobnie, z tą różnicą, że zamiast łatać wartość stanu, ustawi ją na newValue
wartość, którą otrzymała.
W miarę pojawiania się zmian będziemy rejestrować poprzednie i następne wartości stanu, co pomoże nam debugować i łatwo śledzić zmiany stanu.
Kładąc wszystko razem
Dobra, zobaczmy to wszystko w akcji. Utworzymy stronę pracowników, która będzie zawierała listę pracowników oraz formularz dodawania nowych pracowników.
Zaktualizujmy app.component.html
, aby dodać prosty pasek nawigacyjny:
<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>
Następnie utworzymy moduł Core:
ng gm Core
W core/core.module.ts
dodamy moduły wymagane dla naszej aplikacji:
// ... 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 { }
Teraz utwórzmy stronę pracowników, zaczynając od modułu Pracownicy:
ng gm Employees --routing
W employees-routing.module.ts
dodajmy trasę dla employees
:
// ... import { EmployeesPageComponent } from './components/employees-page/employees-page.component'; // ... const routes: Routes = [ { path: 'employees', component: EmployeesPageComponent } ]; // ...
A w employees.module.ts
zaimportujemy ReactiveFormsModule
:
// ... import { ReactiveFormsModule } from '@angular/forms'; // ... @NgModule({ // ... imports: [ // ... ReactiveFormsModule ] }) export class EmployeesModule { }
Teraz dodajmy te dwa moduły w pliku app.module.ts
:
// ... import { EmployeesModule } from './employees/employees.module'; import { CoreModule } from './core/core.module'; imports: [ // ... CoreModule, EmployeesModule ],
Na koniec utwórzmy rzeczywiste komponenty strony naszych pracowników oraz odpowiedni model, usługę, sklep i stan.
ng gc employees/components/EmployeesPage ng gc employees/components/EmployeesList ng gc employees/components/EmployeesForm
Do naszego modelu potrzebujemy pliku o nazwie models/employee.ts
:
export interface Employee { id: string; name: string; location: string; hasDriverLicense: boolean; }
Nasza usługa będzie znajdować się w pliku o nazwie employees/services/employee.firestore.ts
. Ta usługa rozszerzy ogólną FirestoreService<T>
utworzoną wcześniej, a my po prostu ustawimy basePath
kolekcji Firestore:
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'; }
Następnie utworzymy plik employees/states/employees-page.ts
. Będzie to służyć jako stan strony pracowników:
import { Employee } from '../models/employee'; export interface EmployeesPage { loading: boolean; employees: Employee[]; formStatus: string; }
Stan będzie miał wartość loading
, która określa czy wyświetlać komunikat wczytywania na stronie, samych employees
oraz zmienną formStatus
do obsługi stanu formularza (np. Saving
lub Saved
).
Będziemy potrzebować pliku na employees/services/employees-page.store.ts
. Tutaj rozszerzymy utworzony wcześniej StoreService<T>
. Ustawimy nazwę sklepu, która będzie używana do jego identyfikacji podczas debugowania.
Ta usługa zainicjuje i utrzyma stan strony pracowników. Zauważ, że konstruktor wywołuje super()
z początkowym stanem strony. W takim przypadku zainicjujemy stan z loading=true
i pustą tablicą pracowników.
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: [], }) } }
Teraz stwórzmy EmployeesService
, aby zintegrować EmployeeFirestore
i EmployeesPageStore
:
ng gs employees/services/Employees
Pamiętaj, że w tej usłudze wstrzykujemy EmployeeFirestore
i EmployeesPageStore
. Oznacza to, że EmployeesService
będzie zawierać i koordynować wywołania Firestore i sklepu w celu aktualizacji stanu. Pomoże nam to stworzyć pojedynczy interfejs API do wywoływania komponentów.
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") }) } }
Przyjrzyjmy się, jak będzie działać usługa.

W konstruktorze zasubskrybujemy kolekcję pracowników Firestore. Gdy tylko Firestore wyemituje dane z kolekcji, zaktualizujemy sklep, ustawiając loading=false
i employees
ze zwróconą kolekcją Firestore. Ponieważ wstrzyknęliśmy EmployeeFirestore
, obiekty zwrócone z Firestore są wpisywane do Employee
, co umożliwia korzystanie z większej liczby funkcji IntelliSense.
Ta subskrypcja będzie aktywna, gdy aplikacja będzie aktywna, nasłuchując wszystkich zmian i aktualizując sklep za każdym razem, gdy Firestore przesyła dane strumieniowo.
this.firestore.collection$().pipe( tap(employees => { this.store.patch({ loading: false, employees, }, `employees collection subscription`) }) ).subscribe()
Funkcje employees$()
i loading$()
wybiorą element stanu, którego chcemy później użyć w komponencie. employees$()
zwróci pustą tablicę podczas ładowania stanu. Umożliwi nam to wyświetlanie odpowiednich komunikatów w widoku.
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)) }
Ok, więc teraz mamy gotowe wszystkie usługi i możemy zbudować nasze komponenty widoku. Ale zanim to zrobimy, szybkie odświeżenie może się przydać…
Obserwable RxJs i potok async
Obserwable pozwalają subskrybentom odbierać emisje danych jako strumień. To, w połączeniu z rurą async
, może być bardzo potężne.
async
zajmuje się subskrybowaniem obserwowalnego i aktualizowaniem widoku, gdy są emitowane nowe dane. Co ważniejsze, automatycznie anuluje subskrypcję, gdy komponent zostanie zniszczony, chroniąc nas przed wyciekami pamięci.
Możesz przeczytać więcej o bibliotekach Observables i RxJs ogólnie w oficjalnych dokumentach.
Tworzenie komponentów widoku
W employees/components/employees-page/employees-page.component.html
umieścimy ten kod:
<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>
Podobnie, employees/components/employees-list/employees-list.component.html
będą to mieli przy użyciu wspomnianej powyżej techniki potoków async
:
<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>
Ale w tym przypadku będziemy również potrzebować kodu TypeScript dla składnika. Plik employees/components/employees-list/employees-list.component.ts
będą potrzebować tego:
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); } }
Przechodząc do przeglądarki, teraz będziemy mieli:
A konsola będzie miała następujące dane wyjściowe:
Patrząc na to, możemy stwierdzić, że Firestore przesyłał strumieniowo kolekcję employees
z pustymi wartościami, a sklep employees-page
został załatany, ustawiając loading
z true
na false
.
OK, zbudujmy formularz, aby dodać nowych pracowników do Firestore:
Formularz dla pracowników
W employees/components/employees-form/employees-form.component.html
dodamy ten kod:
<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>
Odpowiadający kod TypeScript będzie dostępny w 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() } }
Formularz wywoła metodę create()
usługi EmployeesService
. W tej chwili strona wygląda tak:
Przyjrzyjmy się, co się dzieje, gdy dodajemy nowego pracownika.
Dodawanie nowego pracownika
Po dodaniu nowego pracownika zobaczymy, że na konsolę trafiają następujące dane:
Są to wszystkie zdarzenia, które są wyzwalane podczas dodawania nowego pracownika. Przyjrzyjmy się bliżej.
Gdy wywołamy metodę create()
, wykonamy następujący kod, ustawiając loading=true
, formStatus='Saving...'
i tablicę employees
na pustą ( (1)
na powyższym obrazku).
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") })
Następnie wywołujemy bazową usługę Firestore w celu utworzenia pracownika, który loguje się (4)
. W wywołaniu zwrotnym obietnicy ustawiamy formStatus='Saved!'
i log (5)
. Na koniec ustawiamy limit czasu, aby ustawić formStatus
z powrotem na pusty, logowanie (6)
.
Zdarzenia dziennika (2)
i (3)
to zdarzenia wywoływane przez subskrypcję Firestore do kolekcji pracowników. Po utworzeniu wystąpienia usługi EmployeesService
subskrybujemy kolekcję i otrzymujemy kolekcję po każdej zachodzącej zmianie.
To ustawia nowy stan sklepu z loading=false
, ustawiając tablicę employees
na pracowników pochodzących z Firestore.
Jeśli rozszerzymy grupy logów, zobaczymy szczegółowe dane o każdym zdarzeniu i aktualizacji sklepu, z poprzednią wartością i następną, co jest przydatne przy debugowaniu.
Tak wygląda strona po dodaniu nowego pracownika:
Dodawanie komponentu podsumowania
Załóżmy, że chcemy teraz wyświetlić na naszej stronie kilka danych podsumowujących. Powiedzmy, że chcemy mieć całkowitą liczbę pracowników, ilu to kierowcy, a ilu pochodzi z Rosario.
Zaczniemy od dodania nowych właściwości stanu do modelu stanu strony w employees/states/employees-page.ts
:
// ... export interface EmployeesPage { loading: boolean; employees: Employee[]; formStatus: string; totalEmployees: number; totalDrivers: number; totalRosarioEmployees: number; }
A my zainicjujemy je w sklepie w employees/services/emplyees-page.store.ts
:
// ... constructor() { super({ loading: true, employees: [], totalDrivers: 0, totalEmployees: 0, totalRosarioEmployees: 0 }) } // ...
Następnie obliczymy wartości dla nowych właściwości i dodamy ich odpowiednie selektory w 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)) } // ...
Teraz utwórzmy komponent podsumowujący:
ng gc employees/components/EmployeesSummary
Umieścimy to w 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>
A w 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$; } }
Następnie dodamy komponent do employees/employees-page/employees-page.component.html
:
// ... <div class="col-12 mb-3"> <h4> Employees </h4> <app-employees-summary></app-employees-summary> </div> // ...
Wynik jest następujący:
W konsoli mamy:
Usługa Pracownicy oblicza sumę totalEmployees
, totalDrivers
i totalRosarioEmployees
dla każdej emisji i aktualizuje stan.
Pełny kod tego samouczka jest dostępny w serwisie GitHub, a także dostępne jest demo na żywo.
Zarządzanie stanem aplikacji Angular za pomocą obserwacji… Sprawdź!
W tym samouczku omówiliśmy proste podejście do zarządzania stanem w aplikacjach Angular przy użyciu zaplecza Firebase.
To podejście dobrze pasuje do wytycznych Angulara dotyczących używania Observables. Ułatwia również debugowanie, zapewniając śledzenie wszystkich aktualizacji stanu aplikacji.
Ogólna usługa sklepu może być również używana do zarządzania stanem aplikacji, które nie korzystają z funkcji Firebase, do zarządzania tylko danymi aplikacji lub danymi pochodzącymi z innych interfejsów API.
Ale zanim zaczniesz stosować to bezkrytycznie, jedną rzeczą do rozważenia jest to, że EmployeesService
subskrybuje Firestore na konstruktorze i nasłuchuje, gdy aplikacja jest aktywna. Może to być przydatne, jeśli używamy listy pracowników na wielu stronach w aplikacji, aby uniknąć pobierania danych z Firestore podczas nawigowania między stronami.
Ale może to nie być najlepsza opcja w innych scenariuszach, na przykład gdy trzeba tylko raz pobrać początkowe wartości, a następnie ręcznie uruchomić ponowne ładowanie danych z Firebase. Najważniejsze jest to, że zawsze ważne jest zrozumienie wymagań aplikacji, aby wybrać lepsze metody implementacji.