IndexedDB: мощное хранилище данных в браузере для веб-разработки
Для кого эта статья:
- Фронтенд-разработчики, желающие углубить свои знания в работе с веб-технологиями.
- Студенты или начинающие специалисты в области веб-разработки, интересующиеся клиентским хранилищем данных.
Профессионалы, которые ищут решения для повышения производительности своих веб-приложений, особенно в условиях оффлайн-доступа.
В мире фронтенд-разработки эффективное управление данными часто становится краеугольным камнем производительности. Когда localStorage задыхается от объема информации, а серверные запросы отнимают драгоценные миллисекунды отклика — на сцену выходит IndexedDB. Этот мощный Web API дает возможность хранить гигабайты структурированных данных прямо в браузере пользователя, обеспечивая молниеносный доступ и надежную офлайн-функциональность. Готовы превратить свое приложение в настоящий дата-центр на стороне клиента? Давайте разберемся, как укротить этого зверя. 🚀
Хотите профессионально освоить IndexedDB и другие передовые технологии веб-разработки? Наш курс Обучение веб-разработке от Skypro предлагает погружение в реальные проекты под руководством практикующих разработчиков. Вы не просто изучите API, а научитесь создавать быстрые, отзывчивые приложения с продвинутым клиентским хранилищем данных. От базовых концепций до сложных паттернов офлайн-первой разработки — мы превратим вас в специалиста, востребованного на рынке.
Основы IndexedDB: структура и преимущества для веб-разработки
IndexedDB — это низкоуровневый API для хранения значительных объемов структурированных данных непосредственно в браузере клиента. В отличие от других методов хранения, IndexedDB предлагает полноценную нереляционную базу данных на стороне клиента, что открывает совершенно новые возможности для веб-приложений. 📊
Ключевые концепции IndexedDB выстроены вокруг нескольких фундаментальных понятий:
- Базы данных (Databases) — контейнеры верхнего уровня, имеющие уникальное имя и версию
- Хранилища объектов (Object Stores) — аналоги таблиц, содержащие коллекции данных
- Ключи (Keys) — уникальные идентификаторы для каждой записи в хранилище
- Индексы (Indexes) — дополнительные пути доступа к данным для оптимизации поиска
- Транзакции (Transactions) — атомарные операции для гарантии целостности данных
- Курсоры (Cursors) — механизмы для итерации по результатам запросов
Архитектура IndexedDB основана на событийной модели и асинхронных операциях, что делает ее идеальной для современных веб-приложений.
| Характеристика | IndexedDB | localStorage | Cookies |
|---|---|---|---|
| Объем хранения | Гигабайты (зависит от браузера) | 5-10 МБ | 4 КБ |
| Тип данных | JavaScript объекты, файлы, блобы | Строки | Строки |
| API | Асинхронный | Синхронный | Синхронный |
| Структурирование | Полноценная БД с индексами | Простые пары ключ-значение | Простые пары ключ-значение |
| Транзакции | Поддерживаются | Не поддерживаются | Не поддерживаются |
Преимущества использования IndexedDB в веб-разработке:
- Офлайн-функциональность — приложение продолжает работать без подключения к сети
- Производительность — снижение нагрузки на сервер и уменьшение задержек
- Масштабируемость — возможность хранения больших объемов данных
- Гибкость — поддержка сложных типов данных, включая бинарные
- Поисковые возможности — оптимизация выборки через индексы
Антон Северов, Lead Frontend Developer
Когда я работал над финтех-приложением с интенсивной аналитикой, мы столкнулись с серьезными проблемами производительности. Клиенты жаловались на "подвисания" при загрузке истории транзакций, а серверные запросы создавали неприемлемые задержки.
Решение пришло неожиданно. Вместо оптимизации бэкенда мы перенесли значительную часть данных в IndexedDB. Реализовали инкрементальную синхронизацию в фоне и кеширование часто запрашиваемых отчетов.
Результат превзошел ожидания: интерфейс стал реагировать мгновенно, нагрузка на сервера упала на 70%, а возможность работы офлайн стала приятным бонусом для пользователей с нестабильным интернетом. IndexedDB буквально спасла проект, когда казалось, что архитектурные ограничения загнали нас в угол.

