Работа с IndexedDB через Dexie.js всегда опирается на асинхронные
операции, возвращающие Promise. В контексте Vue 3 это
создает разрыв между императивным доступом к данным и реактивной моделью
представления. Основная задача интеграции — превратить результаты
запросов Dexie в реактивные источники состояния, автоматически
обновляющиеся при изменениях базы.
Ключевое свойство Dexie — возможность построения подписок на
изменения через механизмы наблюдения (включая liveQuery).
Vue, в свою очередь, оперирует ref, reactive,
computed и системой эффектов, что позволяет связать
результат запроса с UI без ручного обновления состояния.
Самый прямолинейный способ интеграции — загрузка данных при
инициализации компонента и помещение результата в ref.
import { ref, onMounted } fr om 'vue'
import { db } from './db'
export function useTodos() {
const todos = ref([])
const loading = ref(true)
const load = async () => {
loading.value = true
todos.value = await db.todos.toArray()
loading.value = false
}
onMounted(load)
return { todos, loading, reload: load }
}
Такой подход прост, но не обеспечивает автоматического обновления при изменении базы данных.
Dexie предоставляет liveQuery, который преобразует
запрос в поток изменений. При каждом изменении данных запрос
пересчитывается, а подписчики получают новое значение.
import { liveQuery } from 'dexie'
import { ref, onMounted, onUnmounted } from 'vue'
import { db } from './db'
export function useLiveTodos() {
const todos = ref([])
let subscription = null
onMounted(() => {
subscription = liveQuery(() => db.todos.toArray())
.subscribe({
next(result) {
todos.value = result
},
error(err) {
console.error(err)
}
})
})
onUnmounted(() => {
subscription?.unsubscribe()
})
return { todos }
}
Механизм liveQuery устраняет необходимость ручного
обновления состояния, обеспечивая реактивность на уровне источника
данных.
Повторяющаяся логика подписки может быть вынесена в универсальный composable, принимающий функцию запроса.
import { ref, onMounted, onUnmounted } from 'vue'
import { liveQuery } from 'dexie'
export function useDexieQuery(queryFn) {
const state = ref(null)
const error = ref(null)
let subscription = null
onMounted(() => {
subscription = liveQuery(queryFn).subscribe({
next(value) {
state.value = value
error.value = null
},
error(err) {
error.value = err
}
})
})
onUnmounted(() => {
subscription?.unsubscribe()
})
return { state, error }
}
Использование:
import { db } from './db'
import { useDexieQuery } from './useDexieQuery'
export function useActiveUsers() {
return useDexieQuery(() =>
db.users.wh ere('active').equals(1).toArray()
)
}
Результаты liveQuery могут выступать базой для
вычисляемых значений через computed.
import { computed } fr om 'vue'
import { useDexieQuery } from './useDexieQuery'
import { db } from './db'
export function useStats() {
const { state: todos } = useDexieQuery(() =>
db.todos.toArray()
)
const completedCount = computed(() =>
todos.value ? todos.value.filter(t => t.done).length : 0
)
return { todos, completedCount }
}
Такой подход разделяет уровень хранения данных и уровень их представления.
При работе с несколькими таблицами Dexie часто требуется агрегировать данные.
import { useDexieQuery } from './useDexieQuery'
import { db } from './db'
import { computed } from 'vue'
export function useDashboard() {
const users = useDexieQuery(() => db.users.toArray())
const posts = useDexieQuery(() => db.posts.toArray())
const summary = computed(() => ({
usersCount: users.state.value?.length ?? 0,
postsCount: posts.state.value?.length ?? 0
}))
return { users, posts, summary }
}
Каждый источник остаётся независимым, но итоговое состояние формируется реактивно.
При использовании liveQuery любые операции записи
автоматически отражаются в подписках.
import { db } from './db'
export async function addTodo(title) {
await db.todos.add({
title,
done: false,
createdAt: Date.now()
})
}
export async function toggleTodo(id, done) {
await db.todos.update(id, { done })
}
Подписки, созданные через liveQuery, реагируют без
дополнительного кода обновления.
Подписки Dexie требуют явного завершения при уничтожении компонента.
onUnmounted(() => {
subscription?.unsubscribe()
})
При использовании универсального composable важно гарантировать:
unsubscribe;Частые обновления базы могут приводить к множественным пересчётам
liveQuery. Для оптимизации применяются:
where,
filter);liveQuery.liveQuery(() =>
db.logs
.wh ere('level')
.equals('error')
.toArray()
)
import { Ref } from 'vue'
interface UseDexieResult<T> {
state: Ref<T | null>
error: Ref<Error | null>
}
Типизация composable позволяет фиксировать структуру данных на уровне доменной модели и снижает риск рассинхронизации состояния.
Типичная структура приложения с Dexie и Vue включает:
Реактивный слой не содержит бизнес-логики хранения, а только трансформирует поток данных в форму, удобную для UI.
export const createListHook = (table) => {
return () =>
useDexieQuery(() => table.toArray())
}
Такой подход снижает дублирование при работе с множеством таблиц с одинаковым паттерном доступа.
Dexie гарантирует консистентность транзакций, однако реактивный слой Vue может получать обновления в быстром темпе. В таких случаях важно:
computed;subscription = liveQuery(queryFn).subscribe({
next(value) {
state.value = value
},
error(err) {
error.value = err
}
})
Ошибки запроса не должны прерывать жизненный цикл компонента, а фиксируются как реактивное состояние.
При серверном рендеринге Dexie недоступен, поэтому реактивные обёртки должны учитывать:
onMounted.const isClient = typeof window !== 'undefined'
При больших объёмах данных предпочтительно:
Это снижает нагрузку на liveQuery и Vue reactivity
system.