การจัดการสถานะในเชิงมุมโดยใช้ Firebase
เผยแพร่แล้ว: 2022-03-11การจัดการสถานะเป็นสถาปัตยกรรมที่สำคัญมากที่ควรพิจารณาเมื่อพัฒนาเว็บแอป
ในบทช่วยสอนนี้ เราจะพูดถึงวิธีการง่ายๆ ในการจัดการสถานะในแอปพลิเคชัน Angular ที่ใช้ Firebase เป็นแบ็กเอนด์
เราจะพูดถึงแนวคิดบางอย่าง เช่น รัฐ ร้านค้า และบริการ หวังว่านี่จะช่วยให้คุณเข้าใจข้อกำหนดเหล่านี้ได้ดีขึ้นและเข้าใจไลบรารีการจัดการสถานะอื่นๆ เช่น NgRx และ NgX ได้ดีขึ้น
เราจะสร้างหน้าผู้ดูแลระบบของพนักงานเพื่อให้ครอบคลุมสถานการณ์การจัดการสถานะต่างๆ และวิธีการที่สามารถจัดการได้
ส่วนประกอบ บริการ Firestore และการจัดการสถานะใน Angular
ในแอปพลิเคชัน Angular ทั่วไป เรามีส่วนประกอบและบริการ โดยปกติ ส่วนประกอบจะทำหน้าที่เป็นเทมเพลตมุมมอง บริการจะมีตรรกะทางธุรกิจและ/หรือสื่อสารกับ API ภายนอกหรือบริการอื่นๆ เพื่อดำเนินการหรือดึงข้อมูลให้เสร็จสิ้น
คอมโพเนนต์มักจะแสดงข้อมูลและอนุญาตให้ผู้ใช้โต้ตอบกับแอปเพื่อดำเนินการต่างๆ ขณะทำเช่นนี้ ข้อมูลอาจเปลี่ยนแปลงและแอปสะท้อนถึงการเปลี่ยนแปลงเหล่านั้นโดยอัปเดตมุมมอง
กลไกตรวจจับการเปลี่ยนแปลงของ Angular จะตรวจสอบเมื่อค่าในส่วนประกอบที่ผูกกับมุมมองมีการเปลี่ยนแปลงและอัปเดตมุมมองตามลำดับ
เมื่อแอปเติบโตขึ้น เราจะเริ่มมีส่วนประกอบและบริการมากขึ้นเรื่อยๆ บ่อยครั้งการทำความเข้าใจว่าข้อมูลมีการเปลี่ยนแปลงอย่างไรและการติดตามว่าเกิดขึ้นที่ใดอาจเป็นเรื่องยุ่งยาก
เชิงมุมและ Firebase
เมื่อเราใช้ Firebase เป็นแบ็กเอนด์ เราได้รับ API ที่เรียบร้อยจริงๆ ซึ่งประกอบด้วยการดำเนินการและฟังก์ชันส่วนใหญ่ที่เราต้องการเพื่อสร้างแอปพลิเคชันแบบเรียลไทม์
@angular/fire
เป็นไลบรารี Angular Firebase อย่างเป็นทางการ เป็นเลเยอร์ที่ด้านบนของไลบรารี Firebase JavaScript SDK ที่ทำให้การใช้ Firebase SDK ในแอป Angular ง่ายขึ้น มันมีความเหมาะสมกับแนวปฏิบัติที่ดีของ Angular เช่น การใช้ Observables เพื่อรับและแสดงข้อมูลจาก Firebase ไปยังส่วนประกอบของเรา
ร้านค้าและรัฐ
เราสามารถนึกถึง "สถานะ" ว่าเป็นค่าที่แสดง ณ เวลาใดก็ตามในแอป ร้านค้าเป็นเพียงผู้ถือสถานะแอปพลิเคชันนั้น
สามารถสร้างแบบจำลองสถานะเป็นวัตถุธรรมดาชิ้นเดียวหรือเป็นชุดได้ ซึ่งสะท้อนถึงค่าของแอปพลิเคชัน
แอปตัวอย่างเชิงมุม/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
/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 Base Classes
เราจะสร้างคลาสนามธรรมทั่วไปสองคลาส จากนั้นเราจะพิมพ์และขยายจากเพื่อสร้างบริการของเรา
Generics ให้คุณเขียนพฤติกรรมโดยไม่มีประเภทผูกมัด เพิ่มความสามารถในการใช้ซ้ำและความยืดหยุ่นให้กับโค้ดของคุณ
บริการ Firestore ทั่วไป
เพื่อใช้ประโยชน์จาก TypeScript generics สิ่งที่เราจะทำคือสร้าง wrapper พื้นฐานทั่วไปสำหรับบริการ @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
ช่วยให้สมาชิกได้รับค่าที่ปล่อยออกมาล่าสุดทันทีที่พวกเขาสมัคร ในกรณีของเรา สิ่งนี้มีประโยชน์เพราะเราสามารถเริ่มต้นร้านค้าด้วยค่าเริ่มต้นสำหรับส่วนประกอบทั้งหมดของเราเมื่อพวกเขาสมัครใช้งานร้านค้า
ทางร้านจะมี 2 วิธี คือ 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()
the 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
Pipe
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
กลับเป็นค่าว่าง การบันทึก (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 โดยใช้แบ็คเอนด์ของ Firebase
แนวทางนี้เหมาะสมอย่างยิ่งกับแนวทางเชิงมุมของการใช้ Observables นอกจากนี้ยังอำนวยความสะดวกในการดีบักด้วยการติดตามการอัปเดตสถานะของแอปทั้งหมด
บริการร้านค้าทั่วไปยังสามารถใช้เพื่อจัดการสถานะของแอพที่ไม่ได้ใช้คุณสมบัติ Firebase ไม่ว่าจะเพื่อจัดการเฉพาะข้อมูลของแอพหรือข้อมูลที่มาจาก API อื่น ๆ
แต่ก่อนที่คุณจะใช้สิ่งนี้โดยไม่เลือกปฏิบัติ สิ่งหนึ่งที่ต้องพิจารณาก็คือ EmployeesService
สมัครใช้งาน Firestore บน Constructor และคอยฟังในขณะที่แอปทำงานอยู่ ซึ่งอาจเป็นประโยชน์หากเราใช้รายชื่อพนักงานในหลายหน้าในแอป เพื่อหลีกเลี่ยงการรับข้อมูลจาก Firestore เมื่อนำทางระหว่างหน้าต่างๆ
แต่นี่อาจไม่ใช่ตัวเลือกที่ดีที่สุดในสถานการณ์อื่นๆ เช่น หากคุณต้องการดึงค่าเริ่มต้นเพียงครั้งเดียว จากนั้นทริกเกอร์การโหลดข้อมูลใหม่จาก Firebase ด้วยตนเอง สิ่งสำคัญที่สุดคือ การเข้าใจข้อกำหนดของแอปเป็นสิ่งสำคัญเสมอ เพื่อเลือกวิธีการติดตั้งใช้งานที่ดีขึ้น