Використання бібліотеки Angular Fire Firestore — I

В цій серії ми налаштуємо проєкт, що використовує бібліотеку Angular Fire для створення, перегляду, редагування та видалення документів у Firestore.

pic

Ця стаття:

  • … стосується архітектури проєкту Angular, обсервацій та промісів
  • … не стосується налаштування або обслуговування Firebase
  • … не стосується аутентифікації Firebase (див. тут)
  • … використовує офіційну бібліотеку @angular/fire

Останнє оновлення Angular, разом з Angular Fire, було досить сумним. Я розумію необхідність бути актуальним, але не можу зрозуміти, чому потрібно відмовлятися від синтаксису, який працює, на користь іншого модного синтаксису! Але ось ми тут.

Налаштування

Ось посилання на налаштування Firebase Firestore та на налаштування Angular Fire.

Слідуйте цьому проєкту на StackBlitz. Я прибрав конфігурацію Firebase, тому він не працюватиме належним чином.

Давайте створимо проєкт, який відображатиме категорії, де кожна категорія складається з:

  • назви
  • ключа

Ось так

pic

Файл main.ts містить завантажувач у такому вигляді:

// main.ts   
// зверніть увагу, звідки імпортуються бібліотеки, це має бути з @angular/fire  
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';  
import { getFirestore, provideFirestore } from '@angular/fire/firestore';  

const fbApp = () => initializeApp({  
 //... firebaseConfig тут  
});  
// ... за потреби додати аутентифікацію   

const firebaseProviders = [  
 provideFirebaseApp(fbApp),  
 provideFirestore(() => getFirestore()),  
];  

bootstrapApplication(AppComponent, {  
 providers: [  
 ...firebaseProviders,  
 // ... інші провайдери  
 ]  
})

Посилання на Firestore

Бібліотека Angular Fire ініціалізує базу даних за допомогою getFirestore(app); та надає її для інжекції.

У сервісі — сервіс категорій, клас виглядає так:

// services/category.service.ts  

@Injectable({ providedIn: 'root' })  
export class CategoryService {  
 // інжекція  
 private db: Firestore = inject(Firestore);  
}

База даних готова до використання в categoryService.

Примітка щодо правильного SDK

Якщо ви натрапили на таку помилку:

FirebaseError: Тип не відповідає очікуваному екземпляру. Чи передали ви посилання з іншого SDK Firestore?

Переконайтеся, що ви використовуєте останні версії бібліотек Firebase. Ті, які працювали для мене — це firebase 11.2.0 та @angular/fire 19.0.0, разом з @angular/core 19.0.6 мінімум.

Отримання даних

Працюючи в зворотному напрямку, ми хочемо отримати компонент, який споживає повернутий список таким чином:

// компонент, який споживає, наприклад, список категорій  

@Component({  
 //...  
 template: `@let cats = categories$ | async`  
})  
export class CategoryListComponent implements OnInit {  
 // приклад  
 categories$: Observable;  
 private categoryService = inject(CategoryService);  

 ngOnInit(): void {  
 // отримати категорії з сервісу, очікуючи обсервабель, до якого можна застосувати async  
 this.categories$ = this.categoryService.GetCategories();  
 }  
}

Щоб отримати дані, ми використовуємо collectionData, який повертає обсервабель.
Функція collection сама по собі повертає посилання на колекцію.

// services/category.service.ts  

@Injectable({ providedIn: 'root' })  
export class CategoryService {  
 // інжекція, або через конструктор  
 private db: Firestore = inject(Firestore);  

 GetCategories(): Observable {  
 // collectionData повертає гарячий обсервабель  
 return collectionData(collection(this.db, 'categories')).pipe(  
 map((response: any) => {  
 // перетворення повернених документів в ICategory  
 // я не включаю цей момент тут  
 return Category.NewInstances(response);  
 })  
 );  
 }  
}

Чи знали ви, що можна імпортувати з @angular/fire/firestore/lite? Це бібліотека firestore lite для простих REST та CRUD операцій.

Функція collectionData сама по собі є ГАРЯЧИМ обсервабельним потоком, експортованим з бібліотеки rxfire від Firebase. Ось список, який вже обгорнутий та експортований з @angular/fire:

