Интеграция клиентского хранилища в Angular требует соблюдения принципов инверсии зависимостей, тестируемости и изоляции побочных эффектов. Библиотека localForage, предоставляющая единый асинхронный API поверх IndexedDB, WebSQL и localStorage, органично встраивается в Angular через сервисный слой и механизм Dependency Injection (DI).
Ключевая задача интеграции заключается не в прямом вызове API localForage из компонентов, а в построении абстракции, которая:
localForage предоставляет глобальный объект-конфигуратор, который может быть переиспользован внутри Angular-сервиса:
import localforage from 'localforage';
localforage.config({
name: 'MyApp',
storeName: 'app_storage',
description: 'Primary storage for Angular application'
});
Однако в Angular-подходе конфигурация должна быть инкапсулирована в DI, чтобы избежать глобального состояния.
Для гибкости создаётся токен конфигурации:
import { InjectionToken } from '@angular/core';
export interface LocalForageConfig {
name: string;
storeName: string;
version?: number;
}
export const LOCAL_FORAGE_CONFIG =
new InjectionToken<LocalForageConfig>('LOCAL_FORAGE_CONFIG');
Это позволяет задавать конфигурацию на уровне модуля:
providers: [
{
provide: LOCAL_FORAGE_CONFIG,
useValue: {
name: 'MyApp',
storeName: 'main_store'
}
}
]
Сервис является центральной точкой доступа к хранилищу:
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import localforage from 'localforage';
import { LOCAL_FORAGE_CONFIG, LocalForageConfig } from './localforage.tokens';
@Injectable({
providedIn: 'root'
})
export class LocalForageService {
private storage = localforage;
private isBrowser: boolean;
constructor(
@Inject(LOCAL_FORAGE_CONFIG) config: LocalForageConfig,
@Inject(PLATFORM_ID) platformId: Object
) {
this.isBrowser = isPlatformBrowser(platformId);
this.storage.config({
name: config.name,
storeName: config.storeName
});
}
private ensureBrowser(): boolean {
return this.isBrowser;
}
Асинхронная природа localForage требует строгой типизации возвращаемых Promise:
async setItem<T>(key: string, value: T): Promise<T | null> {
if (!this.ensureBrowser()) return null;
return await this.storage.setItem<T>(key, value);
}
async getItem<T>(key: string): Promise<T | null> {
if (!this.ensureBrowser()) return null;
return await this.storage.getItem<T>(key);
}
async removeItem(key: string): Promise<void> {
if (!this.ensureBrowser()) return;
await this.storage.removeItem(key);
}
Для сценариев миграции и сброса состояния:
async clear(): Promise<void> {
if (!this.ensureBrowser()) return;
await this.storage.clear();
}
async length(): Promise<number> {
if (!this.ensureBrowser()) return 0;
return await this.storage.length();
}
localForage поддерживает перебор записей, что полезно для кэшей и офлайн-очередей:
async iterate<T>(
iterator: (value: T, key: string, iterationNumber: number) => void
): Promise<void> {
if (!this.ensureBrowser()) return;
await this.storage.iterate<T, void>((value, key, iterationNumber) => {
iterator(value, key, iterationNumber);
});
}
Angular-проекты часто требуют реактивного слоя поверх Promise API. Используется обёртка через Observable:
import { from, Observable } from 'rxjs';
getItem$<T>(key: string): Observable<T | null> {
return from(this.getItem<T>(key));
}
setItem$<T>(key: string, value: T): Observable<T | null> {
return from(this.setItem<T>(key, value));
}
Для более сложных сценариев (например, кеширование состояния UI)
можно добавлять BehaviorSubject:
import { BehaviorSubject } from 'rxjs';
private cache$ = new BehaviorSubject<Record<string, unknown>>({});
getCache$() {
return this.cache$.asObservable();
}
Angular Universal не имеет доступа к browser storage, поэтому критично изолировать вызовы:
private safeCall<T>(fn: () => Promise<T>, fallback: T): Promise<T> {
if (!this.isBrowser) {
return Promise.resolve(fallback);
}
return fn();
}
Использование:
async getItem<T>(key: string): Promise<T | null> {
return this.safeCall(
() => this.storage.getItem<T>(key),
null
);
}
Часто требуется комбинировать память и persistent storage:
private memoryCache = new Map<string, unknown>();
async getCached<T>(key: string): Promise<T | null> {
if (this.memoryCache.has(key)) {
return this.memoryCache.get(key) as T;
}
const value = await this.getItem<T>(key);
if (value !== null) {
this.memoryCache.set(key, value);
}
return value;
}
При изменении структуры данных требуется миграция:
private readonly version = 2;
async migrate(): Promise<void> {
const storedVersion = await this.getItem<number>('__version__') || 0;
if (storedVersion < this.version) {
await this.clear();
await this.setItem('__version__', this.version);
}
}
Хотя Angular не имеет встроенных interceptors для local storage, можно реализовать паттерн декоратора:
export class CachedStorageService {
constructor(private storage: LocalForageService) {}
async get<T>(key: string): Promise<T | null> {
const cached = await this.storage.getItem<T>(key);
if (cached) return cached;
const fresh = await this.fetchFromApi<T>(key);
await this.storage.setItem(key, fresh);
return fresh;
}
private async fetchFromApi<T>(key: string): Promise<T> {
throw new Error('Implementation required');
}
}
В современных Angular-приложениях сервисы легко подключаются через
provide:
export const appProviders = [
{
provide: LOCAL_FORAGE_CONFIG,
useValue: {
name: 'ModernApp',
storeName: 'cache'
}
}
];
Mock localForage позволяет изолировать тесты:
const mockStorage = {
getItem: jasmine.createSpy().and.resolveTo('value'),
setItem: jasmine.createSpy().and.resolveTo('value'),
removeItem: jasmine.createSpy().and.resolveTo(undefined),
clear: jasmine.createSpy().and.resolveTo(undefined)
};
Подмена через DI:
{
provide: LocalForageService,
useValue: mockStorage
}
localForage может падать в условиях приватного режима или ограниченного storage quota:
async safeGet<T>(key: string): Promise<T | null> {
try {
return await this.storage.getItem<T>(key);
} catch {
return null;
}
}
Для предотвращения коллизий ключи должны быть централизованы:
export const STORAGE_KEYS = {
USER: 'user:data',
SETTINGS: 'app:settings',
CACHE: 'app:cache'
};
Компонент работает только через сервисный слой:
constructor(private storage: LocalForageService) {}
async ngOnInit() {
const settings = await this.storage.getItem('app:settings');
}
Прямой доступ к localForage исключается, что обеспечивает архитектурную стабильность и единообразие работы с данными в приложении Angular.