Държавното управление е много важна част от архитектурата, която трябва да се има предвид при разработването на уеб приложение.
В този урок ще разгледаме един прост подход за управление на състоянието в Ъглово приложение който използва Firebase като задния му край.
Ще разгледаме някои понятия като държава, магазини и услуги. Надяваме се, че това ще ви помогне да разберете по-добре тези термини и също така да разберете по-добре други библиотеки за управление на държавата като NgRx и NgXs.
Ще създадем страница за администратор на служител, за да обхванем някои различни сценарии за управление на състоянието и подходите, които могат да се справят с тях.
На типичен Ъглова приложение имаме компоненти и услуги. Обикновено компонентите ще служат като шаблон за изглед. Услугите ще съдържат бизнес логика и / или комуникират с външни API или други услуги за извършване на действия или извличане на данни.
Компонентите обикновено показват данни и позволяват на потребителите да взаимодействат с приложението, за да изпълняват действия. Докато правите това, данните могат да се променят и приложението отразява тези промени, като актуализира изгледа.
Двигателят за откриване на промени на Angular се грижи за проверка, когато дадена стойност в компонент, обвързан с изгледа, се е променил и актуализира изгледа съответно.
С развитието на приложението ще започнем да имаме все повече и повече компоненти и услуги. Често разбирането как се променят данните и проследяването на това, което се случва, може да бъде сложно.
Когато използваме Firebase като наш заден край, ние сме снабдени с наистина чист API, който съдържа повечето от операциите и функционалността, от които се нуждаем, за да изградим приложение в реално време.
@angular/fire
е официалната библиотека Angular Firebase. Това е слой върху библиотеката на SDK на Firebase JavaScript, който опростява използването на Firebase SDK в приложение Angular. Той осигурява добро съответствие с добрите практики на Angular, като например използването на Observables за получаване и показване на данни от 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: '', authDomain: '', databaseURL: '', projectId: '', storageBucket: '', messagingSenderId: '' } };
На този етап разполагаме с основното скеле за нашето приложение. Ако ng serve
, ще получим:
Android работи на фонова нишка
Ще създадем два родови абстрактни класа, които след това ще напишем и разширим, за да изградим нашите услуги.
Дженерици ви позволява да пишете поведение без обвързан тип. Това добавя многократна употреба и гъвкавост към вашия код.
За да се възползваме от TypeScript generics, това, което ще направим, е да създадем основна обща обвивка за @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 { protected abstract basePath: string; constructor( @Inject(AngularFirestore) protected firestore: AngularFirestore, ) { } doc$(id: string): Observable { return this.firestore.doc(`${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 { return this.firestore.collection(`${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 излъчва данни - това ще стане много удобно за отстраняване на грешки - и след като обектът бъде създаден или изтрит.
Нашата обща услуга за магазини ще бъде изградена с помощта на RxJS ’BehaviorSubject
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 { protected bs: BehaviorSubject; state$: Observable; state: T; previous: T; protected abstract store: string; constructor(initialValue: Partial) { this.bs = new BehaviorSubject(initialValue as T); this.state$ = this.bs.asObservable(); this.state = initialValue as T; this.state$.subscribe(s => { this.state = s }) } patch(newValue: Partial, 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, 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
. Това ще ни позволи да прилагаме стойности само към някои свойства на състоянието. Конструкторът също ще се абонира за вътрешния BehaviorSubject
емисии и поддържайте вътрешното състояние актуализирано след всяка промяна.
patch()
ще получи newValue
от тип Partial
и ще го обедини с текущия this.state
стойност на магазина. И накрая, ние next()
newState
и излъчва новото състояние на всички абонати на магазина.
set()
работи много подобно, само че вместо да коригира стойността на състоянието, тя ще я зададе на newValue
то получи.
Ще регистрираме предишните и следващите стойности на състоянието, когато настъпят промени, което ще ни помогне да отстраним грешките и лесно да проследяваме промените в състоянието.
Добре, нека видим всичко това в действие. Това, което ще направим, е да създадем страница на служителите, която ще съдържа списък със служители, както и формуляр за добавяне на нови служители.
Нека актуализираме app.component.html
за да добавите проста навигационна лента:
След това ще създадем основен модул:
ng g m 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 g m 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 g c employees/components/EmployeesPage ng g c employees/components/EmployeesList ng g c employees/components/EmployeesForm
За нашия модел ще ни трябва файл models/employee.ts
:
export interface Employee { id: string; name: string; location: string; hasDriverLicense: boolean; }
Нашата услуга ще работи във файл, наречен employees/services/employee.firestore.ts
. Тази услуга ще разшири общия FirestoreService
създадени преди и ние просто ще зададем 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 { 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
създадени преди. Ще зададем името на магазина, което ще се използва за идентифициране при отстраняване на грешки.
Тази услуга ще инициализира и задържи състоянието на страницата на служителите. Имайте предвид, че конструкторът извиква 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 { protected store: string = 'employees-page'; constructor() { super({ loading: true, employees: [], }) } }
Сега нека създадем EmployeesService
за интегриране EmployeeFirestore
и EmployeesPageStore
:
ng g s 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 { return this.store.state$.pipe(map(state => state.loading ? [] : state.employees)) } get loading$(): Observable { return this.store.state$.pipe(map(state => state.loading)) } get noResults$(): Observable { return this.store.state$.pipe( map(state => { return !state.loading && state.employees && state.employees.length === 0 }) ) } get formStatus$(): Observable { 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 { return this.store.state$.pipe(map(state => state.loading ? [] : state.employees)) } get loading$(): Observable { return this.store.state$.pipe(map(state => state.loading)) }
Добре, така че вече имаме всички услуги готови и можем да изградим компонентите си за изглед. Но преди да направим това, бързо опресняване може да е полезно ...
async
ТръбаНаблюдателните позволяват на абонатите да получават емисии на данни като поток. Това, в комбинация с async
тръба, може много мощен.
async
pipe се грижи за абониране за Observable и актуализиране на изгледа при излъчване на нови данни. По-важното е, че той автоматично се отписва, когато компонентът е унищожен, предпазвайки ни от изтичане на памет.
Можете да прочетете повече за Observables и RxJs библиотеката като цяло в официалните документи .
В employees/components/employees-page/employees-page.component.html
ще поставим този код:
Employees
По същия начин, employees/components/employees-list/employees-list.component.html
ще има това, като използва async
тръбна техника, спомената по-горе:
console.log не е функция
Loading... No results {{employee.location}} {{employee.name}}
{{employee.hasDriverLicense ? 'Can drive': ''}}
Delete
Но в този случай ще ни е необходим и 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; employees$: Observable; noResults$: Observable; 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
ще добавим този код:
Name Please enter a Name. Choose location {{loc}} Please select a Location. Has driver license Add { status$ }
Съответният код на 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 ; constructor( private employees: EmployeesService ) {} ngOnInit() { this.status$ = this.employees.formStatus$; } isInvalid(name) 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!'
и log (5)
. И накрая, зададохме време за изчакване, за да зададем formStatus
обратно към празно, регистриране (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 { return this.store.state$.pipe(map(state => state.totalEmployees)) } get totalDrivers$(): Observable { return this.store.state$.pipe(map(state => state.totalDrivers)) } get totalRosarioEmployees$(): Observable { return this.store.state$.pipe(map(state => state.totalRosarioEmployees)) } // ...
Сега нека създадем обобщаващия компонент:
ng g c employees/components/EmployeesSummary
Ще сложим това в employees/components/employees-summary/employees-summary.html
:
Total: { async}
Drivers: { async}
Rosario: { async}
И в 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 ; drivers$: Observable ; rosario$: Observable ; 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
:
// ... Employees
// ...
Резултатът е следният:
В конзолата имаме:
Службата за служители изчислява общата totalEmployees
, totalDrivers
и totalRosarioEmployees
за всяка емисия и актуализира състоянието.
The пълният код на този урок е достъпен на GitHub и има също демо на живо .
В този урок разгледахме един прост подход за управление на състоянието в ъглови приложения, използвайки Firebase back end.
Този подход се вписва добре с ъгловите насоки за използване на наблюдаеми. Той също така улеснява отстраняването на грешки, като осигурява проследяване на всички актуализации на състоянието на приложението.
Общата услуга за съхранение може също да се използва за управление на състоянието на приложенията, които не използват функциите на Firebase, или за управление само на данните на приложението или данните, идващи от други API.
Но преди да започнете да прилагате това безразборно, едно нещо, което трябва да имате предвид е, че EmployeesService
се абонира за Firestore в конструктора и продължава да слуша, докато приложението е активно. Това може да е полезно, ако използваме списъка със служители на множество страници в приложението, за да избегнем получаването на данни от Firestore при навигация между страниците.
Но това може да не е най-добрият вариант в други сценарии, като ако просто трябва да изтеглите първоначалните стойности веднъж и след това ръчно да заредите презареждане на данни от Firebase. Изводът е, че винаги е важно да разберете изискванията на приложението си, за да изберете по-добри методи за внедряване.
Angular (първоначално AngularJS) е популярна интерфейсна рамка за създаване на приложения на една страница (SPA). Той е с отворен код и подкрепен от Google.
Държавното управление е свързано с правилното проследяване на променливи в уеб приложение. Напр. ако потребителят на приложение за чат смени чат-стаите, това е промяна в състоянието. Ако след това са изпратили съобщение, но функцията за изпращане не е била наясно с по-ранната промяна на състоянието, тя ще изпрати съобщението до предишната чат стая, което ще доведе до много лош UX.
Google Firebase е платформа за разработка на мобилни приложения. Той е добре известен с оригиналното си предлагане в реално време на база данни, но днес включва интегрирано отчитане на сривове, удостоверяване и хостинг на активи, наред с други.