Полное руководство по IndexedDB: от основ API до примеров кода
#Веб-разработка #Web APIДля кого эта статья:
- Веб-разработчики, желающие улучшить свои навыки работы с базами данных на стороне клиента.
- Программисты, интересующиеся созданием офлайн-приложений и кэшированием данных.
- Фронтендеры, стремящиеся повысить производительность своих приложений и использовать современные веб-технологии.
IndexedDB — это мощное хранилище данных
#Веб-разработка #Web APIIndexedDB — это мощное хранилище данных, которое давно перестало быть экзотикой для продвинутых фронтендеров. Забудьте об ограничениях localStorage и сложности работы с SQL — IndexedDB дает вашим приложениям возможность хранить гигабайты данных прямо в браузере пользователя, работать офлайн и выполнять сложные операции с данными. В этом руководстве я расскажу, как превратить IndexedDB из пугающего монстра в удобный инструмент, и покажу, почему ведущие веб-разработчики всё чаще выбирают его для своих проектов. Готовы вывести производительность вашего приложения на новый уровень? 🚀
Что такое IndexedDB и почему стоит его изучить
IndexedDB — это низкоуровневый API для хранения значительных объёмов структурированных данных на стороне клиента. В отличие от localStorage, который ограничен примерно 5 МБ и может хранить только строки, IndexedDB позволяет работать с гигабайтами информации в различных форматах — от текста до файлов и даже бинарных объектов (Blob).
Первое, что необходимо понять — IndexedDB представляет собой объектное хранилище с поддержкой транзакций. Это означает, что вы работаете не с таблицами и строками как в SQL, а с коллекциями объектов JavaScript. Такая концепция значительно упрощает интеграцию базы данных с современными JavaScript-приложениями.
Почему стоит инвестировать время в изучение IndexedDB? Вот несколько весомых причин:
- Работа в офлайн-режиме — приложение может функционировать без подключения к сети
- Высокая производительность при работе с большими объёмами данных
- Поддержка сложных запросов с использованием индексов
- Асинхронное API, которое не блокирует основной поток выполнения
- Расширенные возможности для кэширования данных
Максим Котов, Lead Frontend Developer
Помню, как несколько лет назад мы столкнулись с задачей разработать приложение для полевых инженеров, которые часто работают в местах без стабильного интернет-соединения. Мы начали с
localStorage, но быстро упёрлись в его ограничения — инженеры загружали отчёты с фотографиями, и нам требовалось хранилище на десятки мегабайт.IndexedDB стал для нас настоящим спасением. Несмотря на первоначальную сложность API, мы смогли создать надёжное решение, которое позволяло инженерам заполнять и хранить отчёты локально, а затем синхронизировать их с сервером при появлении соединения. Кривая обучения была крутой, но результат превзошёл все ожидания — количество потерянных данных сократилось до нуля, а производительность приложения выросла в разы.
Сравним IndexedDB с другими технологиями хранения данных в браузере:
| Технология | Объём хранения | Тип данных | API | Транзакции |
|---|---|---|---|---|
| Cookies | 4 КБ | Строки | Синхронный | Нет |
| localStorage | ~5 МБ | Строки | Синхронный | Нет |
| sessionStorage | ~5 МБ | Строки | Синхронный | Нет |
| IndexedDB | До 50% доступного диска | JS-объекты, файлы, Blob | Асинхронный | Да |
| WebSQL | ~50 МБ | SQL-данные | Асинхронный | Да |
Хотя кривая обучения IndexedDB может показаться крутой, преимущества, которые она предоставляет, значительно перевешивают затраченные усилия. В следующих разделах мы рассмотрим, как структурирована эта технология и как начать с ней работать. 🔍

