IndexedDB: руководство по работе в браузере

Что такое IndexedDB
IndexedDB — это встроенная в браузер транзакционная клиентская база данных (NoSQL). Она хранит сложные объекты (JSON), поддерживает индексы, транзакции и оффлайн‑доступ. В отличие от localStorage, IndexedDB рассчитана на большие объёмы данных, асинхронные операции и более гибкие схемы доступа.
Ключевые свойства в одной строке:
- NoSQL: храним объекты, а не таблицы с жёсткой схемой.
- Транзакции: группировка операций с откатом при ошибке.
- Индексы: быстрый поиск и сортировка по полям.
- Оффлайн: данные доступны без сети.
Настройка базы данных
Чтобы создать или открыть базу, используйте indexedDB.open(name, version). Метод возвращает объект IDBOpenDBRequest, на котором нужно слушать события: success, error и upgradeneeded. Версия позволяет обновлять структуру базы и триггерит событие upgradeneeded.
Пример: открыть базу usersdb версии 1:
const openRequest = indexedDB.open("usersdb", 1);
openRequest.onsuccess = function (event) {
const db = event.target.result;
console.log("Database opened", db);
};
openRequest.onerror = function (event) {
console.error("Open error", event.target.error);
};
openRequest.onupgradeneeded = function (event) {
const db = event.target.result;
// Здесь создаём object store и индексы (см. раздел ниже)
};Важно: upgradeneeded выполняется только при создании базы или при повышении версии. Все изменения схемы (создание/удаление object store или индексов) нужно делать внутри обработчика upgradeneeded.
Создание объекта‑хранилища (object store)
Object store в IndexedDB похож на таблицу, но хранит произвольные объекты. При создании указывают ключ (keyPath) или включают автоинкремент.
Пример создания object store с автоинкрементируемым ключом id:
openRequest.onupgradeneeded = function (event) {
const db = event.target.result;
const userObjectStore = db.createObjectStore("userStore", {
keyPath: "id",
autoIncrement: true,
});
// Создание индексов
userObjectStore.createIndex("name", "name", { unique: false });
userObjectStore.createIndex("email", "email", { unique: true });
};Пояснения:
- keyPath — путь к свойству объекта, которое используется как первичный ключ.
- autoIncrement — если true и ключ отсутствует, браузер сгенерирует числовой ключ.
- Удаление или изменение object store нельзя делать в обычных success‑обработчиках; только в upgradeneeded.
Индексы и поиск
Индекс ускоряет поиск по полю. Индекс создаётся методом createIndex(name, keyPath, options). Если индекс уникальный (unique: true), попытка добавления дубликата вызовет ошибку.
Пример создания индексов (см. выше). Для выполнения поиска по индексу используйте индекс через objectStore.index(name):
const transaction = db.transaction("userStore", "readonly");
const store = transaction.objectStore("userStore");
const index = store.index("email");
const request = index.get("user@example.com");
request.onsuccess = function () {
console.log("User by email:", request.result);
};Используйте openCursor/getAll для выборок с диапазоном или постраничной загрузки.
Добавление данных
Все операции на запись должны выполняться в транзакции с режимом “readwrite”. Для добавления используйте add() (если хотите ошибку при совпадении ключа) или put() (если хотите заменить существующую запись).
Пример функции добавления:
const addUserData = (userData, db) => {
const transaction = db.transaction("userStore", "readwrite");
const userObjectStore = transaction.objectStore("userStore");
const request = userObjectStore.add(userData);
request.onsuccess = function () {
console.log("User added", request.result);
};
request.onerror = function (event) {
console.error("Add error", event.target.error);
};
};Советы:
- Проверяйте, что данные валидны до вызова add/put (валидация на уровне приложения).
- Используйте индексы для поиска перед добавлением, если требуется уникальность по составному набору полей.
Получение данных
Для получения отдельных объектов используйте get(key), для всех — getAll([query]).
Пример получения по id:
const getUserData = (id, db) => {
const transaction = db.transaction("userStore", "readonly");
const userObjectStore = transaction.objectStore("userStore");
const request = userObjectStore.get(id);
request.onsuccess = function () {
console.log(request.result);
};
request.onerror = function (event) {
console.error("Get error", event.target.error);
};
};Для диапазонных запросов используйте IDBKeyRange и курсоры:
const range = IDBKeyRange.bound(10, 20); // ключи от 10 до 20
const cursorRequest = userObjectStore.openCursor(range);
cursorRequest.onsuccess = function (event) {
const cursor = event.target.result;
if (cursor) {
console.log(cursor.key, cursor.value);
cursor.continue();
}
};Обновление данных
Алгоритм: открыть транзакцию readwrite, получить объект через get(), изменить поля, записать назад через put(). put() вставит объект, если ключа нет, или заменит существующий.
Пример:
const updateUserData = (id, newUserData, db) => {
const transaction = db.transaction("userStore", "readwrite");
const store = transaction.objectStore("userStore");
const getRequest = store.get(id);
getRequest.onsuccess = function (event) {
const user = event.target.result;
if (!user) {
console.warn("User not found", id);
return;
}
// Обновляем поля
user.name = newUserData.name || user.name;
user.email = newUserData.email || user.email;
const putRequest = store.put(user);
putRequest.onsuccess = function () {
console.log("User updated");
};
putRequest.onerror = function (e) {
console.error("Put error", e.target.error);
};
};
getRequest.onerror = function (e) {
console.error("Get for update error", e.target.error);
};
};Удаление данных
Удаление выполняется через delete(key) в транзакции readwrite.
const deleteUserData = (id, db) => {
const transaction = db.transaction("userStore", "readwrite");
const store = transaction.objectStore("userStore");
const request = store.delete(id);
request.onsuccess = function () {
console.log("User deleted", id);
};
request.onerror = function (e) {
console.error("Delete error", e.target.error);
};
};Транзакции и обработка ошибок
Транзакция автоматически закрывается при завершении всех запросов. Если один из запросов вызвал ошибку и не был обработан, транзакция может откатиться. Подписывайтесь на transaction.oncomplete, transaction.onerror, transaction.onabort для логики завершения/отката.
Пример отслеживания транзакции:
const tx = db.transaction("userStore", "readwrite");
const store = tx.objectStore("userStore");
tx.oncomplete = () => console.log("Transaction complete");
tx.onerror = (e) => console.error("Transaction error", e.target.error);
tx.onabort = () => console.warn("Transaction aborted");Когда использовать IndexedDB, а когда localStorage
Используйте localStorage, если:
- Нужны простые пары «ключ‑значение» строкового типа.
- Объём данных очень мал и не требует индексации.
- Нужна синхронная блокирующая API (обычно не рекомендуется).
Используйте IndexedDB, если:
- Нужны большие объёмы данных (мультимедиа, каталоги, кеши).
- Требуется поиск, сортировка и индексация по полям.
- Нужна транзакционная целостность.
Краткая матрица сравнения:
| Свойство | localStorage | IndexedDB |
|---|---|---|
| Асинхронность | нет (синхронный) | да (асинхронный) |
| Объём данных | небольшой | большой |
| Индексы | нет | есть |
| Транзакции | нет | есть |
Примеры отказов и ограничения
Когда IndexedDB может подвести:
- Ограничения квот браузера: в некоторых окружениях (мобильные браузеры, private mode) объём хранения ограничен или отсутствует.
- Совместимость: старые браузеры могут не поддерживать некоторые возможности (но базовый IndexedDB широкий).
- Сложность: асинхронная модель и обработка версий требует аккуратной архитектуры.
Контрпример: для хранения простого флага “tema=dark” localStorage проще и надёжнее.
Альтернативные подходы
- Service Worker + Cache API — подходит для кеширования HTTP‑ресурсов.
- Web SQL (устаревший) — не рекомендуется.
- Использование поверх IndexedDB библиотек (localForage, Dexie.js, idb) — облегчают работу с API и управлением версиями.
Совет: для производственных проектов часто используют обёртки (Dexie, idb) — они предоставляют промисы, удобные API и миграции.
Модель мышления и эвристики
- Думайте в терминах объектов, а не таблиц: храните сущности как JSON.
- Разделяйте данные по object store для логической изоляции и производительности.
- Проектируйте индексы только для полей, по которым реально будете искать.
- Всегда планируйте миграции схемы (upgradeneeded) с учётом отката.
Миграции и совместимость версий
- Любые изменения структуры (создание/удаление object store, индексов) выполняйте в upgradeneeded.
- Пишите idempotent‑миграции: проверяйте наличие store/index перед созданием.
Пример безопасной миграции:
openRequest.onupgradeneeded = function (event) {
const db = event.target.result;
if (!db.objectStoreNames.contains("userStore")) {
const store = db.createObjectStore("userStore", { keyPath: "id", autoIncrement: true });
store.createIndex("email", "email", { unique: true });
}
};Критерии приёмки
- База успешно открывается и инициализируется при запуске.
- В upgradeneeded корректно создаются все необходимые object store и индексы.
- Операции CRUD выполняются без необработанных ошибок и возвращают ожидаемые результаты.
- Транзакции завершаются (oncomplete) или корректно откатываются (onabort) при ошибках.
- Тесты покрывают миграции (включая повторный вызов upgradeneeded с increment версии).
Шпаргалка: часто используемые операции
- Открыть базу: indexedDB.open(name, version)
- Создать object store: db.createObjectStore(name, {keyPath, autoIncrement})
- Создать индекс: store.createIndex(name, keyPath, options)
- Транзакция: db.transaction(storeName, “readwrite”|”readonly”)
- Добавить: store.add(obj)
- Обновить: store.put(obj)
- Получить по ключу: store.get(key)
- Получить все: store.getAll()
- Удалить: store.delete(key)
- Курсор: store.openCursor(range)
Безопасность и соответствие (GDPR)
- Данные в IndexedDB хранятся локально в устройстве пользователя. При хранении персональных данных соблюдайте принципы минимизации и оповещайте пользователей о хранении данных.
- Реализуйте шифрование на уровне приложения, если в базе хранится чувствительная информация (например, токены, персональные данные). Браузерное хранилище само по себе не шифрует данные.
- Предусмотрите механизм удаления данных по запросу пользователя (право на удаление). Удаление должно корректно выполняться и подтверждаться.
- Документируйте, какие данные и где хранятся, чтобы соответствовать требованиям прозрачности.
Чек‑лист по внедрению (роли)
Разработчик:
- Реализовал open/upgradeneeded с миграциями.
- Обработал success/error для всех запросов.
- Написал тесты CRUD и миграций.
Тестировщик:
- Проверил добавление/обновление/удаление/чтение.
- Смоделировал ошибки транзакций и подтвердил откат.
Менеджер продукта:
- Подтвердил, что данные, хранящиеся локально, соответствуют политике конфиденциальности.
- Определил требования к объёму и срокам хранения.
Инцидентный сценарий и восстановление
Если данные повреждены или миграция прошла некорректно:
- Локализуйте проблему: просмотрите консоль браузера и логи onerror/onabort.
- Если возможно, откатите фронтенд на предыдущую версию, ожидающую старой схемы.
- Реализуйте idempotent‑миграцию, которая корректно преобразует существующие записи.
- Сообщите пользователю о возможной потере локальных данных и предложите опции (например, сброс локального кэша).
- Обновите тесты миграций и CI, чтобы предотвратить повторение.
Тесты и критерии приёмки
- Unit: мокировать indexedDB или использовать in‑memory реализацию (например, fakeIndexedDB) для тестирования CRUD.
- Integration: прогонять реальные операции в контролируемом окружении браузера.
- Acceptance: при успешной миграции версии данные доступны и консистентны; при ошибке транзакция откатывается.
Короткое резюме
IndexedDB — мощный инструмент для клиентского хранения структурированных и больших данных. Он поддерживает транзакции, индексы и оффлайн‑доступ, но требует аккуратности при проектировании схемы и миграций. Для упрощения разработки рассмотрите использование проверенных библиотек‑обёрток.
Важно: чаще всего в реальных проектах используют библиотеку‑абстракцию (Dexie, idb или localForage) для удобства и устойчивости.
Похожие материалы
Обзор Nothing Launcher — установка и отзывы
Каррирование функций в JavaScript
Открытый драйвер AMD для Linux — как включить
Как смотреть WWDC 2022 в прямом эфире
Как включить Face ID с маской на iPhone