Хранилище localForage опирается на IndexedDB, WebSQL или localStorage в зависимости от окружения, но во всех случаях остаётся одна ключевая проблема: данные живут дольше, чем код, который их создал. Любое изменение структуры данных без механизма версионирования приводит к несовместимости старых записей с новой логикой приложения.
Версия схемы данных — это числовой или строковый идентификатор состояния структуры данных, используемой приложением в конкретный момент времени. Она отражает:
В контексте localForage версия схемы не навязывается библиотекой, а реализуется на уровне прикладной логики.
const SCHEMA_VERSION = 3;
Ключевая особенность — версия должна храниться отдельно от данных, чтобы её можно было прочитать до загрузки бизнес-логики.
Наиболее устойчивый подход — хранение версии в отдельном ключе внутри localForage.
import localforage from "localforage";
const VERSION_KEY = "__schema_version__";
async function getSchemaVersion() {
const version = await localforage.getItem(VERSION_KEY);
return version ?? 0;
}
async function setSchemaVersion(version) {
await localforage.setItem(VERSION_KEY, version);
}
Такой подход позволяет:
Альтернативный вариант — хранение версии внутри каждого объекта:
{
id: "user_1",
name: "Alice",
schemaVersion: 2
}
Однако такой подход усложняет миграции и увеличивает риск фрагментации данных.
Логика инициализации обычно строится вокруг проверки текущей версии и последовательного применения миграций.
const CURRENT_VERSION = 3;
async function initSchema() {
const storedVersion = await getSchemaVersion();
if (storedVersion === CURRENT_VERSION) {
return;
}
await migrateSchema(storedVersion, CURRENT_VERSION);
await setSchemaVersion(CURRENT_VERSION);
}
Ключевое требование — миграции должны быть идемпотентными и безопасными при повторном запуске.
Миграции представляют собой цепочку преобразований данных от одной версии к другой.
const migrations = {
1: async () => {
const legacy = await localforage.getItem("userData");
if (legacy) {
await localforage.setItem("users", [legacy]);
await localforage.removeItem("userData");
}
},
2: async () => {
const users = await localforage.getItem("users") || [];
const updated = users.map(u => ({
...u,
createdAt: Date.now()
}));
await localforage.setItem("users", updated);
}
};
Общий механизм применения миграций:
async function migrateSchema(fromVersion, toVersion) {
for (let v = fromVersion + 1; v <= toVersion; v++) {
const migration = migrations[v];
if (migration) {
await migration();
}
}
}
Такой подход обеспечивает линейное и предсказуемое преобразование структуры данных.
Существуют различные стратегии управления версией схемы в localForage.
Самая распространённая модель:
Недостаток — рост количества миграций при долгоживущих проектах.
Версия разделяется на компоненты:
{
major: 2,
minor: 5,
patch: 1
}
Используется редко, поскольку миграции в хранилище обычно не требуют патч-уровня детализации.
Данные разделяются по пространствам ключей:
const NAMESPACE = "app_v3_";
Пример:
localforage.setItem(`${NAMESPACE}users`, data);
Преимущества:
Недостатки:
Помимо версии, часто требуется хранить дополнительную информацию:
Пример структуры метаданных:
const schemaMeta = {
version: 3,
migratedAt: 1710000000000,
previousVersion: 2,
checksum: "a91f3c"
};
Хранение:
await localforage.setItem("__schema_meta__", schemaMeta);
Миграции должны учитывать возможные сбои на любом этапе выполнения.
Проблема: процесс миграции прерван, данные находятся в промежуточном состоянии.
Решение — использование временных ключей:
await localforage.setItem("users_tmp", transformed);
await localforage.removeItem("users");
await localforage.setItem("users", transformed);
await localforage.removeItem("users_tmp");
Более надёжный подход:
await localforage.setItem("users_v2", newData);
await localforage.setItem("users", newData);
Позволяет откатиться к предыдущей версии при ошибке.
Дополнительно используется контрольный механизм:
function computeChecksum(data) {
return JSON.stringify(data).length.toString(16);
}
Перед завершением миграции проверяется соответствие структуры ожидаемой схеме.
Вместо миграции всех данных при запуске применяется подход «on read»:
async function getUser(id) {
const user = await localforage.getItem(`user_${id}`);
if (user.schemaVersion < CURRENT_VERSION) {
return migrateUser(user);
}
return user;
}
Преимущества:
Недостатки:
В некоторых случаях несколько версий схемы могут существовать одновременно:
Пример:
await localforage.setItem("users_v2", usersV2);
await localforage.setItem("users_v3", usersV3);
Выбор актуальной версии осуществляется на уровне бизнес-логики.
Проверка корректности миграций требует симуляции различных состояний хранилища.
async function testMigration() {
await localforage.clear();
await localforage.setItem("__schema_version__", 1);
await localforage.setItem("users", legacyData);
await migrateSchema(1, 3);
const result = await localforage.getItem("users");
console.assert(result.length > 0);
}
Особое внимание уделяется:
Любая миграция должна учитывать сценарии частичного отказа:
async function safeMigration(step) {
try {
await step();
} catch (e) {
await rollbackMigration();
throw e;
}
}
Rollback-стратегия может включать:
При росте приложения структура localForage-хранилища часто усложняется:
Пример registry:
const schemaRegistry = {
users: 3,
settings: 2,
cache: 5
};
Такой подход позволяет независимо эволюционировать разные части приложения без глобальных миграций.