Управление состоянием в Angular с использованием Firebase
Опубликовано: 2022-03-11Управление состоянием — очень важная часть архитектуры, которую следует учитывать при разработке веб-приложения.
В этом руководстве мы рассмотрим простой подход к управлению состоянием в приложении Angular, использующем Firebase в качестве серверной части.
Мы рассмотрим некоторые понятия, такие как состояние, магазины и службы. Надеюсь, это поможет вам лучше понять эти термины, а также лучше понять другие библиотеки управления состоянием, такие как NgRx и NgXs.
Мы создадим страницу администратора для сотрудников, чтобы охватить несколько различных сценариев управления состоянием и подходы, которые могут с ними справиться.
Компоненты, службы, Firestore и управление состоянием в Angular
В типичном приложении Angular у нас есть компоненты и сервисы. Обычно компоненты служат шаблоном представления. Службы будут содержать бизнес-логику и/или связываться с внешними API или другими службами для выполнения действий или получения данных.
Компоненты обычно отображают данные и позволяют пользователям взаимодействовать с приложением для выполнения действий. При этом данные могут измениться, и приложение отражает эти изменения, обновляя представление.
Механизм обнаружения изменений Angular проверяет, когда значение в компоненте, привязанном к представлению, изменилось, и соответствующим образом обновляет представление.
По мере роста приложения у нас будет все больше и больше компонентов и сервисов. Часто бывает сложно понять, как меняются данные, и отследить, где это происходит.
Угловой и Firebase
Когда мы используем Firebase в качестве серверной части, нам предоставляется действительно удобный API, который содержит большинство операций и функций, необходимых для создания приложения реального времени.
@angular/fire
— официальная библиотека Angular Firebase. Это слой поверх библиотеки Firebase JavaScript SDK, который упрощает использование Firebase SDK в приложении Angular. Он хорошо сочетается с передовыми практиками Angular, такими как использование Observables для получения и отображения данных из Firebase в наши компоненты.
Магазины и государство
Мы можем думать о «состоянии» как о значениях, отображаемых в любой момент времени в приложении. Магазин — это просто держатель этого состояния приложения.
Состояние можно смоделировать как один простой объект или их серию, отражающую значения приложения.
Образец приложения Angular/Firebase
Давайте создадим его: во-первых, мы создадим базовый каркас приложения с помощью Angular CLI и соединим его с проектом 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
И в styles.scss
:
// ... @import "~bootstrap/scss/bootstrap";
Далее мы установим @angular/fire
:
npm install firebase @angular/fire
Теперь мы создадим проект Firebase в консоли Firebase.
Теперь мы готовы создать базу данных Firestore.
Для этого урока я начну в тестовом режиме. Если вы планируете выпускать продукт в производство, вам следует ввести правила, запрещающие несанкционированный доступ.
Перейдите в Обзор проекта → Настройки проекта и скопируйте веб-конфигурацию Firebase в локальную 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>" } };
На данный момент у нас есть базовый каркас для нашего приложения. Если мы ng serve
, мы получим:
Базовые классы Firestore и Store
Мы создадим два общих абстрактных класса, которые мы затем напечатаем и расширим для создания наших сервисов.
Обобщения позволяют вам писать поведение без связанного типа. Это добавляет вашему коду возможности повторного использования и гибкости.
Общий сервис Firestore
Чтобы воспользоваться дженериками TypeScript, мы создадим базовую универсальную оболочку для службы @angular/fire
firestore
.
Давайте создадим app/core/services/firestore.service.ts
.
Вот код:
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}`); } }
Этот abstract class
будет работать как универсальная оболочка для наших сервисов Firestore.
Это должно быть единственное место, куда мы должны внедрить AngularFirestore
. Это сведет к минимуму влияние обновления библиотеки @angular/fire
. Также, если в какой-то момент мы захотим изменить библиотеку, нам нужно будет обновить только этот класс.
Я добавил doc$
, collection$
, create
и delete
. Они обертывают методы @angular/fire
и обеспечивают ведение журнала при потоковой передаче данных Firebase — это станет очень удобным для отладки — и после создания или удаления объекта.
Общий сервис магазина
Наш общий сервис хранилища будет построен с использованием BehaviorSubject
RxJS. BehaviorSubject
позволяет подписчикам получать последнее отправленное значение сразу после подписки. В нашем случае это полезно, потому что мы сможем начать хранилище с начальным значением для всех наших компонентов, когда они подпишутся на хранилище.
У хранилища будет два метода: patch
и set
. (Мы создадим методы get
позже.)
Давайте создадим 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) } }
Как общий класс, мы отложим ввод до тех пор, пока он не будет должным образом расширен.
Конструктор получит начальное значение типа Partial<T>
. Это позволит нам применять значения только к некоторым свойствам состояния. Конструктор также подпишется на внутренние выбросы BehaviorSubject
и обновит внутреннее состояние после каждого изменения.
patch()
получит newValue
типа Partial<T>
и объединит его с текущим значением this.state
хранилища. Наконец, мы next()
newState
и передаем новое состояние всем подписчикам магазина.
set()
работает очень похоже, за исключением того, что вместо того, чтобы исправлять значение состояния, оно устанавливает полученное newValue
.
Мы будем регистрировать предыдущие и следующие значения состояния по мере возникновения изменений, что поможет нам отлаживать и легко отслеживать изменения состояния.
Собираем все вместе
Хорошо, давайте посмотрим все это в действии. Что мы сделаем, так это создадим страницу сотрудников, которая будет содержать список сотрудников, а также форму для добавления новых сотрудников.
Давайте обновим app.component.html
, чтобы добавить простую панель навигации:
<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>
Далее мы создадим основной модуль:
ng gm Core
В core/core.module.ts
мы добавим модули, необходимые для нашего приложения:
// ... 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 { }
Теперь давайте создадим страницу сотрудников, начиная с модуля «Сотрудники»:
ng gm Employees --routing
В employees-routing.module.ts
добавим маршрут employees
:
// ... import { EmployeesPageComponent } from './components/employees-page/employees-page.component'; // ... const routes: Routes = [ { path: 'employees', component: EmployeesPageComponent } ]; // ...
А в employees.module.ts
мы импортируем ReactiveFormsModule
:
// ... import { ReactiveFormsModule } from '@angular/forms'; // ... @NgModule({ // ... imports: [ // ... ReactiveFormsModule ] }) export class EmployeesModule { }
Теперь добавим эти два модуля в файл app.module.ts
:
// ... import { EmployeesModule } from './employees/employees.module'; import { CoreModule } from './core/core.module'; imports: [ // ... CoreModule, EmployeesModule ],
Наконец, давайте создадим фактические компоненты нашей страницы сотрудников, а также соответствующую модель, службу, хранилище и состояние.
ng gc employees/components/EmployeesPage ng gc employees/components/EmployeesList ng gc employees/components/EmployeesForm
Для нашей модели нам понадобится файл с именем models/employee.ts
:
export interface Employee { id: string; name: string; location: string; hasDriverLicense: boolean; }
Наш сервис будет находиться в файле с именем employees/services/employee.firestore.ts
. Эта служба расширит общий FirestoreService<T>
, созданный ранее, и мы просто установим basePath
коллекции 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'; }
Затем мы создадим файл employees/states/employees-page.ts
. Это будет служить состоянием страницы сотрудников:
import { Employee } from '../models/employee'; export interface EmployeesPage { loading: boolean; employees: Employee[]; formStatus: string; }
Состояние будет иметь значение loading
, которое определяет, отображать ли сообщение о загрузке на странице, самих employees
и переменную formStatus
для обработки состояния формы (например, Saving
или Saved
).
Нам понадобится файл по адресу employees/services/employees-page.store.ts
. Здесь мы расширим StoreService<T>
созданный ранее. Мы зададим имя хранилища, которое будет использоваться для его идентификации при отладке.
Эта служба будет инициализировать и хранить состояние страницы сотрудников. Обратите внимание, что конструктор вызывает super()
с начальным состоянием страницы. В этом случае мы инициализируем состояние с loading=true
и пустым массивом сотрудников.
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: [], }) } }
Теперь давайте создадим EmployeesService
для интеграции EmployeeFirestore
и EmployeesPageStore
:
ng gs employees/services/Employees
Обратите внимание, что мы внедряем в эту службу EmployeeFirestore
и EmployeesPageStore
. Это означает, что EmployeesService
будет содержать и координировать вызовы Firestore и магазина для обновления состояния. Это поможет нам создать единый API для вызова компонентов.
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") }) } }
Давайте посмотрим, как будет работать сервис.

В конструкторе мы подпишемся на коллекцию сотрудников Firestore. Как только Firestore отправит данные из коллекции, мы обновим хранилище, установив loading=false
и employees
с возвращенной коллекцией Firestore. Поскольку мы внедрили EmployeeFirestore
, объекты, возвращаемые из Firestore, типизируются как Employee
, что позволяет использовать больше функций IntelliSense.
Эта подписка будет активна, пока приложение активно, прослушивает все изменения и обновляет хранилище каждый раз, когда Firestore передает данные.
this.firestore.collection$().pipe( tap(employees => { this.store.patch({ loading: false, employees, }, `employees collection subscription`) }) ).subscribe()
Функции employees$()
и loading$()
выберут часть состояния, которую мы хотим позже использовать в компоненте. employees$()
вернет пустой массив при загрузке состояния. Это позволит нам отображать правильные сообщения в представлении.
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)) }
Итак, теперь у нас есть готовые сервисы, и мы можем создавать наши компоненты представления. Но прежде чем мы это сделаем, может пригодиться краткое освежение…
RxJs Observables и async
канал
Observables позволяют подписчикам получать выбросы данных в виде потока. Это, в сочетании с async
каналом, может быть очень мощным.
async
канал заботится о подписке на Observable и обновлении представления при отправке новых данных. Что еще более важно, он автоматически отписывается при уничтожении компонента, защищая нас от утечек памяти.
Вы можете прочитать больше о Observables и библиотеке RxJs в целом в официальной документации.
Создание компонентов представления
В employees/components/employees-page/employees-page.component.html
мы поместим этот код:
<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>
Точно так же employees/components/employees-list/employees-list.component.html
будет это с использованием техники 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>
Но в этом случае нам также понадобится код TypeScript для компонента. employees/components/employees-list/employees-list.component.ts
понадобится это:
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); } }
Итак, переходим в браузер, что у нас сейчас будет:
И консоль будет иметь следующий вывод:
Глядя на это, мы можем сказать, что Firestore передал коллекцию employees
с пустыми значениями, а хранилище employees-page
было исправлено, установив loading
с true
на false
.
Хорошо, давайте создадим форму для добавления новых сотрудников в Firestore:
Форма сотрудников
В employees/components/employees-form/employees-form.component.html
мы добавим этот код:
<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>
Соответствующий код TypeScript будет 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() } }
Форма вызовет метод create()
службы EmployeesService
. Сейчас страница выглядит так:
Давайте посмотрим, что происходит, когда мы добавляем нового сотрудника.
Добавление нового сотрудника
После добавления нового сотрудника мы увидим следующее:
Это все события, которые запускаются при добавлении нового сотрудника. Давайте посмотрим поближе.
Когда мы вызовем create()
, мы выполним следующий код, установив loading=true
, formStatus='Saving...'
и пустое значение массива employees
( (1)
на изображении выше).
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") })
Затем мы вызываем базовую службу Firestore для создания сотрудника, который регистрирует (4)
. В обратном вызове обещания мы устанавливаем formStatus='Saved!'
и журнал (5)
. Наконец, мы устанавливаем тайм-аут, чтобы вернуть formStatus
в пустое состояние, logging (6)
.
События журнала (2)
и (3)
— это события, инициированные подпиской Firestore на коллекцию сотрудников. Когда создается экземпляр EmployeesService
, мы подписываемся на коллекцию и получаем ее при каждом изменении, которое происходит.
Это устанавливает новое состояние для магазина с loading=false
, устанавливая массив employees
для сотрудников, поступающих из Firestore.
Если мы развернем группы журналов, мы увидим подробные данные о каждом событии и обновлении хранилища с предыдущим значением и следующим, что полезно для отладки.
Вот так выглядит страница после добавления нового сотрудника:
Добавление сводного компонента
Допустим, теперь мы хотим отобразить некоторые сводные данные на нашей странице. Допустим, нам нужно общее количество сотрудников, сколько водителей и сколько из Росарио.
Мы начнем с добавления новых свойств состояния в модель состояния страницы в employees/states/employees-page.ts
:
// ... export interface EmployeesPage { loading: boolean; employees: Employee[]; formStatus: string; totalEmployees: number; totalDrivers: number; totalRosarioEmployees: number; }
И инициализируем их в магазине в employees/services/emplyees-page.store.ts
:
// ... constructor() { super({ loading: true, employees: [], totalDrivers: 0, totalEmployees: 0, totalRosarioEmployees: 0 }) } // ...
Далее мы вычислим значения для новых свойств и добавим соответствующие селекторы в 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)) } // ...
Теперь давайте создадим компонент сводки:
ng gc employees/components/EmployeesSummary
Мы поместим это в 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>
И в 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$; } }
Затем мы добавим компонент в employees/employees-page/employees-page.component.html
:
// ... <div class="col-12 mb-3"> <h4> Employees </h4> <app-employees-summary></app-employees-summary> </div> // ...
Результат следующий:
В консоли имеем:
Служба сотрудников вычисляет общее количество totalEmployees
, totalDrivers
и totalRosarioEmployees
для каждой эмиссии и обновляет состояние.
Полный код этого руководства доступен на GitHub, а также есть живая демонстрация.
Управление состоянием приложения Angular с помощью Observables… Проверьте!
В этом руководстве мы рассмотрели простой подход к управлению состоянием в приложениях Angular с использованием серверной части Firebase.
Этот подход хорошо согласуется с рекомендациями Angular по использованию Observables. Это также упрощает отладку, обеспечивая отслеживание всех обновлений состояния приложения.
Универсальный сервис хранилища также можно использовать для управления состоянием приложений, которые не используют функции Firebase, либо для управления только данными приложения, либо данными, поступающими из других API.
Но прежде чем вы начнете применять это без разбора, нужно учитывать, что EmployeesService
подписывается на Firestore в конструкторе и продолжает прослушивать, пока приложение активно. Это может быть полезно, если мы используем список сотрудников на нескольких страницах приложения, чтобы избежать получения данных из Firestore при переходе между страницами.
Но это может быть не лучшим вариантом в других сценариях, например, если вам просто нужно один раз получить начальные значения, а затем вручную запустить перезагрузку данных из Firebase. Суть в том, что всегда важно понимать требования вашего приложения, чтобы выбрать лучшие методы реализации.