IndexedDB в браузере допускает одновременное открытие одной и той же
базы данных из разных контекстов выполнения. Однако при работе с
Dexie.js множественные экземпляры Dexie-базы, созданные в
рамках одного приложения, приводят к ряду архитектурных и
эксплуатационных проблем:
version().upgrade)on('ready'),
populate)Особенно критично это проявляется в SPA-приложениях, где модули могут импортироваться повторно в разных частях дерева зависимостей или при горячей перезагрузке (HMR).
Singleton-экземпляр базы данных в Dexie.js — это не просто глобальная
переменная, а гарантированная точка доступа к единственному экземпляру
Dexie, связанного с конкретной физической
IndexedDB-базой.
Ключевой смысл:
DexieВ отличие от обычных singleton-паттернов, здесь важно учитывать
асинхронную природу db.open() и состояние подключения.
Наиболее распространённый подход основан на кэшировании экземпляра в области модуля ES Modules.
import Dexie from 'dexie';
class AppDatabase extends Dexie {
constructor() {
super('app_database');
this.version(1).stores({
users: '++id, email, name',
posts: '++id, userId, title'
});
}
}
let dbInstance = null;
export function getDB() {
if (!dbInstance) {
dbInstance = new AppDatabase();
}
return dbInstance;
}
Ключевая особенность:
dbInstance существует в замыкании модуляDexie требует явного открытия соединения через
db.open(), которое возвращает Promise. Это создает проблему
гонки инициализации при параллельных вызовах.
При таком коде:
const db1 = getDB();
const db2 = getDB();
db1.open();
db2.open();
возможны:
open/closedreadyКорректный подход заключается в кэшировании Promise открытия:
import Dexie from 'dexie';
class AppDatabase extends Dexie {
constructor() {
super('app_database');
this.version(1).stores({
users: '++id, email, name'
});
}
}
let dbInstance = null;
let dbOpenPromise = null;
export function getDB() {
if (!dbInstance) {
dbInstance = new AppDatabase();
}
return dbInstance;
}
export function openDB() {
if (!dbOpenPromise) {
dbOpenPromise = getDB().open();
}
return dbOpenPromise;
}
Особенности:
dbOpenPromise гарантирует единственное открытиеDexie поддерживает db.close(), что требует аккуратного
обращения с singleton-моделью.
После закрытия:
db.close();
await db.open();
экземпляр остается валидным, но его состояние становится сложно контролируемым в глобальном singleton.
Используется явное сбрасывание кэша:
let dbInstance = null;
let dbOpenPromise = null;
export function getDB() {
if (!dbInstance) {
dbInstance = new AppDatabase();
}
return dbInstance;
}
export async function resetDB() {
if (dbInstance) {
dbInstance.close();
}
dbInstance = null;
dbOpenPromise = null;
}
Такой подход позволяет:
В dev-средах с HMR (Webpack, Vite) модуль может пересоздаваться без
перезагрузки страницы, что ломает классический singleton через
let dbInstance.
Каждый HMR reload:
globalThisРешение — хранение экземпляра в глобальном объекте:
class AppDatabase extends Dexie {
constructor() {
super('app_database');
this.version(1).stores({
users: '++id, email'
});
}
}
const globalKey = '__APP_DB__';
export function getDB() {
if (!globalThis[globalKey]) {
globalThis[globalKey] = new AppDatabase();
}
return globalThis[globalKey];
}
Преимущества:
Dexie строго привязан к версии схемы. Singleton-архитектура напрямую влияет на миграции.
При неправильной организации возможно:
Версия должна быть фиксированной внутри singleton-класса:
class AppDatabase extends Dexie {
constructor() {
super('app_database');
this.version(1).stores({
users: '++id'
});
this.version(2).stores({
users: '++id, email',
posts: '++id, userId'
});
}
}
Singleton обеспечивает:
В крупных архитектурах прямой singleton может ограничивать тестируемость. Альтернативой становится DI-слой поверх Dexie.
export class DatabaseService {
constructor(db) {
this.db = db;
}
getUsers() {
return this.db.users.toArray();
}
}
Singleton используется только на уровне создания:
import { getDB } from './db';
const db = getDB();
export const databaseService = new DatabaseService(db);
Преимущества:
Dexie часто тестируется с fake-indexeddb, где singleton
может мешать изоляции тестов.
Если singleton не сбрасывается:
export function createTestDB() {
const db = new Dexie('test_db');
db.version(1).stores({
users: '++id'
});
return db;
}
И отказ от singleton в тестах:
let db;
beforeEach(async () => {
db = createTestDB();
await db.open();
});
afterEach(async () => {
db.close();
await Dexie.delete('test_db');
});
Такой подход гарантирует:
Часто база не должна открываться сразу при импорте модуля.
let dbInstance;
let dbOpenPromise;
export function getDB() {
if (!dbInstance) {
dbInstance = new AppDatabase();
}
return dbInstance;
}
export function ensureDB() {
if (!dbOpenPromise) {
dbOpenPromise = getDB().open();
}
return dbOpenPromise;
}
Особенности:
В крупных приложениях Dexie часто используется в нескольких слоях:
Без строгого singleton каждый слой может создать собственный экземпляр, что приводит к:
Централизация через singleton обеспечивает:
liveQuery,
hooks)class AppDatabase extends Dexie {
static instance;
static getInstance() {
if (!this.instance) {
this.instance = new AppDatabase();
}
return this.instance;
}
}
Проблема:
const db = new Dexie('db');
db.open();
Проблема:
Без resetDB() невозможно:
Корректная модель управления включает:
Такая структура обеспечивает стабильную работу IndexedDB в условиях SPA, SSR-клиентов, HMR и тестовых сред, устраняя основные источники гонок и дублирования состояния.