إدارة الحالة في Angular باستخدام Firebase

نشرت: 2022-03-11

تعد إدارة الدولة جزءًا مهمًا جدًا من الهندسة يجب مراعاته عند تطوير تطبيق ويب.

في هذا البرنامج التعليمي ، سننتقل إلى نهج بسيط لإدارة الحالة في تطبيق Angular الذي يستخدم Firebase كنهاية خلفية له.

سنستعرض بعض المفاهيم مثل الحالة والمتاجر والخدمات. نأمل أن يساعدك هذا في الحصول على فهم أفضل لهذه المصطلحات وكذلك فهم مكتبات إدارة الدولة الأخرى بشكل أفضل مثل NgRx و NgXs.

سننشئ صفحة مسؤول موظف لتغطية بعض سيناريوهات إدارة الحالة المختلفة والأساليب التي يمكنها التعامل معها.

المكونات ، الخدمات ، Firestore ، وإدارة الدولة في Angular

في تطبيق Angular النموذجي ، لدينا مكونات وخدمات. عادة ، ستعمل المكونات كقالب عرض. ستحتوي الخدمات على منطق الأعمال و / أو التواصل مع واجهات برمجة التطبيقات الخارجية أو الخدمات الأخرى لإكمال الإجراءات أو استرداد البيانات.

تتصل المكونات بالخدمات التي تتصل بخدمات أخرى أو واجهات برمجة تطبيقات HTTP.

ستعرض المكونات عادةً البيانات وتسمح للمستخدمين بالتفاعل مع التطبيق لتنفيذ الإجراءات. أثناء القيام بذلك ، قد تتغير البيانات ويعكس التطبيق هذه التغييرات عن طريق تحديث العرض.

يعتني محرك الكشف عن التغيير في Angular بالتحقق من تغير قيمة في مكون مرتبط بالعرض ويقوم بتحديث العرض وفقًا لذلك.

مع نمو التطبيق ، سنبدأ في الحصول على المزيد والمزيد من المكونات والخدمات. غالبًا ما يكون فهم كيفية تغير البيانات وتتبع مكان حدوث ذلك أمرًا صعبًا.

الزاوي و Firebase

عندما نستخدم Firebase كواجهة خلفية ، يتم تزويدنا بواجهة برمجة تطبيقات أنيقة حقًا تحتوي على معظم العمليات والوظائف التي نحتاجها لإنشاء تطبيق في الوقت الفعلي.

@angular/fire هي مكتبة Angular Firebase الرسمية. إنها طبقة أعلى مكتبة Firebase JavaScript SDK تعمل على تبسيط استخدام Firebase SDK في تطبيق Angular. إنه يوفر توافقًا رائعًا مع ممارسات Angular الجيدة مثل استخدام Observables للحصول على البيانات وعرضها من Firebase إلى مكوناتنا.

تشترك المكونات في Firebase JavaScript API عبر @ angular / fire باستخدام Observables.

المتاجر والدولة

يمكننا التفكير في "الحالة" على أنها القيم المعروضة في أي نقطة زمنية معينة في التطبيق. المتجر هو ببساطة صاحب حالة التطبيق هذه.

يمكن نمذجة الحالة ككائن واحد عادي أو سلسلة منها ، مما يعكس قيم التطبيق.

حالة تخزين المخزن ، والتي تحتوي على نموذج لعنصر مع بعض أزواج قيمة المفتاح البسيطة للاسم والمدينة والبلد.

Angular / Firebase Sample App

لنقم ببنائه: أولاً ، سننشئ سقالة تطبيق أساسية باستخدام 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 Base

سننشئ فصلين تجريديين عامين ، سنقوم بعد ذلك بكتابتهما وتوسيع نطاقهما لبناء خدماتنا.

تتيح لك Generics كتابة السلوك بدون نوع منضم. هذا يضيف قابلية إعادة الاستخدام والمرونة إلى التعليمات البرمجية الخاصة بك.

خدمة Firestore العامة

من أجل الاستفادة من الأدوية الجنيسة من TypeScript ، ما سنفعله هو إنشاء غلاف عام أساسي لخدمة @angular/fire firestore service.

لنقم بإنشاء 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() القيمة الجديدة من النوع Partial<T> وسيقوم بدمجها مع قيمة newValue الحالية 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 :

 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 والمتجر لتحديث الحالة. سيساعدنا هذا في إنشاء واجهة برمجة تطبيقات واحدة للمكونات للاتصال.

 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 والأنبوب غير async

تسمح المراقبات للمشتركين بتلقي انبعاثات البيانات كتيار. هذا ، إلى جانب الأنبوب async ، يمكن أن يكون قويًا للغاية.

يعتني الأنبوب غير async بالاشتراك في "يمكن ملاحظته" وتحديث طريقة العرض عند إصدار بيانات جديدة. والأهم من ذلك ، أنه يلغي الاشتراك تلقائيًا عند تدمير المكون ، مما يحمينا من تسرب الذاكرة.

يمكنك قراءة المزيد عن 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 مرة أخرى على فارغة ، وتسجيل (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> // ...

والنتيجة هي ما يلي:

صفحة الموظفين ، الآن مع ملخص أعلى القائمة ، يعرض تعداد إجمالي الموظفين ، والسائقين ، وأولئك من روساريو.

في وحدة التحكم لدينا:

يُظهر إخراج وحدة التحكم حدث تصحيح يغير قيم الملخص.

تحسب خدمة الموظفين الإجمالي الإجمالي للموظفين ، وإجمالي السائقين ، totalDrivers totalRosarioEmployees totalEmployees كل إصدار وتقوم بتحديث الحالة.

الكود الكامل لهذا البرنامج التعليمي متاح على GitHub ، وهناك أيضًا عرض توضيحي مباشر.

إدارة Angular App State باستخدام Observables ... تحقق!

في هذا البرنامج التعليمي ، غطينا نهجًا بسيطًا لإدارة الحالة في تطبيقات Angular باستخدام الواجهة الخلفية لـ Firebase.

هذا النهج يتناسب بشكل جيد مع المبادئ التوجيهية Angular لاستخدام Observables. كما أنه يسهل التصحيح من خلال توفير تتبع لجميع التحديثات لحالة التطبيق.

يمكن أيضًا استخدام خدمة المتجر العام لإدارة حالة التطبيقات التي لا تستخدم ميزات Firebase ، إما لإدارة بيانات التطبيق فقط أو البيانات الواردة من واجهات برمجة التطبيقات الأخرى.

ولكن قبل أن تقوم بتطبيق هذا بشكل عشوائي ، هناك شيء واحد يجب مراعاته وهو أن EmployeesService يشترك في Firestore على المُنشئ ويستمر في الاستماع أثناء تنشيط التطبيق. قد يكون هذا مفيدًا إذا استخدمنا قائمة الموظفين في صفحات متعددة على التطبيق ، لتجنب الحصول على البيانات من Firestore عند التنقل بين الصفحات.

ولكن قد لا يكون هذا هو الخيار الأفضل في سيناريوهات أخرى مثل ما إذا كنت تحتاج فقط إلى سحب القيم الأولية مرة واحدة ثم تشغيل عمليات إعادة تحميل البيانات يدويًا من Firebase. خلاصة القول ، من المهم دائمًا فهم متطلبات تطبيقك من أجل اختيار طرق أفضل للتنفيذ.