Интеграция с Vue: реактивность и хранилище

Интеграция localForage с Vue-экосистемой строится вокруг синхронизации асинхронного ключ-значение хранилища с реактивной моделью данных Vue. Основная сложность заключается в том, что localForage работает через Promise API, тогда как реактивность Vue предполагает мгновенное обновление состояния при изменении значения.

Ключевая архитектурная идея заключается в разделении двух слоёв:

  • слой персистентного хранилища (localForage)
  • слой реактивного состояния (Vue reactive / ref / computed)

Их связывание выполняется через инициализацию состояния, наблюдение за изменениями и асинхронную синхронизацию.


Базовая модель синхронизации состояния

Асинхронная природа 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

Прямое использование 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

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 становится слоем персистенции.

Пример интеграции с Pinia

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();

Таким образом состояние становится доступным до рендеринга интерфейса.


Реактивные вычисления поверх persisted state

При использовании 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-приложениях часто выделяются два слоя:

  • ephemeral state (UI, локальные переключатели, модальные окна)
  • persisted state (профиль пользователя, настройки, кэш данных)

localForage применяется только ко второму слою, что снижает нагрузку и упрощает модель данных.


Интеграция с SSR-архитектурой

При серверном рендеринге localForage недоступен, так как работает через браузерные API. Поэтому вводится проверка окружения:

const isBrowser = typeof window !== "undefined";

Инициализация хранения выполняется только на клиенте:

if (isBrowser) {
  await localforage.getItem("app");
}

Серверная часть использует заглушки или предзагруженные данные, переданные через hydration payload.