// обгорнуто та експортовано angular fire, з rxFire  
 auditTrail,  
 collection,  
 collectionChanges,  
 collectionCount,  
 collectionCountSnap,  
 collectionData,  
 doc,  
 docData,  
 fromRef,  
 snapToData,  
 sortedChanges

Повернутий обсервабель є гарячим, він насправді настільки гарячий, що він працює між користувачами, між браузерами. Що не завжди ідеально в більшості випадків. Під капотом я помітив такий рядок у бібліотеці rxfire

RxFire ref: import('firebase/database').Query,

Так, вони імпортують з реального часу бази даних!

Один із способів вирішення цієї проблеми — це додати оператори take(1) або first()

// categories.service  

GetCategories(): Observable {  
 // collectionData повертає гарячий обсервабель  
 return collectionData(collection(this.db, 'categories')).pipe(  
 // вимикаємо  
 first(),  
 map((response: any) => {  
 // ...  
 })  
 );  
}

Ми пізніше знайдемо кращий спосіб.

Отримання ідентифікатора документа

Вищезазначене повертає масив категорій без ідентифікатора документа. Давайте порівняємо офіційну документацію з документацією AngularFire. Згідно з документацією Firestore

// як зазначено в документації  
getDocs(collection(this.db, 'categories')).then(  
 (s) => {  
 // це повертає .id та .data()  
 s.forEach(f => console.log(f.id))  
 }  
);

Наше рішення використовує rxFire collectionData, який під капотом є обсервабельним потоком, що обгорнутий навколо DocumentSnapshot. Функція очікує options, які можуть мати маппер для idField, що є способом обгорнути як id, так і data() в один об'єкт. Одна з загадок Firebase!

// services/category.service.ts  

GetCategories(): Observable {  
 // мапуємо id разом з idField  
 return collectionData(collection(this.db, 'categories'), {idField: 'id'}).pipe(  
 // вимикаємо гарячий обсервабель  
 first(),  
 // ... мапінг  
 );  
}

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

