Полное руководство по IndexedDB: от основ API до примеров кода
Перейти

Полное руководство по IndexedDB: от основ API до примеров кода

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

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

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

IndexedDB — это мощное хранилище данных

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

IndexedDB — это мощное хранилище данных, которое давно перестало быть экзотикой для продвинутых фронтендеров. Забудьте об ограничениях 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:

  1. Асинхронность — все операции с базой выполняются асинхронно, что не блокирует пользовательский интерфейс
  2. Событийная модель — результаты операций обрабатываются через колбэки или промисы
  3. Транзакционность — гарантирует целостность данных даже при ошибках
  4. Версионирование — схема БД меняется через механизм версий

Рассмотрим процесс взаимодействия с IndexedDB на концептуальном уровне:

  1. Открытие соединения с базой данных
JS
Скопировать код
const request = indexedDB.open("MyDatabase", 1);

  1. Создание структуры базы данных (выполняется только при создании или обновлении)
JS
Скопировать код
request.onupgradeneeded = function(event) {
const db = event.target.result;
const store = db.createObjectStore("customers", { keyPath: "id" });
store.createIndex("name", "name", { unique: false });
};

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

Начнем с самого первого шага — открытия соединения с базой данных:

JS
Скопировать код
// Открываем базу данных (или создаем, если она не существует)
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):

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

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

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

JS
Скопировать код
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 рекомендуется создать вспомогательный класс или утилиты, которые инкапсулируют сложную логику работы с базой данных. Вот пример простой обертки:

JS
Скопировать код
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 — для изменения структуры базы данных

Пример создания и использования транзакции:

JS
Скопировать код
// Создание транзакции для нескольких хранилищ
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}`);
}
};

Важно помнить о нескольких правилах при работе с транзакциями:

  1. Транзакции автоматически завершаются, когда нет активных запросов и JavaScript возвращается в цикл событий.
  2. Транзакции имеют ограниченное время жизни (обычно до нескольких секунд).
  3. Нельзя создавать новые запросы после завершения транзакции.
  4. Каждый запрос в рамках транзакции выполняется последовательно (даже если API асинхронный).

Курсоры для эффективной обработки данных

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

Существует два типа курсоров:

  • IDBCursor — базовый курсор для перебора записей
  • IDBCursorWithValue — расширенный курсор, который дополнительно предоставляет значение текущей записи

Пример использования курсора для обработки всех записей:

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

Курсоры также поддерживают направление и диапазоны, что делает их ещё более гибкими:

JS
Скопировать код
// Открываем курсор для перебора в обратном порядке
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")

Пример использования курсора с индексом для поиска задач по приоритету и сроку выполнения:

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

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

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

JS
Скопировать код
// Класс для работы с пользовательскими документами
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.

JS
Скопировать код
// Полноценный класс для управления заметками
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 — это не п

Проверь как ты усвоил материалы статьи
Пройди тест и узнай насколько ты лучше других читателей
Что такое IndexedDB?
1 / 5

Вероника Лисицына

фронтенд-инженер

Свежие материалы

Загрузка...