Idb-keyval представляет собой лёгкую обёртку над
IndexedDB, позволяя работать с хранилищем ключ–значение в браузере с
минимальной сложностью. Основные операции включают запись, чтение,
удаление и очистку данных, что делает библиотеку удобной для кэширования
состояния приложения или хранения пользовательских настроек.
import { set, get, del, clear } from 'idb-keyval';
// Запись значения
await set('username', 'Alice');
// Чтение значения
const username = await get('username');
// Удаление значения
await del('username');
// Очистка всего хранилища
await clear();
Для поддержки миграций полезно создавать отдельные экземпляры хранилища, чтобы изолировать данные разных версий приложения.
import { Store } from 'idb-keyval';
const storeV1 = new Store('app-db', 'store-v1');
const storeV2 = new Store('app-db', 'store-v2');
Использование отдельных объектов Store позволяет
контролировать, к каким коллекциям данных обращается каждая версия кода,
и облегчает миграцию старых данных в новый формат.
Миграции в IndexedDB отличаются от обычных SQL-баз. В Idb-keyval нет встроенных средств версионирования, поэтому миграции реализуются вручную через проверку наличия старых данных и их преобразование.
const oldData = await get('userSettings', storeV1);
if (!oldData) return; // Данных нет, миграция не нужна
const newData = {
theme: oldData.darkMode ? 'dark' : 'light',
language: oldData.lang || 'en',
};
await set('settings', newData, storeV2);
await del('userSettings', storeV1);
Таким образом, данные сохраняются в актуальном формате, а старые записи удаляются, предотвращая конфликт версий.
Все операции Idb-keyval возвращают промисы, что позволяет безопасно выполнять последовательные шаги миграции, избегая состояния гонки.
async function migrateV1toV2() {
const oldData = await get('userSettings', storeV1);
if (!oldData) return;
const newData = transform(oldData);
await set('settings', newData, storeV2);
await del('userSettings', storeV1);
}
Использование async/await делает код линейным и
читаемым, упрощая обработку ошибок:
try {
await migrateV1toV2();
} catch (error) {
console.error('Ошибка миграции данных', error);
}
Иногда необходимо поддерживать несколько последовательных миграций при обновлении приложения с версий v1 → v2 → v3. Рекомендуется хранить текущую версию данных в хранилище и выполнять миграции последовательно.
const currentVersion = await get('dbVersion', storeV2) || 1;
if (currentVersion === 1) {
await migrateV1toV2();
await set('dbVersion', 2, storeV2);
}
if (currentVersion === 2) {
await migrateV2toV3();
await set('dbVersion', 3, storeV2);
}
Store для тестов, чтобы не влиять на
реальные данные.Jest, Mocha) для
проверки промисов и обработки ошибок.test('Миграция V1 → V2 корректно преобразует данные', async () => {
await set('userSettings', { darkMode: true, lang: 'ru' }, storeV1);
await migrateV1toV2();
const newData = await get('settings', storeV2);
expect(newData).toEqual({ theme: 'dark', language: 'ru' });
const oldData = await get('userSettings', storeV1);
expect(oldData).toBeUndefined();
});
Для сложных приложений важно предусматривать откат изменений. Можно сохранять резервные копии старых данных перед миграцией:
const backup = await get('userSettings', storeV1);
await set('userSettings_backup', backup, storeV1);
try {
await migrateV1toV2();
} catch (error) {
console.error('Ошибка миграции, откат данных');
await set('userSettings', backup, storeV1);
}
Такой подход гарантирует сохранность данных, если процесс миграции прервётся или завершится ошибкой.
При миграции крупных объектов стоит разбивать процесс на части, чтобы
не блокировать главный поток браузера. Использование
for...of с await позволяет постепенно
записывать и удалять записи:
for (const [key, value] of Object.entries(largeData)) {
const transformed = transformValue(value);
await set(key, transformed, storeV2);
await del(key, storeV1);
}
Это предотвращает перегрузку памяти и снижает вероятность ошибок при миграции огромных массивов данных.