// category/list.component  
template:`@let cats = categories$ | async;  


{{cat.name}}    
    selected Category: {{cat | json}}    
   `;      selected$: Observable;      select(category: ICategory) {    // щоб реалізувати GetCategory    this.selected$ = this.categoryService.GetCategory(category.id);   } ```  ## Отримання за ідентифікатором документа

Щоб отримати одну категорію за ідентифікатором документа, згідно з AngularFire, просто передайте `categories/:id`.
Але це не те, як воно виглядає.

Я отримую іншу підказку в офіційній [документації](https://firebase.google.com/docs/firestore/query-data/get-data#get_a_document)

const docRef = doc(db, "cities", "SF");
const docSnap = await getDoc(docRef);
```

Ви завжди можете думати про collection як про таблицю, а doc — як про окремий рядок.

Щоб отримати колекцію або підколекцію, використовуйте collectionData з URL-синтаксисом, який має непарну кількість сегментів
categories/:id/something

Щоб отримати документ де завгодно, ми повинні використовувати docData, і URL має мати парну кількість сегментів
categories/:id/something/:somethingid

Отже, якщо ви не дотримуєтесь кількості сегментів у URL, наприклад, використовуючи collectionData для отримання двох сегментів, ви отримаєте смішні повідомлення про помилки, такі як:

Collection references must have an odd number of segments, but categories/RG3e5jWzithsEzoPWBag has 2.

Правильний спосіб — використовувати doc та docData замість цього.

// category.service  
GetCategory(id: string): Observable {  
 // передайте URL: categories/:id, або передайте аргументи окремо  
 return docData(doc(this.db, 'categories', id), { idField: 'id' }).pipe(  
 first(),  
 // ... мапінг  
 );  
}

Запит

Щоб отримати список категорій на основі будь-яких критеріїв, ми використовуємо query, а потім collectionData. Результат завжди є масивом.

// category.service  

// запит для отримання всіх категорій  
const _query = query(collection(this.db, 'categories'))  

return collectionData(_query, { idField: 'id' }).pipe(  
 first(),  
 // ... мапінг  
);

Додавання умови where для кількох полів, за замовчуванням використовується оператор AND, формат посилання такий: query(ref, where(), where() ...)

// categories.service  

// умова where для кількох полів  
GetCategories(params: IWhere[]): Observable {  

 // перетворюємо параметри в масив умов where, а потім розгортаємо його  
 const _query = query(collection(this.db, 'categories'),   
 ...params.map(n => where(n.fieldPath, n.opStr, n.value))  
 );  

 return collectionData(_query).pipe(  
 first(),  
 // ... мапінг  
 );  
}

Формат or виглядає ось так: query(ref, or(where(), where() ...), а комбінація може виглядати так: query(ref, and(where(), where(), or(where(), where()...) ...)

Знайти посилання тут

Тепер умови where складаються з трьох сегментів: поле, оператор, значення. Тому в іншому файлі моделі ми визначаємо інтерфейс:

// where.model.ts  
export type IWhereOp =  
 | '<'  
 | '<='  
 | '=='  
 | '!=' // будьте обережні, деякі оператори не є безкоштовними  
 | '>='  
 | '>'  
 | 'array-contains'  
 | 'in'   
 | 'array-contains-any'  
 | 'not-in';  

export interface IWhere {  
 fieldPath: string;  
 opStr: IWhereOp;  
 value?: any;  
}

Щоб використовувати його, ми передаємо умови where:

// categories/list.component  

this.categories$ = this.categoryService.GetCategories(  
 [  
 {fieldPath: 'key', opStr: '==', value: 'household'}  
 ]  
);

Це виглядає не дуже добре. Пізніше ми знайдемо кращий спосіб.

Створення нової категорії — це передача об'єкта до addDoc, але є підступ. Немає обсервабеля, щоб за ним стежити. Це Promise. Але нам потрібен обсервабель, щоб ми могли застосувати pipe для належної обробки.

На цьому етапі я хочу зупинитись і зробити крок назад. Вище наведені рішення використовують обсервабелі rxfire, які є всі гарячими. Це не ідеально, коли працюєш з дуже дорогим сервісом, як Firestore. Ми використовували first, щоб охолодити його. Але давайте спробуємо щось інше. Щоб використовувати з усіма діями.

Холодні обсервабелі

Використання collectionData та docData трохи розчаровує, тому що не всі дії обгорнуті всередині них, і вони гарячі! Ми можемо використовувати from. Або defer, якщо використовуємо then безпосередньо.
Дозвольте показати вам обидва способи:

Використовуючи from

// categories.service  

// getDocs імпортовано з @angular/fire  
// отримуємо дані за допомогою "from"  
return from(getDocs(q)).pipe(  
 map((s: any) => {  
 // s — це querysnapshot, який має метод forEach  
 s.forEach(n => console.log(n.data()));  
 // пізніше будемо виконувати мапінг  
 return s;  
 })  
);

Або можна використовувати then безпосередньо, якщо ви пам'ятаєте використовувати defer в RxJs (тому що then викликає негайний виклик):

// categories.sevice  

// інший спосіб  
const docs = () => getDocs(q).then(s => {  
 console.log(s);  
 return s;  
});  

// повертаємо defer для підписки  
return defer(docs)

Мені подобається спосіб з from. Тому я залишаюсь з ним.

Мапінг даних

Давайте знайдемо спосіб:

  • правильно мапити наші дані з ідентифікатором документа
  • показувати ефект завантаження (так)

Перед тим, як рухатись далі, давайте перевіримо, чи виправлено проблему з додаванням документів:

// categories.service  

CreateCategory(category: Partial): Observable {  
 return from(addDoc(collection(this.db, 'categories'), category));  
 // todo: правильний мапінг  
}

Так, це працює як очікувалося. Але є й інші способи додавати документи. Наступного вівторка, іншаллах. 😴

Чи знали ви, що ІОФ зруйнувало 80% Гази? Але Газа відновиться.

Ресурси

Схожі публікації

[

Використання бібліотеки Angular Fire Firestore - I - Sekrab Garage

Angular Firebase Firestore. Ця стаття: ... про архітектуру Angular проектів, про обсервабелі та обіцянки…

garage.sekrab.com

](https://garage.sekrab.com/posts/putting-angular-fire-firestore-library-to-use-i?source=post_page-----7df3238600bd--------------------------------)

Перекладено з: Putting Angular Fire Firestore library to use — I

Leave a Reply

Your email address will not be published. Required fields are marked *