Создание и подключение IndexedDB базы данных в приложении
Создание и подключение к базе данных IndexedDB — первый шаг в интеграции этого мощного инструмента в ваше веб-приложение. Процесс асинхронный и основан на системе событий, что требует особого подхода к написанию кода. 🔌
Базовый шаблон для подключения к IndexedDB выглядит следующим образом:
// Проверка поддержки IndexedDB в браузере
if (!window.indexedDB) {
console.error("Ваш браузер не поддерживает стабильную версию IndexedDB");
// Возможно, стоит предложить альтернативу
}
// Открытие соединения с базой данных
const dbRequest = window.indexedDB.open("MyAppDatabase", 1);
// Обработчик ошибок
dbRequest.onerror = function(event) {
console.error("Ошибка открытия БД:", event.target.errorCode);
};
// Обработчик успешного подключения
dbRequest.onsuccess = function(event) {
const db = event.target.result;
console.log("База данных успешно открыта");
// Здесь можно начинать работу с базой данных
};
// Обработчик обновления структуры БД
dbRequest.onupgradeneeded = function(event) {
const db = event.target.result;
console.log("Обновление структуры БД");
// Создаем хранилище объектов (таблицу)
const objectStore = db.createObjectStore("users", { keyPath: "id", autoIncrement: true });
// Добавляем индексы для быстрого поиска
objectStore.createIndex("name", "name", { unique: false });
objectStore.createIndex("email", "email", { unique: true });
console.log("Хранилище объектов создано");
}
Важно понимать жизненный цикл подключения к IndexedDB и различать события:
- onsuccess — вызывается, когда соединение успешно установлено
- onerror — вызывается при возникновении ошибки
- onupgradeneeded — критически важное событие, вызываемое при создании новой БД или обновлении версии существующей
Именно в обработчике onupgradeneeded происходит определение структуры базы данных — создание хранилищ объектов и индексов.
При работе с IndexedDB следует учитывать несколько ключевых моментов:
- Версионирование базы данных — при изменении схемы необходимо увеличивать номер версии
- Определение ключевого пути (keyPath) для однозначной идентификации записей
- Настройка автоинкремента для автоматического назначения идентификаторов
- Создание необходимых индексов для оптимизации поиска
Для структурированного управления подключениями к базе данных удобно использовать обертку в виде класса:
class IndexedDBManager {
constructor(dbName, version) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = event => reject(`Ошибка: ${event.target.errorCode}`);
request.onsuccess = event => {
this.db = event.target.result;
resolve(this.db);
};
request.onupgradeneeded = event => {
const db = event.target.result;
// Определение схемы БД при первом создании или обновлении
if (!db.objectStoreNames.contains('users')) {
const usersStore = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
usersStore.createIndex('name', 'name', { unique: false });
usersStore.createIndex('email', 'email', { unique: true });
}
// Определение других хранилищ при необходимости
};
});
}
close() {
if (this.db) {
this.db.close();
this.db = null;
}
}
}
// Использование
const dbManager = new IndexedDBManager('MyAppDB', 1);
dbManager.open()
.then(db => console.log('БД успешно открыта'))
.catch(error => console.error('Ошибка открытия БД:', error));
| Операция | Когда использовать | Особенности |
|---|---|---|
| Создание новой БД | При первом запуске приложения | Срабатывает onupgradeneeded |
| Открытие существующей БД | При каждом запуске приложения | Срабатывает только onsuccess |
| Обновление схемы БД | При необходимости изменить структуру | Требуется увеличение номера версии |
| Удаление БД | При полной очистке данных приложения | Необратимая операция |
| Закрытие соединения | При завершении работы с БД | Рекомендуется для освобождения ресурсов |
Работа с данными: CRUD-операции в IndexedDB с примерами кода
После успешного подключения к базе данных, следующим логическим шагом является выполнение базовых CRUD-операций (Create, Read, Update, Delete). В IndexedDB эти операции выполняются в контексте транзакций, что обеспечивает целостность данных. 📝
Важно понимать, что каждая операция с данными в IndexedDB должна происходить в рамках транзакции. Существует три типа транзакций:
- readonly — для операций чтения (самый быстрый тип транзакций)
- readwrite — для операций чтения и записи
- versionchange — специальный тип для изменения структуры базы данных
Рассмотрим примеры реализации основных операций:
1. Создание (добавление) записи:
function addUser(db, userData) {
return new Promise((resolve, reject) => {
// Создаем транзакцию
const transaction = db.transaction(['users'], 'readwrite');
// Получаем хранилище объектов
const store = transaction.objectStore('users');
// Выполняем операцию добавления
const request = store.add(userData);
// Обработчики событий
request.onsuccess = event => {
resolve(event.target.result); // Возвращаем ID новой записи
};
request.onerror = event => {
reject(`Ошибка добавления: ${event.target.error}`);
};
});
}
// Пример использования
const user = {
name: 'Иван Петров',
email: 'ivan@example.com',
age: 28,
registrationDate: new Date()
};
dbManager.open()
.then(db => addUser(db, user))
.then(userId => console.log(`Пользователь добавлен с ID: ${userId}`))
.catch(error => console.error(error));
2. Чтение данных:
function getUserById(db, userId) {
return new Promise((resolve, reject) => {
// Создаем транзакцию только для чтения
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
// Получаем запись по ключу
const request = store.get(userId);
request.onsuccess = event => {
const result = event.target.result;
if (result) {
resolve(result);
} else {
reject(`Пользователь с ID ${userId} не найден`);
}
};
request.onerror = event => {
reject(`Ошибка получения данных: ${event.target.error}`);
};
});
}
// Получение всех пользователей
function getAllUsers(db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.getAll();
request.onsuccess = event => {
resolve(event.target.result);
};
request.onerror = event => {
reject(`Ошибка получения данных: ${event.target.error}`);
};
});
}
3. Обновление записи:
function updateUser(db, userData) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
// Метод put обновит запись, если она существует, иначе создаст новую
const request = store.put(userData);
request.onsuccess = event => {
resolve(event.target.result);
};
request.onerror = event => {
reject(`Ошибка обновления: ${event.target.error}`);
};
});
}
// Пример обновления
dbManager.open()
.then(db => getUserById(db, 1))
.then(user => {
user.age = 29;
user.lastModified = new Date();
return dbManager.open().then(db => updateUser(db, user));
})
.then(() => console.log('Пользователь обновлен'))
.catch(error => console.error(error));
4. Удаление записи:
function deleteUser(db, userId) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
// Удаление записи по ключу
const request = store.delete(userId);
request.onsuccess = event => {
resolve();
};
request.onerror = event => {
reject(`Ошибка удаления: ${event.target.error}`);
};
});
}
// Удаление всех записей
function clearUsers(db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.clear();
request.onsuccess = event => {
resolve();
};
request.onerror = event => {
reject(`Ошибка очистки хранилища: ${event.target.error}`);
};
});
}
Для выполнения нескольких операций в рамках одной транзакции используйте следующий паттерн:
function batchOperations(db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
// Обработчики на уровне транзакции
transaction.oncomplete = event => {
resolve('Все операции выполнены успешно');
};
transaction.onerror = event => {
reject(`Транзакция прервана: ${event.target.error}`);
};
// Последовательные операции в рамках одной транзакции
store.add({ name: 'Пользователь 1', email: 'user1@example.com' });
store.add({ name: 'Пользователь 2', email: 'user2@example.com' });
const keyToUpdate = 5;
const getRequest = store.get(keyToUpdate);
getRequest.onsuccess = event => {
const data = event.target.result;
if (data) {
data.lastAccess = new Date();
store.put(data);
}
};
});
}
Мария Захарова, Frontend Team Lead
При разработке CRM-системы для медицинской клиники мы столкнулись с необходимостью хранить данные пациентов локально для быстрого доступа, но с периодической синхронизацией с сервером. Особенно критичным был доступ к карточкам пациентов во время обхода врача, когда интернет-соединение могло быть нестабильным.
Реализация CRUD-операций через IndexedDB потребовала полного пересмотра архитектуры фронтенда. Мы внедрили слой абстракции, который перенаправлял все запросы к данным сначала в локальную базу, а потом уже на сервер.
Самым сложным оказалось корректно обрабатывать конфликты при синхронизации — когда данные менялись и локально, и на сервере. Пришлось реализовать сложную систему временных меток и разрешения конфликтов. Но результат того стоил — врачи получили возможность работать с системой даже при временном отсутствии связи, а скорость работы приложения выросла в разы.
Продвинутые техники: индексы, курсоры и транзакции в IndexedDB
Для создания эффективных приложений с IndexedDB недостаточно знать только базовые CRUD-операции. Продвинутые техники работы с индексами, курсорами и транзакциями открывают новый уровень производительности и гибкости. 🔍
Работа с индексами
Индексы в IndexedDB работают аналогично индексам в традиционных СУБД — они ускоряют поиск по определенным полям. Без индексов для поиска записи по значению поля пришлось бы перебирать все записи, что крайне неэффективно.
Создание индекса происходит при определении схемы хранилища объектов:
request.onupgradeneeded = function(event) {
const db = event.target.result;
const store = db.createObjectStore('customers', { keyPath: 'id' });
// Создаем индекс по полю email (уникальный)
store.createIndex('email', 'email', { unique: true });
// Индекс по имени (не уникальный)
store.createIndex('name', 'name', { unique: false });
// Составной индекс по городу и стране
store.createIndex('city_country', ['city', 'country'], { unique: false });
// Индекс с включением дополнительных полей для оптимизации
store.createIndex('age', 'age', { unique: false, multiEntry: false });
};
Использование индексов для поиска:
function findCustomersByName(db, nameQuery) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['customers'], 'readonly');
const store = transaction.objectStore('customers');
// Используем индекс "name"
const index = store.index('name');
// Метод getAll с IDBKeyRange для поиска по диапазону
const request = index.getAll(IDBKeyRange.bound(nameQuery, nameQuery + '\uffff'));
request.onsuccess = event => {
resolve(event.target.result);
};
request.onerror = event => {
reject(`Ошибка поиска: ${event.target.error}`);
};
});
}
// Пример поиска с использованием составного индекса
function findCustomersInLocation(db, city, country) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['customers'], 'readonly');
const store = transaction.objectStore('customers');
const index = store.index('city_country');
const request = index.getAll([city, country]);
request.onsuccess = event => {
resolve(event.target.result);
};
request.onerror = event => {
reject(`Ошибка поиска: ${event.target.error}`);
};
});
}
Использование курсоров
Курсоры предоставляют эффективный способ перебора больших наборов данных и выполнения операций над ними. Вместо загрузки всех записей в память, курсоры позволяют обрабатывать записи по одной.
function processCustomers(db, processCallback, ageLimit = 18) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['customers'], 'readonly');
const store = transaction.objectStore('customers');
const index = store.index('age');
// Создаем границы диапазона: от ageLimit и выше
const range = IDBKeyRange.lowerBound(ageLimit);
// Открываем курсор с указанным диапазоном
const cursorRequest = index.openCursor(range);
const results = [];
cursorRequest.onsuccess = event => {
const cursor = event.target.result;
if (cursor) {
// Обрабатываем текущую запись
const processedData = processCallback(cursor.value);
results.push(processedData);
// Переходим к следующей записи
cursor.continue();
} else {
// Курсор завершил обход
resolve(results);
}
};
cursorRequest.onerror = event => {
reject(`Ошибка курсора: ${event.target.error}`);
};
});
}
// Пример использования
dbManager.open()
.then(db => {
return processCustomers(db, customer => {
// Трансформация данных
return {
fullName: `${customer.name} ${customer.surname}`,
isActive: customer.lastLogin > (Date.now() – 30 * 24 * 60 * 60 * 1000)
};
}, 21); // Минимальный возраст – 21 год
})
.then(results => console.log('Обработано записей:', results.length))
.catch(error => console.error(error));
Продвинутая работа с транзакциями
Правильное использование транзакций критически важно для производительности и надежности приложения с IndexedDB.
| Аспект транзакций | Рекомендации | Примеры использования |
|---|---|---|
| Время жизни | Транзакции автоматически завершаются, если нет активных запросов | Подготовить все операции заранее, не выполнять асинхронные операции вне транзакции |
| Обработка ошибок | Одна ошибка отменяет всю транзакцию | Использовать try/catch и обработчики onerror на всех уровнях |
| Изоляция | Транзакции изолированы, но могут блокировать друг друга | Минимизировать время жизни транзакций, особенно readwrite |
| Вложенность | IndexedDB не поддерживает вложенные транзакции | Планировать структуру хранилищ так, чтобы минимизировать зависимости |
| Производительность | Группировка операций в транзакции намного эффективнее | Объединять связанные операции в одну транзакцию |
Пример оптимизированной работы с транзакциями:
function optimizedBatchUpdate(db, items) {
return new Promise((resolve, reject) => {
// Создаем одну транзакцию для всех операций
const transaction = db.transaction(['inventory', 'orders'], 'readwrite');
const inventoryStore = transaction.objectStore('inventory');
const ordersStore = transaction.objectStore('orders');
// Обработчики на уровне транзакции
transaction.oncomplete = () => resolve('Обновление завершено успешно');
transaction.onerror = event => reject(`Транзакция прервана: ${event.target.error}`);
// Оптимизация: предварительно подготовленный массив с результатами
// операций, которые мы хотим отслеживать
const updateResults = new Array(items.length).fill(false);
let completedCount = 0;
// Выполняем все обновления в рамках одной транзакции
items.forEach((item, index) => {
// Обновляем запись в инвентаре
const inventoryRequest = inventoryStore.get(item.productId);
inventoryRequest.onsuccess = event => {
const product = event.target.result;
if (product && product.stock >= item.quantity) {
// Уменьшаем количество в инвентаре
product.stock -= item.quantity;
inventoryStore.put(product);
// Создаем запись в заказах
const order = {
productId: item.productId,
quantity: item.quantity,
customerId: item.customerId,
orderDate: new Date(),
status: 'pending'
};
ordersStore.add(order).onsuccess = () => {
updateResults[index] = true;
completedCount++;
};
} else {
// Не хватает товара на складе
completedCount++;
}
};
inventoryRequest.onerror = () => completedCount++;
});
});
}
Оптимизация работы с большими объемами данных
При работе с большими наборами данных рекомендуется использовать следующие техники:
- Отложенная загрузка (Lazy Loading) — использовать курсоры с ограничением count для загрузки только части данных
- Пагинация — реализовать механизм постраничной навигации с помощью курсоров
- Индексы с включением (включение дополнительных полей) — оптимизировать запросы, требующие нескольких полей
- Составные индексы — для оптимизации сложных запросов с несколькими условиями
- Асинхронная обработка — использовать Web Workers для тяжелых операций с данными
// Пример пагинации с курсорами
function paginateData(db, storeName, pageSize, pageNum) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
let advanceCount = pageSize * pageNum;
let count = 0;
const data = [];
const cursorRequest = store.openCursor();
cursorRequest.onsuccess = event => {
const cursor = event.target.result;
if (!cursor) {
// Больше записей нет
resolve({ data, hasMore: false, totalCount: count });
return;
}
if (advanceCount > 0) {
// Пропускаем записи для предыдущих страниц
cursor.advance(advanceCount);
advanceCount = 0;
return;
}
if (count < pageSize) {
// Собираем данные для текущей страницы
data.push(cursor.value);
count++;
cursor.continue();
} else {
// Достигли конца страницы
resolve({ data, hasMore: true, totalCount: count + pageSize * pageNum });
}
};
cursorRequest.onerror = event => {
reject(`Ошибка курсора: ${event.target.error}`);
};
});
}
Реализация офлайн-функциональности с помощью IndexedDB
Одно из главных преимуществ IndexedDB — возможность создавать полноценные офлайн-приложения, которые продолжают работать даже при отсутствии подключения к интернету. Такая функциональность критически важна для мобильных приложений, прогрессивных веб-приложений (PWA) и сервисов, используемых в условиях нестабильного соединения. 📱
Для реализации офлайн-функциональности с IndexedDB необходимо решить несколько ключевых задач:
- Определение стратегии синхронизации данных между клиентом и сервером
- Обнаружение состояния подключения и реакция на его изменения
- Кеширование и управление данными в офлайн-режиме
- Обработка конфликтов при синхронизации
Рассмотрим пошаговую реализацию офлайн-функциональности:
1. Обнаружение состояния сети
class NetworkStatusService {
constructor() {
this._isOnline = navigator.onLine;
this._listeners = [];
// Подписываемся на события браузера
window.addEventListener('online', () => this._updateStatus(true));
window.addEventListener('offline', () => this._updateStatus(false));
}
_updateStatus(isOnline) {
this._isOnline = isOnline;
this._notifyListeners();
}
_notifyListeners() {
for (const listener of this._listeners) {
listener(this._isOnline);
}
}
isOnline() {
return this._isOnline;
}
addListener(callback) {
this._listeners.push(callback);
return () => {
this._listeners = this._listeners.filter(listener => listener !== callback);
};
}
}
const networkStatus = new NetworkStatusService();
// Пример использования
networkStatus.addListener(isOnline => {
if (isOnline) {
console.log('Подключение восстановлено, начинаем синхронизацию');
syncManager.synchronize();
} else {
console.log('Подключение потеряно, переходим в офлайн-режим');
notifyUser('Вы работаете в офлайн-режиме. Изменения будут синхронизированы при восстановлении подключения.');
}
});
2. Реализация очереди операций
Ключевой компонент офлайн-функциональности — очередь операций, которые накапливаются при отсутствии подключения и выполняются при его восстановлении.
class OfflineOperationQueue {
constructor(dbManager) {
this.dbManager = dbManager;
this.initialized = false;
this._init();
}
async _init() {
try {
const db = await this.dbManager.open();
// Создаем хранилище для операций, если это первый запуск
if (!db.objectStoreNames.contains('operationsQueue')) {
const version = db.version + 1;
this.dbManager.close();
const newDb = await new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbManager.dbName, version);
request.onupgradeneeded = event => {
const db = event.target.result;
db.createObjectStore('operationsQueue', { keyPath: 'id', autoIncrement: true });
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
this.dbManager.db = newDb;
}
this.initialized = true;
} catch (error) {
console.error('Failed to initialize OfflineOperationQueue:', error);
}
}
async addOperation(operation) {
if (!this.initialized) {
await this._init();
}
const db = await this.dbManager.open();
const transaction = db.transaction(['operationsQueue'], 'readwrite');
const store = transaction.objectStore('operationsQueue');
const operationData = {
type: operation.type, // 'create', 'update', 'delete'
entity: operation.entity, // название сущности/таблицы
data: operation.data, // данные операции
timestamp: Date.now(),
attempts: 0
};
return new Promise((resolve, reject) => {
const request = store.add(operationData);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getOperations() {
if (!this.initialized) {
await this._init();
}
const db = await this.dbManager.open();
const transaction = db.transaction(['operationsQueue'], 'readonly');
const store = transaction.objectStore('operationsQueue');
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async removeOperation(id) {
if (!this.initialized) {
await this._init();
}
const db = await this.dbManager.open();
const transaction = db.transaction(['operationsQueue'], 'readwrite');
const store = transaction.objectStore('operationsQueue');
return new Promise((resolve, reject) => {
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async updateOperationAttempts(id, attempts) {
if (!this.initialized) {
await this._init();
}
const db = await this.dbManager.open();
const transaction = db.transaction(['operationsQueue'], 'readwrite');
const store = transaction.objectStore('operationsQueue');
return new Promise(async (resolve, reject) => {
const getRequest = store.get(id);
getRequest.onsuccess = () => {
const operation = getRequest.result;
if (operation) {
operation.attempts = attempts;
const updateRequest = store.put(operation);
updateRequest.onsuccess = () => resolve();
updateRequest.onerror = () => reject(updateRequest.error);
} else {
reject(new Error(`Operation with id ${id} not found`));
}
};
getRequest.onerror = () => reject(getRequest.error);
});
}
}
3. Реализация сервиса синхронизации
class SynchronizationService {
constructor(dbManager, apiService) {
this.dbManager = dbManager;
this.apiService = apiService;
this.operationQueue = new OfflineOperationQueue(dbManager);
this.isSynchronizing = false;
// Подписываемся на изменение статуса сети
networkStatus.addListener(isOnline => {
if (isOnline && !this.isSynchronizing) {
this.synchronize();
}
});
}
async synchronize() {
if (this.isSynchronizing || !networkStatus.isOnline()) {
return false;
}
this.isSynchronizing = true;
let success = true;
try {
// Получаем все накопленные операции
const operations = await this.operationQueue.getOperations();
// Сортируем операции по времени создания
operations.sort((a, b) => a.timestamp – b.timestamp);
// Обрабатываем операции последовательно
for (const operation of operations) {
try {
let apiResult;
switch (operation.type) {
case 'create':
apiResult = await this.apiService.create(
operation.entity,
operation.data
);
break;
case 'update':
apiResult = await this.apiService.update(
operation.entity,
operation.data.id,
operation.data
);
break;
case 'delete':
apiResult = await this.apiService.delete(
operation.entity,
operation.data.id
);
break;
}
// Если операция успешна, удаляем её из очереди
await this.operationQueue.removeOperation(operation.id);
// Если это была операция создания, обновляем ID в локальной БД
if (operation.type === 'create' && apiResult && apiResult.id) {
const db = await this.dbManager.open();
const transaction = db.transaction([operation.entity], 'readwrite');
const store = transaction.objectStore(operation.entity);
// Заменяем временный ID на реальный от сервера
await new Promise((resolve, reject) => {
const getRequest = store.get(operation.data.id);
getRequest.onsuccess = () => {
const item = getRequest.result;
if (item) {
// Удаляем запись с временным ID
store.delete(operation.data.id).onsuccess = () => {
// Создаем новую запись с правильным ID
item.id = apiResult.id;
store.add(item).onsuccess = () => resolve();
};
} else {
resolve();
}
};
getRequest.onerror = () => reject(getRequest.error);
});
}
} catch (error) {
console.error(`Failed to process operation ${operation.id}:`, error);
// Увеличиваем счетчик попыток
operation.attempts += 1;
// Если слишком много попыток, помечаем операцию как проблемную
if (operation.attempts >= 3) {
// Можно добавить логику разрешения конфликтов или
// уведомления пользователя
console.warn(`Operation ${operation.id} failed too many times`);
}
await this.operationQueue.updateOperationAttempts(
operation.id,
operation.attempts
);
success = false;
}
}
// Загружаем новые данные с сервера
await this.downloadLatestData();
} catch (error) {
console.error('Synchronization failed:', error);
success = false;
} finally {
this.isSynchronizing = false;
}
return success;
}
async downloadLatestData() {
// Получаем последнюю метку времени синхронизации
const lastSyncTimestamp = localStorage.getItem('lastSyncTimestamp') || 0;
// Загружаем только изменения после последней синхронизации
const entities = ['users', 'products', 'orders']; // список сущностей
for (const entity of entities) {
try {
const updatedItems = await this.apiService.getUpdated(
entity,
lastSyncTimestamp
);
if (updatedItems && updatedItems.length) {
const db = await this.dbManager.open();
const transaction = db.transaction([entity], 'readwrite');
const store = transaction.objectStore(entity);
// Обновляем или добавляем записи в локальную БД
for (const item of updatedItems) {
store.put(item);
}
}
} catch (error) {
console.error(`Failed to sync ${entity}:`, error);
}
}
// Обновляем метку времени
localStorage.setItem('lastSyncTimestamp', Date.now());
}
// Метод для интеграции с формами и UI
async processOfflineAction(type, entity, data) {
// Добавляем операцию в очередь
const operationId = await this.operationQueue.addOperation({
type,
entity,
data
});
// Если мы онлайн, сразу запускаем синхронизацию
if (networkStatus.isOnline()) {
this.synchronize();
}
return operationId;
}
}
// Пример использования
const apiService = {
create: async (entity, data) => {
const response = await fetch(`https://api.example.com/${entity}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
},
update: async (entity, id, data) => {
const response = await fetch(`https://api.example.com/${entity}/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
},
delete: async (entity, id) => {
const response = await fetch(`https://api.example.com/${entity}/${id}`, {
method: 'DELETE'
});
return response.status === 204 ? true : response.json();
},
getUpdated: async (entity, timestamp) => {
const response = await fetch(
`https://api.example.com/${entity}?updated_after=${timestamp}`
);
return response.json();
}
};
// Создаем сервис синхронизации
const syncManager = new SynchronizationService(dbManager, ap