В цій серії ми налаштуємо проєкт, що використовує бібліотеку Angular Fire для створення, перегляду, редагування та видалення документів у Firestore.
Ця стаття:
- … стосується архітектури проєкту Angular, обсервацій та промісів
- … не стосується налаштування або обслуговування Firebase
- … не стосується аутентифікації Firebase (див. тут)
- … використовує офіційну бібліотеку
@angular/fire
Останнє оновлення Angular, разом з Angular Fire, було досить сумним. Я розумію необхідність бути актуальним, але не можу зрозуміти, чому потрібно відмовлятися від синтаксису, який працює, на користь іншого модного синтаксису! Але ось ми тут.
Налаштування
Ось посилання на налаштування Firebase Firestore та на налаштування Angular Fire.
Слідуйте цьому проєкту на StackBlitz. Я прибрав конфігурацію Firebase, тому він не працюватиме належним чином.
Давайте створимо проєкт, який відображатиме категорії, де кожна категорія складається з:
- назви
- ключа
Ось так
Файл 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% Гази? Але Газа відновиться.
Ресурси
- Проект на StackBlitz
- Документація Firestore
- Посилання на Firestore
- Бібліотека AngularFire
- Бібліотека RxFire
Схожі публікації
[
Використання бібліотеки 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