Dexie.js выступает тонким и мощным слоем над IndexedDB, предоставляя удобный API для работы с локальными данными. В Angular-приложениях его интеграция требует учёта двух ключевых аспектов: системы внедрения зависимостей и механизма зон (Zone.js), который управляет обнаружением изменений.
Типичная архитектура строится вокруг изолированного сервиса, инкапсулирующего доступ к базе данных, а также слоя реактивности, связанного с RxJS и Angular Change Detection.
Создание единой точки доступа к IndexedDB через Dexie позволяет централизовать схему базы данных, миграции и операции чтения/записи.
import Dexie, { Table } fr om 'dexie';
import { Injectable } fr om '@angular/core';
export interface UserEntity {
id?: number;
name: string;
email: string;
createdAt: number;
}
export class AppDB extends Dexie {
users!: Table<UserEntity, number>;
constructor() {
super('AppDB');
this.version(1).stores({
users: '++id, email, createdAt'
});
}
}
Сервис Angular инкапсулирует экземпляр базы:
import { Injectable } fr om '@angular/core';
import { AppDB, UserEntity } from './db';
@Injectable({ providedIn: 'root' })
export class UsersRepository {
private db = new AppDB();
getAllUsers() {
return this.db.users.toArray();
}
addUser(user: UserEntity) {
return this.db.users.add(user);
}
updateUser(id: number, changes: Partial<UserEntity>) {
return this.db.users.update(id, changes);
}
deleteUser(id: number) {
return this.db.users.delete(id);
}
}
Такой слой отделяет бизнес-логику Angular от деталей IndexedDB и Dexie API.
Angular Zone.js автоматически отслеживает асинхронные операции и инициирует change detection. Dexie активно использует промисы, что приводит к частым триггерам обновления UI даже при незначительных изменениях.
Для оптимизации используется NgZone.runOutsideAngular,
позволяющий выполнять операции с IndexedDB вне зоны Angular.
import { Injectable, NgZone } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class UsersRepository {
constructor(
private zone: NgZone,
private db: AppDB
) {}
getAllUsers() {
return this.zone.runOutsideAngular(() =>
this.db.users.toArray()
);
}
}
После получения данных при необходимости возвращение в Angular-зону выполняется явно:
this.usersRepository.getAllUsers().then(users => {
this.zone.run(() => {
this.users = users;
});
});
Такой подход снижает количество лишних циклов обнаружения изменений.
Dexie не предоставляет встроенных Observable API (за исключением
liveQuery в экосистеме Dexie), поэтому интеграция с Angular
часто строится через from, BehaviorSubject или
Observable.
import { from } from 'rxjs';
getUsers$() {
return from(this.db.users.toArray());
}
Однако такой поток является одноразовым. Для реактивности требуется более сложная модель.
Dexie предоставляет механизм реактивных запросов через
liveQuery, который автоматически отслеживает изменения в
таблицах.
import { liveQuery } from 'dexie';
import { from } from 'rxjs';
getUsersLive$() {
return from(
liveQuery(() => this.db.users.toArray())
);
}
Этот поток будет эмитить значения при каждом изменении таблицы
users.
В Angular важно учитывать зоны:
getUsersLive$() {
return from(
liveQuery(() =>
this.zone.runOutsideAngular(() => this.db.users.toArray())
)
);
}
При использовании ChangeDetectionStrategy.OnPush Angular
обновляет компонент только при изменении входных данных или явных
событиях.
RxJS-потоки из Dexie удобно комбинировать с async
pipe:
users$ = this.usersRepository.getUsersLive$();
<div *ngFor="let user of users$ | async">
{{ user.name }}
</div>
Такой подход устраняет необходимость ручного вызова
markForCheck.
В сложных приложениях слой доступа к Dexie разделяется на:
Пример репозитория:
@Injectable({ providedIn: 'root' })
export class UsersRepository {
constructor(private db: AppDB) {}
getByEmail(email: string) {
return this.db.users.wh ere('email').equals(email).first();
}
searchByName(query: string) {
return this.db.users
.wh ere('name')
.startsWithIgnoreCase(query)
.toArray();
}
}
Dexie поддерживает транзакции, которые критичны для сложных операций:
this.db.transaction('rw', this.db.users, async () => {
const id = await this.db.users.add({
name: 'Alice',
email: 'alice@mail.com',
createdAt: Date.now()
});
await this.db.users.update(id, { name: 'Alice Cooper' });
});
В Angular-сервисах транзакции часто оборачиваются в методы репозитория, чтобы скрыть детали реализации.
Dexie использует версионирование для управления изменениями схемы:
this.version(2).stores({
users: '++id, email, createdAt, isActive'
}).upgrade(tx => {
return tx.table('users').toCollection().modify(user => {
user.isActive = true;
});
});
Angular-сервис при этом не требует изменений, если контракт репозитория стабилен.
Dexie позволяет эффективно работать с курсорами и индексами:
this.db.users
.orderBy('createdAt')
.reverse()
.lim it(50)
.toArray();
Для потоковой обработки:
this.db.users.each(user => {
console.log(user.email);
});
В Angular такие операции выполняются вне зоны:
this.zone.runOutsideAngular(() => {
this.db.users.each(u => console.log(u));
});
В крупных Angular-приложениях Dexie часто скрывается за фасадом состояния:
@Injectable({ providedIn: 'root' })
export class UsersFacade {
users$ = this.repo.getUsersLive$();
constructor(private repo: UsersRepository) {}
add(user: UserEntity) {
return this.repo.addUser(user);
}
remove(id: number) {
return this.repo.deleteUser(id);
}
}
Компоненты взаимодействуют только с фасадом, не зная о Dexie напрямую.
При использовании live-потоков важно управлять подписками:
private destroy$ = new Subject<void>();
ngOnInit() {
this.facade.users$
.pipe(takeUntil(this.destroy$))
.subscribe(users => {
this.users = users;
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
При использовании async pipe этот слой управления становится избыточным, но при ручных подписках остаётся критически важным.
В зрелых Angular-приложениях с Dexie формируются устойчивые паттерны:
Такое сочетание позволяет Dexie выступать полноценным локальным хранилищем уровня клиентской базы данных, не нарушая архитектурную целостность Angular-приложения.