IndexedDB представляет собой встроенную браузерную базу данных, предназначенную для хранения больших объёмов структурированных данных на стороне клиента. Технология поддерживает индексы, транзакции, курсоры, поиск по диапазонам значений и работу с бинарными объектами.
Несмотря на широкие возможности, нативный API IndexedDB считается одним из самых сложных браузерных интерфейсов хранения данных. Основные причины связаны с асинхронной событийной моделью, громоздким синтаксисом и необходимостью ручного управления большим количеством низкоуровневых деталей.
Библиотека Dexie.js создавалась как надстройка над IndexedDB, которая сохраняет всю мощность движка базы данных, но предоставляет гораздо более удобный интерфейс программирования.
Нативный IndexedDB строится вокруг событий:
const request = indexedDB.open("AppDB", 1);
request.onupgradenee ded = event => {
const db = event.target.result;
db.createObjectStore("users", {
keyPath: "id"
});
};
request.onsucc ess = event => {
const db = event.target.result;
const tx = db.transaction("users", "readwrite");
const store = tx.objectStore("users");
store.add({
id: 1,
name: "Alex"
});
};
Даже простая операция требует нескольких уровней вложенности и большого количества служебного кода.
Тот же пример на Dexie.js выглядит значительно компактнее:
const db = new Dexie("AppDB");
db.version(1).stores({
users: "id"
});
await db.users.add({
id: 1,
name: "Alex"
});
Вместо работы через события используется современная модель Promise и синтаксис async/await.
Создание базы осуществляется через функцию
indexedDB.open().
const request = indexedDB.open("StoreDB", 1);
Если версия увеличивается, запускается обработчик обновления схемы:
request.onupgradenee ded = event => {
const db = event.target.result;
db.createObjectStore("products", {
keyPath: "id"
});
};
Разработчик самостоятельно контролирует процесс создания хранилищ, индексов и миграций.
В Dexie структура базы описывается декларативно:
const db = new Dexie("StoreDB");
db.version(1).stores({
products: "id"
});
Описание схемы становится компактным и читаемым.
Для сложных проектов схема базы выглядит как единый конфигурационный блок:
db.version(1).stores({
users: "++id,name,email",
orders: "++id,userId,date",
products: "++id,title,category"
});
Создание индексов выполняется вручную:
request.onupgradenee ded = event => {
const db = event.target.result;
const store = db.createObjectStore("users", {
keyPath: "id"
});
store.createIndex("email", "email", {
unique: true
});
store.createIndex("name", "name");
};
Для большого количества полей код быстро разрастается.
Индексы описываются строкой схемы:
db.version(1).stores({
users: "++id,&email,name"
});
Обозначения:
| Символ | Назначение |
|---|---|
| ++ | Автоинкремент |
| & | Уникальный индекс |
| * | Мультииндекс |
| [] | Составной индекс |
Пример:
db.version(1).stores({
users: "++id,&email,[firstName+lastName]"
});
Схема становится самодокументируемой.
Запись объекта требует создания транзакции и получения хранилища:
const tx = db.transaction(
"users",
"readwrite"
);
const store = tx.objectStore("users");
store.add({
id: 1,
name: "John"
});
Для отслеживания результата обычно добавляются обработчики:
const request = store.add(user);
request.onsucc ess = () => {
console.log("Saved");
};
request.oner ror = () => {
console.log("Error");
};
Добавление выполняется одной командой:
await db.users.add({
name: "John"
});
Получение идентификатора:
const id = await db.users.add({
name: "John"
});
Код становится значительно короче.
const tx = db.transaction("users");
const store = tx.objectStore("users");
const request = store.get(1);
request.onsucc ess = event => {
console.log(event.target.result);
};
const user = await db.users.get(1);
console.log(user);
Разница особенно заметна при большом количестве запросов.
Современные браузеры поддерживают метод:
store.getAll();
Однако требуется обработка событий:
const request = store.getAll();
request.onsucc ess = event => {
console.log(event.target.result);
};
const users = await db.users.toArray();
Результат сразу возвращается в виде массива.
const index = store.index("email");
const request = index.get(
"user@mail.com"
);
request.onsucc ess = event => {
console.log(
event.target.result
);
};
const user = await db.users
.where("email")
.equals("user@mail.com")
.first();
Синтаксис напоминает работу с ORM и SQL-подобными запросами.
Для сложных выборок часто приходится использовать курсоры:
const request = store.openCursor();
request.onsucc ess = event => {
const cursor = event.target.result;
if (cursor) {
if (cursor.value.age > 18) {
console.log(cursor.value);
}
cursor.continue();
}
};
const adults = await db.users
.filter(user => user.age > 18)
.toArray();
Логика становится более декларативной.
Курсоры являются одной из самых сложных частей IndexedDB.
const request = store.openCursor();
request.onsucc ess = event => {
const cursor = event.target.result;
if (cursor) {
console.log(cursor.value);
cursor.continue();
}
};
await db.users.each(user => {
console.log(user);
});
Либо:
for await (const user of db.users) {
console.log(user);
}
Работа с коллекциями становится значительно удобнее.
const request = store.put({
id: 1,
name: "Updated"
});
Необходимо контролировать состояние транзакции и результат операции.
await db.users.update(
1,
{
name: "Updated"
}
);
Или:
await db.users.put({
id: 1,
name: "Updated"
});
store.delete(1);
С дополнительной обработкой событий:
const request = store.delete(1);
request.onsucc ess = () => {
console.log("Deleted");
};
await db.users.delete(1);
Код выглядит значительно проще и лучше вписывается в современные приложения.
Работа с транзакциями требует ручного управления:
const tx = db.transaction(
["users", "orders"],
"readwrite"
);
const usersStore =
tx.objectStore("users");
const ordersStore =
tx.objectStore("orders");
usersStore.put(user);
ordersStore.put(order);
tx.oncompl ete = () => {
console.log("Done");
};
await db.transaction(
"rw",
db.users,
db.orders,
async () => {
await db.users.put(user);
await db.orders.put(order);
}
);
Транзакционный код становится линейным и хорошо читаемым.
Ошибки распределены между множеством событий:
request.oner ror = event => {
console.error(
event.target.error
);
};
Для крупных приложений возникает большое количество обработчиков.
Используется привычный механизм Promise:
try {
await db.users.add(user);
}
catch (error) {
console.error(error);
}
Логика обработки ошибок становится централизованной.
При обновлении версии необходимо самостоятельно определять изменения:
request.onupgradenee ded = event => {
const db = event.target.result;
if (event.oldVersion < 2) {
db.createObjectStore("orders");
}
if (event.oldVersion < 3) {
const store =
event.currentTarget
.transaction
.objectStore("users");
store.createIndex(
"email",
"email"
);
}
};
Код быстро усложняется по мере роста проекта.
Каждая версия описывается отдельно:
db.version(1).stores({
users: "++id,name"
});
db.version(2).stores({
users: "++id,name",
orders: "++id,date"
});
Дополнительно поддерживаются миграции данных:
db.version(3)
.stores({
users: "++id,name,email"
})
.upgrade(tx => {
return tx.table("users")
.toCollection()
.modify(user => {
user.email = "";
});
});
Подход существенно упрощает сопровождение базы.
Одним из главных преимуществ Dexie является уменьшение объёма шаблонного кода.
Типичный запрос в IndexedDB:
const tx =
db.transaction(
"users"
);
const store =
tx.objectStore(
"users"
);
const index =
store.index(
"email"
);
const request =
index.get(
email
);
request.onsucc ess =
event => {
resolve(
event.target.result
);
};
Эквивалент на Dexie:
const user =
await db.users
.where("email")
.equals(email)
.first();
Разница становится особенно заметной в проектах с десятками таблиц и сотнями запросов.
Dexie не является отдельной системой хранения данных. Все операции в конечном итоге выполняются через IndexedDB.
По этой причине:
Накладные расходы библиотеки обычно минимальны и практически незаметны для большинства приложений.
В реальных проектах выигрыш в производительности разработки и сопровождения значительно превышает незначительные издержки, связанные с дополнительным уровнем абстракции.
Использование чистого IndexedDB может быть оправдано в следующих случаях:
Во всех остальных сценариях Dexie обычно обеспечивает более высокую скорость разработки и лучшую поддерживаемость кода.
Dexie особенно эффективен в следующих категориях приложений:
Главное преимущество библиотеки заключается в том, что она устраняет большую часть сложности IndexedDB, сохраняя доступ ко всем возможностям встроенной браузерной базы данных. Благодаря декларативному описанию схем, поддержке Promise, удобным запросам, встроенным транзакциям и механизмам миграции Dexie превращает IndexedDB из низкоуровневого API в полноценный и удобный инструмент хранения данных на стороне клиента.