Архитектура и ключевые концепции IndexedDB
Чтобы эффективно работать с IndexedDB, необходимо понять её фундаментальную архитектуру. IndexedDB следует объектно-ориентированной парадигме и имеет несколько ключевых компонентов, формирующих её структуру.
Основные строительные блоки архитектуры IndexedDB:
- Database (База данных) — контейнер верхнего уровня для хранения данных
- Object Store (Хранилище объектов) — аналог таблицы в реляционных БД
- Index (Индекс) — структура для оптимизации поиска по определённым полям
- Transaction (Транзакция) — группа операций, выполняемых как единое целое
- Cursor (Курсор) — механизм для перебора записей в хранилище или индексе
- Key (Ключ) — уникальный идентификатор записи в хранилище
В IndexedDB все данные хранятся в формате "ключ-значение", где значением может быть практически любой JavaScript-объект. Это делает её идеальной для хранения JSON-данных, получаемых с сервера.
| Концепция | Описание | Аналог в реляционных БД |
|---|---|---|
| Database | Контейнер для object stores | База данных |
| Object Store | Контейнер для данных | Таблица |
| Key | Уникальный идентификатор | Первичный ключ |
| Index | Ускоритель поиска по атрибуту | Индекс |
| Transaction | Логическая группа операций | Транзакция |
Важно отметить несколько особенностей работы IndexedDB:
- Асинхронность — все операции с базой выполняются асинхронно, что не блокирует пользовательский интерфейс
- Событийная модель — результаты операций обрабатываются через колбэки или промисы
- Транзакционность — гарантирует целостность данных даже при ошибках
- Версионирование — схема БД меняется через механизм версий
Рассмотрим процесс взаимодействия с IndexedDB на концептуальном уровне:
- Открытие соединения с базой данных
const request = indexedDB.open("MyDatabase", 1);
- Создание структуры базы данных (выполняется только при создании или обновлении)
request.onupgradeneeded = function(event) {
const db = event.target.result;
const store = db.createObjectStore("customers", { keyPath: "id" });
store.createIndex("name", "name", { unique: false });
};
- Выполнение транзакций для работы с данными
request.onsuccess = function(event) {
const db = event.target.result;
const transaction = db.transaction("customers", "readwrite");
const store = transaction.objectStore("customers");
store.add({ id: 1, name: "John", email: "john@example.com" });
};
Понимание архитектуры IndexedDB — это первый шаг к эффективному использованию этой технологии. Хотя изначально она может показаться сложной, её структурированный подход обеспечивает высокую производительность и гибкость при работе с данными в браузере. 📊
Основные операции: создание и взаимодействие с базой
Теперь, когда мы понимаем архитектуру IndexedDB, давайте рассмотрим основные операции для создания базы данных и взаимодействия с ней. Все примеры будут представлены в виде практического кода, готового к использованию в ваших проектах.
Начнем с самого первого шага — открытия соединения с базой данных:
// Открываем базу данных (или создаем, если она не существует)
const openRequest = indexedDB.open("TodoApp", 1);
// Обрабатываем события
openRequest.onerror = function(event) {
console.error("Ошибка открытия БД:", event.target.error);
};
openRequest.onsuccess = function(event) {
const db = event.target.result;
console.log("База данных успешно открыта");
// Здесь можно начинать работу с базой данных
};
// Этот обработчик вызывается при создании новой БД
// или при обновлении её версии
openRequest.onupgradeneeded = function(event) {
const db = event.target.result;
console.log("Инициализация базы данных");
// Создаем хранилище объектов
if (!db.objectStoreNames.contains("tasks")) {
const tasksStore = db.createObjectStore("tasks", {
keyPath: "id",
autoIncrement: true
});
// Создаем индексы для быстрого поиска
tasksStore.createIndex("title", "title", { unique: false });
tasksStore.createIndex("completed", "completed", { unique: false });
tasksStore.createIndex("createdAt", "createdAt", { unique: false });
}
};
После успешного открытия базы данных, мы можем выполнять основные CRUD-операции:
1. Добавление данных (Create):
function addTask(db, taskData) {
return new Promise((resolve, reject) => {
// Создаем транзакцию для записи
const transaction = db.transaction(["tasks"], "readwrite");
const store = transaction.objectStore("tasks");
// Добавляем метку времени создания
taskData.createdAt = new Date().toISOString();
// Добавляем задачу в хранилище
const request = store.add(taskData);
request.onsuccess = function() {
resolve(request.result); // Возвращаем ID новой задачи
};
request.onerror = function() {
reject(request.error);
};
// Обрабатываем завершение транзакции
transaction.oncomplete = function() {
console.log("Транзакция добавления успешно завершена");
};
});
}
// Пример использования
const newTask = {
title: "Изучить IndexedDB",
completed: false,
priority: "high"
};
// Вызов функции добавления
addTask(db, newTask)
.then(id => console.log(`Задача добавлена с ID: ${id}`))
.catch(error => console.error("Ошибка добавления:", error));
2. Чтение данных (Read):
function getTaskById(db, taskId) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(["tasks"], "readonly");
const store = transaction.objectStore("tasks");
const request = store.get(taskId);
request.onsuccess = function() {
resolve(request.result);
};
request.onerror = function() {
reject(request.error);
};
});
}
// Получение всех задач
function getAllTasks(db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(["tasks"], "readonly");
const store = transaction.objectStore("tasks");
const request = store.getAll();
request.onsuccess = function() {
resolve(request.result);
};
request.onerror = function() {
reject(request.error);
};
});
}
// Поиск по индексу
function getTasksByStatus(db, completed) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(["tasks"], "readonly");
const store = transaction.objectStore("tasks");
const index = store.index("completed");
const request = index.getAll(completed);
request.onsuccess = function() {
resolve(request.result);
};
request.onerror = function() {
reject(request.error);
};
});
}
3. Обновление данных (Update):
function updateTask(db, taskId, updatedData) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(["tasks"], "readwrite");
const store = transaction.objectStore("tasks");
// Сначала получаем текущую задачу
const getRequest = store.get(taskId);
getRequest.onsuccess = function() {
const task = getRequest.result;
if (!task) {
reject(new Error("Задача не найдена"));
return;
}
// Обновляем поля задачи
Object.assign(task, updatedData);
// Сохраняем обновленную задачу
const updateRequest = store.put(task);
updateRequest.onsuccess = function() {
resolve(true);
};
updateRequest.onerror = function() {
reject(updateRequest.error);
};
};
getRequest.onerror = function() {
reject(getRequest.error);
};
});
}
4. Удаление данных (Delete):
function deleteTask(db, taskId) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(["tasks"], "readwrite");
const store = transaction.objectStore("tasks");
const request = store.delete(taskId);
request.onsuccess = function() {
resolve(true);
};
request.onerror = function() {
reject(request.error);
};
});
}
// Очистка всех задач
function clearAllTasks(db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(["tasks"], "readwrite");
const store = transaction.objectStore("tasks");
const request = store.clear();
request.onsuccess = function() {
resolve(true);
};
request.onerror = function() {
reject(request.error);
};
});
}
Дмитрий Васильев, Senior Frontend Developer
В одном из моих последних проектов мы столкнулись с интересной проблемой. Пользователям нужно было редактировать большие объемы табличных данных, и при каждом изменении мы отправляли запрос на сервер. Это создавало огромную нагрузку как на сервер, так и на сеть клиента.
Решение пришло в виде IndexedDB. Мы переработали приложение так, чтобы все изменения сначала сохранялись локально. Пользователи могли работать с таблицами, содержащими тысячи строк, без задержек, а синхронизация с сервером происходила либо по требованию, либо автоматически в фоновом режиме.
Этот подход сократил количество сетевых запросов на 95% и значительно улучшил пользовательский опыт. Да, реализация заняла больше времени, чем обычный подход с постоянными запросами к серверу, но результат того стоил — пользователи были в восторге от скорости работы, а нагрузка на наши серверы существенно снизилась.
Для обеспечения надежности работы с IndexedDB рекомендуется создать вспомогательный класс или утилиты, которые инкапсулируют сложную логику работы с базой данных. Вот пример простой обертки:
class TodoDB {
constructor(dbName, version) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
async open() {
if (this.db) return this.db;
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains("tasks")) {
const store = db.createObjectStore("tasks", { keyPath: "id", autoIncrement: true });
store.createIndex("title", "title", { unique: false });
store.createIndex("completed", "completed", { unique: false });
}
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve(this.db);
};
request.onerror = (event) => {
reject("Error opening database: " + event.target.error);
};
});
}
async addTask(task) {
const db = await this.open();
return new Promise((resolve, reject) => {
const tx = db.transaction("tasks", "readwrite");
const store = tx.objectStore("tasks");
const request = store.add(task);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Добавьте другие методы CRUD операций
}
// Использование класса
const todoDB = new TodoDB("TodoApp", 1);
todoDB.open()
.then(() => todoDB.addTask({ title: "Новая задача", completed: false }))
.then(id => console.log(`Задача создана с ID: ${id}`))
.catch(error => console.error(error));
Теперь у вас есть базовые инструменты для создания, чтения, обновления и удаления данных в IndexedDB. В следующем разделе мы углубимся в транзакции и курсоры, которые позволяют ещё эффективнее управлять данными. 💪
Транзакции и курсоры для эффективного управления данными
Транзакции и курсоры — это мощные инструменты IndexedDB, которые позволяют разработчикам эффективно управлять данными и выполнять сложные операции. Эти концепции особенно важны для поддержания целостности данных и оптимизации производительности.
Транзакции в IndexedDB
Транзакции — это основа работы с IndexedDB. Любое взаимодействие с данными должно происходить в рамках транзакции, которая обеспечивает атомарность операций. Это означает, что либо все операции в транзакции выполнятся успешно, либо ни одна из них не повлияет на базу данных.
Существует три типа транзакций в IndexedDB:
readonly— только для чтения данных (наиболее быстрый тип)readwrite— для чтения и записи данныхversionchange— для изменения структуры базы данных
Пример создания и использования транзакции:
// Создание транзакции для нескольких хранилищ
const transaction = db.transaction(["tasks", "categories"], "readwrite");
// Получение доступа к хранилищам в рамках одной транзакции
const tasksStore = transaction.objectStore("tasks");
const categoriesStore = transaction.objectStore("categories");
// Обработка событий транзакции
transaction.oncomplete = function() {
console.log("Все операции успешно выполнены");
};
transaction.onerror = function(event) {
console.error("Ошибка транзакции:", event.target.error);
};
transaction.onabort = function() {
console.warn("Транзакция была прервана");
};
// Выполнение операций в рамках транзакции
const taskRequest = tasksStore.add({ title: "Новая задача", categoryId: 1 });
const categoryRequest = categoriesStore.get(1);
categoryRequest.onsuccess = function() {
const category = categoryRequest.result;
if (category) {
console.log(`Задача добавлена в категорию: ${category.name}`);
}
};
Важно помнить о нескольких правилах при работе с транзакциями:
- Транзакции автоматически завершаются, когда нет активных запросов и JavaScript возвращается в цикл событий.
- Транзакции имеют ограниченное время жизни (обычно до нескольких секунд).
- Нельзя создавать новые запросы после завершения транзакции.
- Каждый запрос в рамках транзакции выполняется последовательно (даже если API асинхронный).
Курсоры для эффективной обработки данных
Курсоры предоставляют эффективный способ перебора записей в хранилище или индексе. Они особенно полезны при работе с большими наборами данных, когда вам нужно обработать каждую запись, применяя определённую логику.
Существует два типа курсоров:
IDBCursor— базовый курсор для перебора записейIDBCursorWithValue— расширенный курсор, который дополнительно предоставляет значение текущей записи
Пример использования курсора для обработки всех записей:
function processAllTasks(db, callback) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(["tasks"], "readonly");
const store = transaction.objectStore("tasks");
const results = [];
// Открываем курсор
const request = store.openCursor();
request.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
// Обрабатываем текущую запись
const task = cursor.value;
// Применяем пользовательскую логику обработки
const processedTask = callback(task);
if (processedTask !== null) {
results.push(processedTask);
}
// Переходим к следующей записи
cursor.continue();
} else {
// Курсор достиг конца хранилища
resolve(results);
}
};
request.onerror = function() {
reject(request.error);
};
});
}
// Пример использования
processAllTasks(db, (task) => {
// Фильтруем и трансформируем задачи
if (task.completed) {
return {
id: task.id,
title: task.title,
completedAt: task.completedAt
};
}
return null; // Пропускаем незавершенные задачи
})
.then(completedTasks => {
console.log("Завершенные задачи:", completedTasks);
})
.catch(error => {
console.error("Ошибка при обработке задач:", error);
});
Курсоры также поддерживают направление и диапазоны, что делает их ещё более гибкими:
// Открываем курсор для перебора в обратном порядке
const request = store.openCursor(null, "prev");
// Открываем курсор в диапазоне (от 10 до 20)
const range = IDBKeyRange.bound(10, 20, false, false);
const request = store.openCursor(range);
Типы диапазонов IDBKeyRange:
| Метод | Описание | Пример |
|---|---|---|
bound(lower, upper, lowerOpen, upperOpen) | Диапазон между двумя границами | IDBKeyRange.bound(10, 20, false, false) |
lowerBound(lower, open) | Диапазон от нижней границы и выше | IDBKeyRange.lowerBound(10, true) |
upperBound(upper, open) | Диапазон до верхней границы | IDBKeyRange.upperBound(20, true) |
only(value) | Точное совпадение с значением | IDBKeyRange.only("completed") |
Пример использования курсора с индексом для поиска задач по приоритету и сроку выполнения:
function getHighPriorityTasksDueThisWeek(db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(["tasks"], "readonly");
const store = transaction.objectStore("tasks");
const priorityIndex = store.index("priority");
// Получаем текущую дату и дату через неделю
const today = new Date();
const nextWeek = new Date();
nextWeek.setDate(today.getDate() + 7);
// Преобразуем даты в строки для сравнения
const todayStr = today.toISOString();
const nextWeekStr = nextWeek.toISOString();
// Диапазон для дат
const dateRange = IDBKeyRange.bound(todayStr, nextWeekStr);
const results = [];
// Открываем курсор по индексу приоритета для значения "high"
const request = priorityIndex.openCursor(IDBKeyRange.only("high"));
request.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
const task = cursor.value;
// Фильтруем задачи по сроку выполнения
if (task.dueDate >= todayStr && task.dueDate <= nextWeekStr) {
results.push(task);
}
cursor.continue();
} else {
resolve(results);
}
};
request.onerror = function() {
reject(request.error);
};
});
}
Комбинирование транзакций и курсоров позволяет создавать сложные операции с данными, поддерживая при этом производительность и целостность. Хотя синтаксис может казаться громоздким, это малая цена за мощность и гибкость, которые предоставляет IndexedDB. 🛠️
Практические сценарии использования IndexedDB в веб-проектах
Теоретическое понимание IndexedDB важно, но реальная ценность этой технологии раскрывается через практические примеры. Рассмотрим несколько типовых сценариев использования IndexedDB, которые вы можете адаптировать для своих проектов.
1. Офлайн-первые приложения
Создание приложений, которые полноценно работают без подключения к интернету — один из главных сценариев использования IndexedDB. Такой подход особенно актуален для мобильных пользователей и регионов с нестабильным интернетом.
// Класс для синхронизации офлайн-данных
class SyncManager {
constructor(db, apiEndpoint) {
this.db = db;
this.apiEndpoint = apiEndpoint;
this.syncQueue = [];
}
// Добавление операции в очередь синхронизации
async addToSyncQueue(operation, data) {
const tx = this.db.transaction(["syncQueue"], "readwrite");
const store = tx.objectStore("syncQueue");
await store.add({
operation, // "create", "update", "delete"
data,
timestamp: Date.now(),
synced: false
});
this.processSyncQueue(); // Пытаемся синхронизировать сразу
}
// Обработка очереди синхронизации
async processSyncQueue() {
// Проверяем наличие соединения
if (!navigator.onLine) {
console.log("Нет соединения. Синхронизация отложена.");
return;
}
const tx = this.db.transaction(["syncQueue"], "readwrite");
const store = tx.objectStore("syncQueue");
const pendingItems = await store.index("synced").getAll(false);
for (const item of pendingItems) {
try {
let endpoint, method;
switch (item.operation) {
case "create":
endpoint = `${this.apiEndpoint}`;
method = "POST";
break;
case "update":
endpoint = `${this.apiEndpoint}/${item.data.id}`;
method = "PUT";
break;
case "delete":
endpoint = `${this.apiEndpoint}/${item.data.id}`;
method = "DELETE";
break;
}
const response = await fetch(endpoint, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(item.data)
});
if (response.ok) {
// Помечаем как синхронизированное
await store.put({ ...item, synced: true });
console.log(`Синхронизирован элемент: ${item.data.id}`);
}
} catch (error) {
console.error("Ошибка синхронизации:", error);
}
}
}
// Подписка на события подключения к сети
setupNetworkListeners() {
window.addEventListener("online", () => {
console.log("Соединение восстановлено, запуск синхронизации");
this.processSyncQueue();
});
}
}
2. Кэширование API-ответов
Кэширование данных API может значительно ускорить загрузку приложения и снизить нагрузку на сервер.
// Класс для кэширования API-ответов
class ApiCache {
constructor(db) {
this.db = db;
}
// Получение данных с кэшированием
async fetchWithCache(url, options = {}) {
const cacheKey = url; // Можно добавить хэширование с учетом параметров
// Проверяем наличие в кэше
const cachedData = await this.getFromCache(cacheKey);
// Если данные есть в кэше и не устарели, используем их
if (cachedData && !this.isExpired(cachedData.timestamp)) {
console.log("Данные получены из кэша:", url);
return cachedData.data;
}
// Иначе делаем запрос к API
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
// Сохраняем в кэш
await this.saveToCache(cacheKey, data);
return data;
} catch (error) {
// В случае ошибки, если есть устаревшие данные, возвращаем их
if (cachedData) {
console.warn("Ошибка API, используются устаревшие данные:", error);
return cachedData.data;
}
throw error;
}
}
// Получение данных из кэша
async getFromCache(key) {
const tx = this.db.transaction(["apiCache"], "readonly");
const store = tx.objectStore("apiCache");
return await store.get(key);
}
// Сохранение данных в кэш
async saveToCache(key, data) {
const tx = this.db.transaction(["apiCache"], "readwrite");
const store = tx.objectStore("apiCache");
await store.put({
key,
data,
timestamp: Date.now()
});
}
// Проверка актуальности данных (например, 1 час)
isExpired(timestamp, maxAge = 3600000) {
return Date.now() – timestamp > maxAge;
}
// Очистка устаревших данных
async clearExpiredCache(maxAge = 3600000) {
const tx = this.db.transaction(["apiCache"], "readwrite");
const store = tx.objectStore("apiCache");
const cursor = await store.openCursor();
while (cursor) {
if (this.isExpired(cursor.value.timestamp, maxAge)) {
await cursor.delete();
}
cursor.continue();
}
}
}
3. Хранение пользовательского контента
IndexedDB отлично подходит для хранения контента, создаваемого пользователями, такого как текстовые документы, изображения или другие файлы.
// Класс для работы с пользовательскими документами
class DocumentManager {
constructor(db) {
this.db = db;
}
// Сохранение документа
async saveDocument(document) {
const tx = this.db.transaction(["documents"], "readwrite");
const store = tx.objectStore("documents");
// Если это новый документ, не указываем id
if (!document.id) {
document.createdAt = Date.now();
}
document.updatedAt = Date.now();
// Возвращаем id сохраненного документа
return await store.put(document);
}
// Сохранение документа с изображениями
async saveDocumentWithImages(document, images) {
const tx = this.db.transaction(["documents", "images"], "readwrite");
const docStore = tx.objectStore("documents");
const imageStore = tx.objectStore("images");
// Сохраняем документ
document.updatedAt = Date.now();
const docId = await docStore.put(document);
// Сохраняем изображения
for (const image of images) {
await imageStore.put({
documentId: docId,
name: image.name,
blob: image.blob,
type: image.type,
uploadedAt: Date.now()
});
}
return docId;
}
// Получение документа со всеми связанными изображениями
async getDocumentWithImages(docId) {
const tx = this.db.transaction(["documents", "images"], "readonly");
const docStore = tx.objectStore("documents");
const imageStore = tx.objectStore("images");
const imageIndex = imageStore.index("documentId");
// Получаем документ
const document = await docStore.get(docId);
if (!document) {
return null;
}
// Получаем все связанные изображения
document.images = await imageIndex.getAll(docId);
return document;
}
// Экспорт документа в файл
async exportDocument(docId) {
const document = await this.getDocumentWithImages(docId);
if (!document) {
throw new Error("Документ не найден");
}
// Создаем объект для экспорта
const exportData = {
document: {
title: document.title,
content: document.content,
createdAt: document.createdAt,
updatedAt: document.updatedAt
},
images: document.images.map(img => ({
name: img.name,
type: img.type,
dataUrl: URL.createObjectURL(img.blob)
}))
};
// Преобразуем в JSON
const jsonData = JSON.stringify(exportData);
// Создаем и скачиваем файл
const blob = new Blob([jsonData], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `document-${docId}.json`;
a.click();
// Очищаем URL объекта
setTimeout(() => URL.revokeObjectURL(url), 100);
}
}
4. Системы управления задачами и заметками
Персональные органайзеры и менеджеры задач идеально подходят для реализации с использованием IndexedDB.
// Полноценный класс для управления заметками
class NotesManager {
constructor(dbName = "NotesApp", version = 1) {
this.dbName = dbName;
this.version = version;
this.db = null;
// Структура базы данных
this.dbStructure = (db) => {
// Хранилище заметок
const noteStore = db.createObjectStore("notes", { keyPath: "id", autoIncrement: true });
noteStore.createIndex("title", "title", { unique: false });
noteStore.createIndex("createdAt", "createdAt", { unique: false });
noteStore.createIndex("updatedAt", "updatedAt", { unique: false });
// Хранилище тегов
const tagStore = db.createObjectStore("tags", { keyPath: "id", autoIncrement: true });
tagStore.createIndex("name", "name", { unique: true });
// Связь заметок и тегов (многие-ко-многим)
const noteTagStore = db.createObjectStore("noteTags", { keyPath: ["noteId", "tagId"] });
noteTagStore.createIndex("noteId", "noteId", { unique: false });
noteTagStore.createIndex("tagId", "tagId", { unique: false });
};
}
// Открытие/инициализация базы данных
async open() {
if (this.db) return this.db;
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onupgradeneeded = (event) => {
const db = event.target.result;
this.dbStructure(db);
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve(this.db);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}
// Создание новой заметки
async createNote(title, content, tags = []) {
await this.open();
// Создаем транзакцию для нескольких хранилищ
const tx = this.db.transaction(["notes", "tags", "noteTags"], "readwrite");
const noteStore = tx.objectStore("notes");
const tagStore = tx.objectStore("tags");
const noteTagStore = tx.objectStore("noteTags");
// Текущее время для меток создания и обновления
const now = Date.now();
// Добавляем заметку
const noteId = await new Promise((resolve, reject) => {
const request = noteStore.add({
title,
content,
createdAt: now,
updatedAt: now
});
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
// Добавляем теги и связи
for (const tagName of tags) {
// Находим или создаем тег
let tagId = await this.findOrCreateTag(tagStore, tagName);
// Создаем связь между заметкой и тегом
await new Promise((resolve, reject) => {
const request = noteTagStore.add({
noteId,
tagId
});
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
return noteId;
}
// Поиск или создание тега
async findOrCreateTag(tagStore, tagName) {
const nameIndex = tagStore.index("name");
const existingTag = await new Promise((resolve, reject) => {
const request = nameIndex.get(tagName);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
if (existingTag) {
return existingTag.id;
}
// Если тег не найден, создаем новый
return new Promise((resolve, reject) => {
const request = tagStore.add({
name: tagName,
createdAt: Date.now()
});
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Получение заметки со всеми связанными тегами
async getNoteWithTags(noteId) {
await this.open();
const tx = this.db.transaction(["notes", "tags", "noteTags"], "readonly");
const noteStore = tx.objectStore("notes");
const tagStore = tx.objectStore("tags");
const noteTagStore = tx.objectStore("noteTags");
// Получаем заметку
const note = await new Promise((resolve, reject) => {
const request = noteStore.get(noteId);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
if (!note) {
return null;
}
// Находим все связи с тегами
const noteTagIndex = noteTagStore.index("noteId");
const noteTags = await new Promise((resolve, reject) => {
const request = noteTagIndex.getAll(noteId);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
// Получаем информацию о каждом теге
note.tags = [];
for (const noteTag of noteTags) {
const tag = await new Promise((resolve, reject) => {
const request = tagStore.get(noteTag.tagId);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
if (tag) {
note.tags.push(tag);
}
}
return note;
}
}
В реальных проектах эти сценарии часто комбинируются для создания комплексных решений. Например, система управления заметками может включать и офлайн-режим, и кэширование API, и работу с пользовательским контентом.
Несмотря на некоторую сложность API, IndexedDB предоставляет мощные возможности для создания современных веб-приложений, которые могут работать в автономном режиме и обеспечивать отличный пользовательский опыт даже при нестабильном соединении. 🚀
IndexedDB — это не п
Вероника Лисицына
фронтенд-инженер