Интеграция localForage с Vue-экосистемой строится вокруг синхронизации асинхронного ключ-значение хранилища с реактивной моделью данных Vue. Основная сложность заключается в том, что localForage работает через Promise API, тогда как реактивность Vue предполагает мгновенное обновление состояния при изменении значения.
Ключевая архитектурная идея заключается в разделении двух слоёв:
Их связывание выполняется через инициализацию состояния, наблюдение за изменениями и асинхронную синхронизацию.
Асинхронная природа localForage требует явной загрузки начального состояния перед тем, как приложение начнёт использовать данные.
Типовая структура модуля хранения:
import localforage from "localforage";
import { ref } from "vue";
const state = ref(null);
const STORAGE_KEY = "app_state";
async function initStore() {
const stored = await localforage.getItem(STORAGE_KEY);
state.value = stored ?? { user: null, settings: {} };
}
async function persist() {
await localforage.setItem(STORAGE_KEY, state.value);
}
Инициализация выполняется один раз на уровне приложения или модуля состояния.
Прямое использование localForage внутри компонентов приводит к разрозненной логике и усложняет контроль состояния. Более устойчивый подход — создание реактивного хранилища-адаптера.
import { reactive, watch } from "vue";
import localforage from "localforage";
export function createPersistentStore(key, initialValue) {
const state = reactive(initialValue);
let hydrated = false;
async function hydrate() {
const stored = await localforage.getItem(key);
if (stored) {
Object.assign(state, stored);
}
hydrated = true;
}
watch(
state,
async () => {
if (!hydrated) return;
await localforage.setItem(key, JSON.parse(JSON.stringify(state)));
},
{ deep: true }
);
return {
state,
hydrate
};
}
Особенность конструкции заключается в глубоком наблюдении
(deep: true) и отсечении первого цикла синхронизации,
возникающего при гидратации данных.
Composition API позволяет выделить слой хранения в самостоятельный composable, который становится единицей переиспользования.
import { ref } from "vue";
import localforage from "localforage";
export function useLocalForage(key, defaultValue) {
const data = ref(defaultValue);
const ready = ref(false);
async function load() {
const stored = await localforage.getItem(key);
data.value = stored ?? defaultValue;
ready.value = true;
}
async function save(value) {
data.value = value;
if (ready.value) {
await localforage.setItem(key, value);
}
}
return {
data,
ready,
load,
save
};
}
Данная модель разделяет чтение и запись, предотвращая неконтролируемые побочные эффекты при инициализации.
В приложениях среднего и крупного масштаба localForage часто используется совместно с глобальным стором, например Pinia. В этом случае localForage становится слоем персистенции.
import { defineStore } from "pinia";
import localforage from "localforage";
export const useUserStore = defineStore("user", {
state: () => ({
profile: null,
token: null,
loaded: false
}),
actions: {
async hydrate() {
const saved = await localforage.getItem("user_store");
if (saved) {
this.profile = saved.profile;
this.token = saved.token;
}
this.loaded = true;
},
async persist() {
await localforage.setItem("user_store", {
profile: this.profile,
token: this.token
});
},
setProfile(profile) {
this.profile = profile;
this.persist();
}
}
});
Такая схема формирует явный контракт: store управляет состоянием, localForage — долговременным хранением.
Асинхронность localForage создаёт риск гонок при частых изменениях состояния. Особенно это заметно при глубоких реактивных объектах и массовых обновлениях.
Решение заключается в сериализации операций записи.
let queue = Promise.resolve();
function enqueueWrite(task) {
queue = queue.then(() => task());
return queue;
}
Использование:
watch(
state,
() => {
enqueueWrite(() =>
localforage.setItem("key", JSON.parse(JSON.stringify(state)))
);
},
{ deep: true }
);
Такой подход гарантирует последовательность сохранений и предотвращает перезапись устаревшими данными.
Частые изменения реактивного состояния приводят к избыточным операциям записи в IndexedDB или WebSQL. Для снижения нагрузки используется debounce-механизм.
function debounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
Применение:
const saveDebounced = debounce(() => {
localforage.setItem("state", state);
}, 300);
В реактивной системе это снижает количество операций записи при массовых обновлениях интерфейса.
В SPA-архитектуре важным этапом становится восстановление состояния до монтирования компонентов. localForage требует асинхронного доступа, поэтому процесс гидратации отделяется от создания приложения.
import { createApp } from "vue";
import App from "./App.vue";
import localforage from "localforage";
async function bootstrap() {
const saved = await localforage.getItem("app");
const app = createApp(App);
app.provide("initialState", saved ?? {});
app.mount("#app");
}
bootstrap();
Таким образом состояние становится доступным до рендеринга интерфейса.
При использовании computed свойств важно учитывать, что persisted state может загружаться с задержкой. Это влияет на поведение вычисляемых значений.
import { computed } from "vue";
const fullName = computed(() => {
if (!store.ready) return "";
return `${store.firstName} ${store.lastName}`;
});
Реактивная система должна учитывать фазу загрузки, чтобы избежать некорректных вычислений.
Для крупных приложений вводится слой абстракции над localForage, скрывающий детали API.
class StorageAdapter {
constructor(prefix = "") {
this.prefix = prefix;
}
key(k) {
return `${this.prefix}:${k}`;
}
async get(k) {
return await localforage.getItem(this.key(k));
}
async set(k, v) {
return await localforage.setItem(this.key(k), v);
}
async remove(k) {
return await localforage.removeItem(this.key(k));
}
}
Такой слой упрощает миграции и тестирование, позволяя заменить backend хранения без изменения логики компонентов.
localForage не предоставляет встроенной синхронизации между вкладками
браузера. Для Vue-приложений это решается через событие
storage или через BroadcastChannel.
const channel = new BroadcastChannel("state_sync");
channel.onmess age = (event) => {
Object.assign(state, event.data);
};
watch(
state,
() => {
channel.postMessage(state);
},
{ deep: true }
);
Так достигается согласованность реактивного состояния между несколькими окнами приложения.
При изменении структуры данных требуется миграция сохранённого состояния. localForage хранит данные без схемы, поэтому версия вводится вручную.
const CURRENT_VERSION = 2;
async function migrate() {
const data = await localforage.getItem("app");
if (!data) return;
if (!data._version || data._version < CURRENT_VERSION) {
data.newField = true;
data._version = CURRENT_VERSION;
await localforage.setItem("app", data);
}
}
Версионирование позволяет безопасно эволюционировать структуру persisted state без потери совместимости.
Не все реактивные данные требуют сохранения. В Vue-приложениях часто выделяются два слоя:
localForage применяется только ко второму слою, что снижает нагрузку и упрощает модель данных.
При серверном рендеринге localForage недоступен, так как работает через браузерные API. Поэтому вводится проверка окружения:
const isBrowser = typeof window !== "undefined";
Инициализация хранения выполняется только на клиенте:
if (isBrowser) {
await localforage.getItem("app");
}
Серверная часть использует заглушки или предзагруженные данные, переданные через hydration payload.