Использование с Angular: сервисы и зоны

Dexie.js выступает тонким и мощным слоем над IndexedDB, предоставляя удобный API для работы с локальными данными. В Angular-приложениях его интеграция требует учёта двух ключевых аспектов: системы внедрения зависимостей и механизма зон (Zone.js), который управляет обнаружением изменений.

Типичная архитектура строится вокруг изолированного сервиса, инкапсулирующего доступ к базе данных, а также слоя реактивности, связанного с RxJS и Angular Change Detection.


Базовый сервис Dexie в Angular

Создание единой точки доступа к 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 и производительность

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;
  });
});

Такой подход снижает количество лишних циклов обнаружения изменений.


Реактивная модель с RxJS

Dexie не предоставляет встроенных Observable API (за исключением liveQuery в экосистеме Dexie), поэтому интеграция с Angular часто строится через from, BehaviorSubject или Observable.

Преобразование промисов в поток

import { from } from 'rxjs';

getUsers$() {
  return from(this.db.users.toArray());
}

Однако такой поток является одноразовым. Для реактивности требуется более сложная модель.


Использование liveQuery и реактивных обновлений

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())
    )
  );
}

Интеграция с OnPush и оптимизация рендера

При использовании ChangeDetectionStrategy.OnPush Angular обновляет компонент только при изменении входных данных или явных событиях.

RxJS-потоки из Dexie удобно комбинировать с async pipe:

users$ = this.usersRepository.getUsersLive$();
<div *ngFor="let user of users$ | async">
  {{ user.name }}
</div>

Такой подход устраняет необходимость ручного вызова markForCheck.


Сервисный слой как единая точка доступа к данным

В сложных приложениях слой доступа к Dexie разделяется на:

  • базовый DB сервис
  • репозитории сущностей
  • фасады состояния (state facades)

Пример репозитория:

@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 формируются устойчивые паттерны:

  • разделение DB-схемы и Angular DI-сервисов
  • использование NgZone для изоляции IndexedDB операций
  • реактивные фасады вместо прямого доступа к Dexie
  • минимизация триггеров change detection
  • использование OnPush как базовой стратегии компонентов
  • централизованные миграции схемы

Такое сочетание позволяет Dexie выступать полноценным локальным хранилищем уровня клиентской базы данных, не нарушая архитектурную целостность Angular-приложения.