В экосистеме Vue 3 подход Composition API позволяет
инкапсулировать логику работы с данными в переиспользуемые функции —
composables. При работе с idb-keyval это особенно
удобно, поскольку IndexedDB асинхронна, а состояние приложения должно
быть реактивным.
Основная задача composable — связать:
idb-keyval)ref, reactive)Результатом становится единый слой доступа к данным, скрывающий детали работы IndexedDB.
Простейшая абстракция — composable, синхронизирующий одно значение с IndexedDB:
import { ref, watch } from 'vue'
import { get, set } from 'idb-keyval'
export function useIDBKey(key, defaultValue = null) {
const state = ref(defaultValue)
const isLoading = ref(true)
const error = ref(null)
// Загрузка значения
get(key)
.then(value => {
if (value !== undefined) {
state.value = value
}
})
.catch(err => {
error.value = err
})
.finally(() => {
isLoading.value = false
})
// Автосохранение при изменении
watch(state, (newValue) => {
set(key, newValue).catch(err => {
error.value = err
})
}, { deep: true })
return {
state,
isLoading,
error
}
}
state автоматически
обновляет UIПри хранении сложных структур (объекты, массивы) важно учитывать
deep-наблюдение:
watch(state, (newValue) => {
set(key, newValue)
}, { deep: true })
Без deep: true изменения вложенных свойств не будут
отслеживаться.
Для более гибкого управления состоянием полезно явно разделить операции:
import { ref } from 'vue'
import { get, set, del } from 'idb-keyval'
export function useIDB(key) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const load = async () => {
loading.value = true
try {
data.value = await get(key)
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
const save = async (value) => {
try {
await set(key, value)
data.value = value
} catch (e) {
error.value = e
}
}
const remove = async () => {
try {
await del(key)
data.value = null
} catch (e) {
error.value = e
}
}
return {
data,
loading,
error,
load,
save,
remove
}
}
Часто требуется загрузка данных при монтировании:
import { onMounted } from 'vue'
onMounted(() => {
load()
})
Или в composable:
import { onMounted } from 'vue'
export function useAutoLoadIDB(key) {
const { data, load } = useIDB(key)
onMounted(load)
return { data }
}
Повторные вызовы get могут быть избыточными. Решение —
локальный кэш:
const cache = new Map()
export async function getCached(key) {
if (cache.has(key)) {
return cache.get(key)
}
const value = await get(key)
cache.set(key, value)
return value
}
Интеграция в composable:
data.value = await getCached(key)
Для хранения списков используется единый ключ:
export function useIDBList(key) {
const list = ref([])
const load = async () => {
list.value = (await get(key)) || []
}
const add = async (item) => {
list.value.push(item)
await set(key, list.value)
}
const remove = async (index) => {
list.value.splice(index, 1)
await set(key, list.value)
}
return { list, load, add, remove }
}
push, splice)Частые изменения состояния могут перегружать IndexedDB. Решение — дебаунс:
import { watch } from 'vue'
import { set } from 'idb-keyval'
import debounce from 'lodash.debounce'
const saveDebounced = debounce((key, value) => {
set(key, value)
}, 300)
watch(state, (val) => {
saveDebounced(key, val)
}, { deep: true })
IndexedDB не уведомляет напрямую о изменениях. Для синхронизации
используется storage или BroadcastChannel:
const channel = new BroadcastChannel('idb-sync')
channel.onmess age = (event) => {
if (event.data.key === key) {
state.value = event.data.value
}
}
watch(state, (val) => {
set(key, val)
channel.postMessage({ key, value: val })
})
Composables удобно типизировать:
import { Ref } from 'vue'
export function useIDBKey<T>(key: string, defaultValue: T): {
state: Ref<T>
isLoading: Ref<boolean>
error: Ref<Error | null>
} {
// реализация
}
Преимущества:
IndexedDB может быть недоступна (например, в приватном режиме). Стратегия:
const isSupported = 'indexedDB' in window
if (!isSupported) {
// fallback на localStorage
}
Пример fallback:
const storage = isSupported ? idb : localStorage
При использовании composables важно определить:
Composables могут инкапсулировать оба сценария.
Рекомендуется создавать composables по смыслу:
useUserSettings()
useCart()
usePreferences()
Внутри каждого — использование idb-keyval, но интерфейс
остаётся бизнес-ориентированным.
Тесты можно писать с моками idb-keyval:
jest.mock('idb-keyval', () => ({
get: jest.fn(),
set: jest.fn(),
}))
Проверяется:
idb-keyval поддерживает пользовательские базы:
import { createStore, get, set } from 'idb-keyval'
const customStore = createStore('app-db', 'settings')
set('theme', 'dark', customStore)
get('theme', customStore)
В composable:
export function useCustomStore(key) {
const store = createStore('app-db', 'settings')
// использование store
}
Composables можно комбинировать с:
Пример с Pinia:
import { defineStore } from 'pinia'
import { get, set } from 'idb-keyval'
export const useSettingsStore = defineStore('settings', {
state: () => ({
theme: 'light'
}),
actions: {
async load() {
this.theme = await get('theme') || 'light'
},
async save() {
await set('theme', this.theme)
}
}
})
Ключевые рекомендации:
setIndexedDB:
Для чувствительных данных:
Хороший composable включает:
Такой подход превращает idb-keyval из низкоуровневого
API в удобный и безопасный слой хранения, органично встроенный в
реактивную модель Vue.