Управление состоянием в Angular с использованием Firebase

Опубликовано: 2022-03-11

Управление состоянием — очень важная часть архитектуры, которую следует учитывать при разработке веб-приложения.

В этом руководстве мы рассмотрим простой подход к управлению состоянием в приложении Angular, использующем Firebase в качестве серверной части.

Мы рассмотрим некоторые понятия, такие как состояние, магазины и службы. Надеюсь, это поможет вам лучше понять эти термины, а также лучше понять другие библиотеки управления состоянием, такие как NgRx и NgXs.

Мы создадим страницу администратора для сотрудников, чтобы охватить несколько различных сценариев управления состоянием и подходы, которые могут с ними справиться.

Компоненты, службы, Firestore и управление состоянием в Angular

В типичном приложении Angular у нас есть компоненты и сервисы. Обычно компоненты служат шаблоном представления. Службы будут содержать бизнес-логику и/или связываться с внешними API или другими службами для выполнения действий или получения данных.

Компоненты подключаются к службам, которые подключаются к другим службам или HTTP API.

Компоненты обычно отображают данные и позволяют пользователям взаимодействовать с приложением для выполнения действий. При этом данные могут измениться, и приложение отражает эти изменения, обновляя представление.

Механизм обнаружения изменений Angular проверяет, когда значение в компоненте, привязанном к представлению, изменилось, и соответствующим образом обновляет представление.

По мере роста приложения у нас будет все больше и больше компонентов и сервисов. Часто бывает сложно понять, как меняются данные, и отследить, где это происходит.

Угловой и Firebase

Когда мы используем Firebase в качестве серверной части, нам предоставляется действительно удобный API, который содержит большинство операций и функций, необходимых для создания приложения реального времени.

@angular/fire — официальная библиотека Angular Firebase. Это слой поверх библиотеки Firebase JavaScript SDK, который упрощает использование Firebase SDK в приложении Angular. Он хорошо сочетается с передовыми практиками Angular, такими как использование Observables для получения и отображения данных из Firebase в наши компоненты.

Компоненты подписываются на Firebase JavaScript API через @angular/fire с использованием Observables.

Магазины и государство

Мы можем думать о «состоянии» как о значениях, отображаемых в любой момент времени в приложении. Магазин — это просто держатель этого состояния приложения.

Состояние можно смоделировать как один простой объект или их серию, отражающую значения приложения.

Состояние хранилища, в котором есть пример объекта с несколькими простыми парами "ключ-значение" для имени, города и страны.

Образец приложения 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.

Диалоговое окно «Добавить проект» в консоли Firebase.

Теперь мы готовы создать базу данных Firestore.

Для этого урока я начну в тестовом режиме. Если вы планируете выпускать продукт в производство, вам следует ввести правила, запрещающие несанкционированный доступ.

Диалоговое окно «Правила безопасности для Cloud Firestore» с выбранным «Запустить в тестовом режиме» вместо «Запустить в заблокированном режиме».

Перейдите в Обзор проекта → Настройки проекта и скопируйте веб-конфигурацию Firebase в локальную environments/environment.ts .

Пустой список приложений для нового проекта Firebase.

 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 , мы получим:

Леска Angular с надписью "Добро пожаловать в сотрудники-админы!"

Базовые классы 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 . Сейчас страница выглядит так:

Тот же пустой список сотрудников, что и раньше, на этот раз с формой для добавления нового сотрудника.

Давайте посмотрим, что происходит, когда мы добавляем нового сотрудника.

Добавление нового сотрудника

После добавления нового сотрудника мы увидим следующее:

События исправления, смешанные с событиями Firestore, пронумерованными от первого до шести (локальное создание, потоковая передача коллекции Firestore, подписка на локальную коллекцию, создание Firestore, успешное локальное создание и сброс состояния формы тайм-аута локального создания).

Это все события, которые запускаются при добавлении нового сотрудника. Давайте посмотрим поближе.

Когда мы вызовем 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. Суть в том, что всегда важно понимать требования вашего приложения, чтобы выбрать лучшие методы реализации.