IndexedDB: мощное хранилище данных в браузере для веб-разработки

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Фронтенд-разработчики, желающие углубить свои знания в работе с веб-технологиями.
  • Студенты или начинающие специалисты в области веб-разработки, интересующиеся клиентским хранилищем данных.
  • Профессионалы, которые ищут решения для повышения производительности своих веб-приложений, особенно в условиях оффлайн-доступа.

    В мире фронтенд-разработки эффективное управление данными часто становится краеугольным камнем производительности. Когда 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 выглядит следующим образом:

JS
Скопировать код
// Проверка поддержки 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) для однозначной идентификации записей
  • Настройка автоинкремента для автоматического назначения идентификаторов
  • Создание необходимых индексов для оптимизации поиска

Для структурированного управления подключениями к базе данных удобно использовать обертку в виде класса:

JS
Скопировать код
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. Создание (добавление) записи:

JS
Скопировать код
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. Чтение данных:

JS
Скопировать код
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. Обновление записи:

JS
Скопировать код
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. Удаление записи:

JS
Скопировать код
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}`);
};
});
}

Для выполнения нескольких операций в рамках одной транзакции используйте следующий паттерн:

JS
Скопировать код
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 работают аналогично индексам в традиционных СУБД — они ускоряют поиск по определенным полям. Без индексов для поиска записи по значению поля пришлось бы перебирать все записи, что крайне неэффективно.

Создание индекса происходит при определении схемы хранилища объектов:

JS
Скопировать код
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 });
};

Использование индексов для поиска:

JS
Скопировать код
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}`);
};
});
}

Использование курсоров

Курсоры предоставляют эффективный способ перебора больших наборов данных и выполнения операций над ними. Вместо загрузки всех записей в память, курсоры позволяют обрабатывать записи по одной.

JS
Скопировать код
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 не поддерживает вложенные транзакции Планировать структуру хранилищ так, чтобы минимизировать зависимости
Производительность Группировка операций в транзакции намного эффективнее Объединять связанные операции в одну транзакцию

Пример оптимизированной работы с транзакциями:

JS
Скопировать код
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 для тяжелых операций с данными
JS
Скопировать код
// Пример пагинации с курсорами
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. Обнаружение состояния сети

JS
Скопировать код
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. Реализация очереди операций

Ключевой компонент офлайн-функциональности — очередь операций, которые накапливаются при отсутствии подключения и выполняются при его восстановлении.

JS
Скопировать код
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. Реализация сервиса синхронизации

JS
Скопировать код
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

Загрузка...