Перейти к содержимому
Поиск
Разработка

Dexie.js: локальная база в браузере — мастерская и 9 примеров

Работаем с IndexedDB через Dexie: девять живых сценариев, транзакции с откатом, поиск, черновики, миграции и перенос JSON. Мастерская и готовые исходники.

Александр Некрасов
Dexie.js — локальные данные в браузере и IndexedDB

Dexie.js на практике: локальная база для черновиков, контактов и задач. Девять живых сценариев, индексы, транзакции с откатом, liveQuery, миграции и исходники для Windows, Linux и VPS.

Что такое Dexie.js и какую задачу решает локальная база

Пользователь заполняет длинную заявку, случайно обновляет страницу и видит пустые поля. Менеджер готовит список задач в дороге, а связь пропадает. Каталог каждый раз заново загружается с сервера, хотя большая часть записей не изменилась. Во всех этих случаях браузеру полезно хранить структурированные данные между открытиями страницы. Для этого существует IndexedDB, а Dexie.js делает её API удобнее для прикладного JavaScript.

Dexie предоставляет таблицы, асинхронные операции, индексы, транзакции и наблюдение за запросами. Вместо цепочек событий низкоуровневого API разработчик пишет понятные вызовы с await. При этом записи остаются в браузерном хранилище конкретного происхождения: учитываются протокол, домен и порт. Сервер сайта автоматически этих записей не получает.

В статье используем Dexie 4.4.7 и собственную мастерскую с девятью работающими сценариями. Можно добавить контакты, проверить настоящий откат транзакции, перенести данные через JSON и увидеть миграцию схемы. Исходники доступны целиком: интерфейс, модель, сборка и тесты. Библиотека поставляется локально вместе с проектом, поэтому запуск не зависит от CDN.

Где применять: черновики, каталоги, задачи и рабочие кабинеты

Хороший первый сценарий — черновик формы. Текст сохраняется после короткой паузы ввода, а при возвращении пользователь продолжает работу. Другой пример — локальный каталог оборудования: приложение получает справочник через API, записывает его в IndexedDB, затем быстро фильтрует нужные позиции. В личном кабинете можно сохранить выбранные фильтры, незавершённый заказ или подготовленные изменения.

Dexie подходит и для небольших инструментов, которым вообще не нужен аккаунт: учебный планировщик, список проверок, редактор заметок, конфигуратор. Записи остаются у человека на устройстве. Такой подход уменьшает количество обязательных серверных компонентов, однако требует ясного объяснения, где находятся данные и как сделать копию.

Для общей CRM локальная база обычно становится вспомогательным слоем. Сервер хранит подтверждённые документы и права доступа, браузер — временный набор для быстрого интерфейса. Совместная работа, обмен между устройствами и разрешение конфликтов требуют отдельной архитектуры. Обычная установка Dexie не создаёт API, авторизацию и автоматическую синхронизацию; в нашей мастерской этих возможностей тоже нет.

IndexedDB, localStorage и серверная SQL-база: что выбрать

В localStorage удобно держать несколько строковых настроек: выбранную тему или последний открытый раздел. Для большого списка объектов приходится самостоятельно сериализовать весь массив, читать его целиком и следить за обновлениями. Интерфейс localStorage синхронный, поэтому объёмная обработка может мешать работе страницы. IndexedDB предлагает асинхронные операции и выборки по индексам.

Dexie не превращает браузер в полноценный сервер СУБД. Здесь нет обычного SQL, серверных пользователей, централизованного журнала и гарантированной резервной копии. Запросы строятся вокруг ключей, диапазонов и коллекций. Если приложение требует сложных объединений больших таблиц, сначала стоит оценить модель и объём данных, а не переносить привычные SQL-запросы буквально.

Практический выбор зависит от ответственности данных. Для темы оформления достаточно localStorage. Для локальных структурированных записей с поиском и атомарными изменениями полезна Dexie. Для подтверждённых заказов, денежных остатков и общей работы команды нужна серверная база; браузерная копия может ускорять чтение, но не становится единственным источником истины.

Задача Подход Ограничение
Тема, простой переключатель localStorage Строки и синхронный API
Черновики, справочник, локальные задачи IndexedDB + Dexie Данные конкретного браузера
Общие документы и права доступа API + серверная база Нужна серверная инфраструктура

Мини-мастерская: девять сценариев прямо на странице

Начните с контактов: создайте вымышленную запись и измените её баллы. Затем откройте поиск, выберите город и проверьте выборку. Перейдите к задачам, поставьте отметку выполнения и обновите страницу. При том же адресе и разрешённом хранилище состояние сохранится. Эти действия используют настоящую IndexedDB через Dexie, а не массив, который исчезает после перезагрузки.

В разделе транзакций сначала зарезервируйте десять учебных комплектов. Остаток уменьшится со ста до девяноста, в журнале появится операция. Затем нажмите кнопку проверки ошибки: код успеет изменить остаток внутри транзакции, но исключение отменит результат целиком. В живой сводке добавление контакта обновляет показатели через подписку.

Импорт, черновик, миграция и перенос данных доступны в том же переключателе. Сброс требует отдельной отметки и касается только учебной базы. Для результатов есть подготовка JSON и постоянная ссылка скачивания. Если встроенная область неудобна или браузер ограничивает хранилище iframe, откройте мастерскую отдельной вкладкой. Используйте вымышленные записи: эта страница предназначена для изучения инструмента.

Установка, закреплённые версии и устройство проекта

В архиве есть исходные модули и уже собранная папка public. Для изменения проекта используется Node.js 24. Команда npm ci устанавливает версии из lockfile, а сборка объединяет приложение с Dexie в локальный JavaScript-файл. Bootstrap 5.3.8 отвечает за базовые элементы интерфейса и тоже копируется в проект. Сетевые запросы к внешним библиотекам при открытии мастерской не нужны.

Файл src/model.js содержит схему, проверки и операции с данными. В src/app.js находятся обработчики интерфейса и девять сцен. Такое разделение позволяет проверить откат транзакции без нажатий в браузере и затем отдельно оценить удобство форм. Сборка сохраняет читаемую копию модели в public/source; её показывает кнопка просмотра кода.

В рабочем проекте не обновляйте зависимости случайно при каждом развёртывании. Сохраните lockfile, изучите изменения новой версии и повторите проверки. Подготовленная статика работает без Node.js на сервере: после сборки нужен только HTTP-сервер, который корректно отдаёт HTML, CSS и JavaScript.

npm install --save-exact dexie@4.4.7 bootstrap@5.3.8
# В скачанном проекте зависимости уже закреплены:
npm ci
npm test
npm run build

Схема базы: таблицы, первичные ключи и индексы

Схема Dexie перечисляет хранилища и индексируемые свойства. Запись ++id означает автоматически увеличиваемый первичный ключ, &email — уникальный индекс, а [city+score] — составной индекс. Он нужен, когда приложение часто выбирает контакты определённого города с баллом в заданном диапазоне. Индекс должен соответствовать реальному запросу, иначе он лишь добавляет работу при записи.

В строке схемы не требуется перечислять каждое свойство объекта. Поля, которые не являются ключами или индексами, всё равно сохраняются. Например, пояснение к контакту можно записывать без отдельного индекса, если поиск по нему не нужен. Это важное отличие от привычного описания колонок SQL-таблицы.

Имя базы выбирайте для конкретного приложения. Мастерская использует aen-dexie-workshop-v1, а опыт миграции — отдельное имя. Она не перечисляет и не очищает остальные базы сайта. В собственном сервисе добавьте понятный префикс и продумайте, какие данные должны быть общими для разных разделов одного происхождения.

import Dexie from 'dexie';
const db = new Dexie('my-service-local');
db.version(1).stores({
  contacts: '++id,&email,name,city,score,[city+score]',
  tasks: '++id,status,priority',
  drafts: 'id'
});
await db.open();

Пример 1. Контакты: создание, чтение, изменение и удаление

Адресная книга показывает базовый цикл работы с записями. Метод add добавляет объект и возвращает ключ. Метод get читает одну запись, update изменяет отдельные свойства, delete удаляет запись. Для интерфейса важно дождаться завершения операции и только затем сообщить пользователю об успехе. Пока запись идёт, кнопка действия временно недоступна.

В мастерской редактирование заполняет ту же форму и меняет подпись кнопки. Email приводится к нижнему регистру, строки очищаются от внешних пробелов, баллы ограничиваются диапазоном. Уникальный индекс защищает от повторного адреса независимо от того, пришёл контакт через форму или импорт. При ошибке старые данные остаются на месте, а интерфейс показывает объяснение.

Метод put подходит для замены записи с известным ключом, но способен создать её, если такого ключа ещё нет. Поэтому операция редактирования в нашей модели сначала проверяет существование контакта внутри транзакции. Для бизнес-интерфейса это полезно: удалённая в другой вкладке запись не должна незаметно появляться заново.

const id = await db.contacts.add({
  name: 'Анна', city: 'Москва',
  email: 'anna@example.test', score: 80
});
const contact = await db.contacts.get(id);
await db.contacts.update(id, { score: 90 });
await db.contacts.delete(id);

Пример 2. Поиск по составному индексу и сортировка

Вторая сцена соединяет три понятных пользователю условия: город, минимум баллов и начало имени. Город с диапазоном баллов обслуживает составной индекс. После получения подходящих записей имя проверяется в JavaScript. Такой порядок уменьшает количество объектов для дополнительной обработки, если город и диапазон действительно отсекают большую часть каталога.

Важно понимать границу этого примера. Префикс имени здесь не является полнотекстовым поиском: нет морфологии, исправления опечаток и ранжирования по смыслу. Для небольшого локального справочника такой фильтр удобен. Для большого поискового сервиса понадобится другая архитектура, отдельный индекс поиска либо серверный запрос.

Результаты сортируются по убыванию балла, при равенстве — по ключу. Пользователь видит количество найденных записей и понятное пустое состояние. Если убрать город, запрос использует индекс баллов. Для исходного набора условия «Москва», «Ан» и минимум восемьдесят возвращают Анну Демину. Попробуйте изменить её балл в первой сцене и повторить выборку.

const candidates = await db.contacts
  .where('[city+score]')
  .between(['Москва', 80], ['Москва', 100], true, true)
  .toArray();
const found = candidates
  .filter(row => row.name.toLocaleLowerCase('ru').startsWith('ан'))
  .sort((a, b) => b.score - a.score || a.id - b.id);

Пример 3. Список задач, который переживает перезагрузку

Для списка задач достаточно хранить название, статус и приоритет. В интерфейсе пользователь добавляет строку, отмечает выполнение или удаляет ненужную задачу. Запись статуса происходит сразу после действия, а результат остаётся после повторного открытия страницы. Такой сценарий легко перенести в чек-лист подготовки документа, план публикаций или личный список проверок.

Переключение статуса полезно выполнять как чтение и запись в одной транзакции. Между двумя отдельными операциями другая вкладка могла бы изменить ту же запись. Транзакционная функция читает актуальный объект и вычисляет новое значение в согласованной последовательности. При этом полноценные конфликты совместной работы пользователей она не решает: здесь речь о локальной базе одного браузера.

После обновления списка мастерская возвращает фокус на изменённый переключатель. После создания — в поле новой задачи. Это небольшая деталь, но она позволяет работать клавиатурой без постоянного поиска позиции. Подпись состояния сообщает число завершённых задач, а ограничение длины помогает не превращать одну строку в бесконечный блок текста.

await db.transaction('rw', db.tasks, async () => {
  const task = await db.tasks.get(id);
  if (!task) throw new Error('Задача уже удалена');
  await db.tasks.update(id, {
    status: task.status === 'done' ? 'todo' : 'done'
  });
});

Пример 4. Две таблицы и настоящий откат транзакции

Представьте учебный склад комплектов. Резервирование должно уменьшить остаток и добавить запись в журнал. Если сохранить только первое действие, интерфейс покажет потерянный товар. Если выполнить только второе, журнал будет обещать резерв, которого нет. Транзакция объединяет эти изменения: они фиксируются вместе либо целиком отменяются.

В мастерской есть отдельная кнопка ошибки. Исключение возникает после изменения остатка, но до создания записи журнала. После завершения неуспешной операции повторное чтение показывает прежние значения. Это полезнее формального примера с двумя успешными вызовами: можно увидеть, зачем транзакция нужна при реальном сбое.

Перехватывайте ошибку снаружи транзакционной функции, если ожидаете полный откат. Если обработать исключение внутри и продолжить выполнение без нового исключения, Dexie может считать операцию успешно завершённой. Сетевой запрос, ожидание таймера и другую внешнюю асинхронную работу вынесите за пределы транзакции: IndexedDB не должна оставаться открытой в ожидании произвольного внешнего результата.

try {
  await db.transaction('rw', db.stock, db.orders, async () => {
    const stock = await db.stock.get('kit');
    if (stock.quantity < 10) throw new Error('Недостаточно');
    await db.stock.update('kit', { quantity: stock.quantity - 10 });
    if (simulateFailure) throw new Error('Учебный сбой');
    await db.orders.add({ quantity: 10, created: new Date().toISOString() });
  });
} catch (error) {
  showError(error.message); // вне транзакции
}

Пример 5. liveQuery: сводка обновляется после записи

После добавления контакта приходится обновлять список, счётчик, средний балл и показатели по городам. Если вручную вызывать все функции в каждом обработчике, легко забыть одну из них. liveQuery позволяет описать чтение данных и подписаться на его результат. При релевантных изменениях через Dexie запрос выполняется снова, а интерфейс получает свежие значения.

В нашей сцене подписка читает контакты, затем вычисляет количество записей, долю города и средний балл. Добавьте учебный контакт: показатели изменятся без отдельной кнопки обновления. Можно открыть вторую вкладку мастерской на том же происхождении и изменить контакты там. Подписка реагирует на наблюдаемые изменения Dexie в этой браузерной среде.

Такое наблюдение не отправляет данные на сервер и не связывает устройства. При уходе со сцены подписка обязательно отключается, иначе скрытые интерфейсы продолжат получать результаты. В большом приложении разумно выбирать только необходимые данные и контролировать стоимость повторного вычисления. Для нескольких простых показателей полное чтение маленького набора приемлемо; для большого каталога следует измерять нагрузку.

import { liveQuery } from 'dexie';
const subscription = liveQuery(() => db.contacts.toArray())
  .subscribe({
    next: rows => renderSummary(rows),
    error: error => showError(error.message)
  });
// При удалении экрана или компонента:
subscription.unsubscribe();

Пример 6. Пакетный импорт JSON без частично сохранённых записей

Вставка массива контактов — типичная задача миграции небольшого справочника из другого инструмента. Сначала приложение разбирает JSON, проверяет тип документа, количество строк, допустимые поля и значения. Только после этого начинается запись. Проверка входа не заменяет ограничения базы: уникальный email может совпасть с уже существующей записью, о которой импорт ничего не знает.

Метод bulkAdd экономит накладные расходы на добавление пакета. Чтобы ошибка одной строки не оставляла часть новых контактов, вызов помещён в транзакцию, а ошибка выходит наружу. В мастерской первый импорт готового массива создаёт две записи. Повторение того же импорта показывает ошибку уникальности и сохраняет прежний набор без дополнительных строк.

Импорт добавляет записи; это отдельное действие от восстановления полного снимка. В интерфейсе эти операции разделены и подписаны. Для производственного инструмента дополните импорт предварительным просмотром, отчётом по строкам и явным выбором политики дублей: отклонить, пропустить или обновить. Политику нельзя незаметно менять только ради того, чтобы скрыть ошибку.

const rows = parseAndValidateContacts(textFromEditor);
await db.transaction('rw', db.contacts, async () => {
  await db.contacts.bulkAdd(rows);
});
// catch находится снаружи: ошибка пакета отменяет его целиком.

Пример 7. Черновик формы и сохранение после паузы

Для черновика не обязательно создавать запись на каждое нажатие клавиши. Мастерская ждёт паузу пятьсот миллисекунд и сохраняет текущий текст. Пользователь видит различие между состояниями «есть изменения» и «сохранено». Кнопка немедленного сохранения полезна перед закрытием страницы, особенно если человек только что дописал важную строку.

При переходе между сценами ожидающий черновик сохраняется до замены интерфейса. При повторном открытии сцены приложение читает запись с постоянным ключом request. Это подходит для одной формы. Если одновременно редактируются разные документы, ключ должен включать идентификатор документа и, при необходимости, пользователя. Иначе черновики разных экранов будут перезаписывать друг друга.

Асинхронную запись нельзя гарантированно завершить после закрытия вкладки. Не обещайте, что обработчик ухода сохранит любой последний символ. В рабочем интерфейсе полезны индикатор незавершённого сохранения, регулярная запись и понятная серверная кнопка отправки. Локальный черновик не равен отправленной заявке: пользователь должен видеть, на каком этапе находятся данные.

let timer;
textarea.addEventListener('input', () => {
  clearTimeout(timer);
  timer = setTimeout(async () => {
    await db.drafts.put({
      id: 'request', content: textarea.value,
      updated: new Date().toISOString()
    });
    showSaved();
  }, 500);
});

Пример 8. Версии схемы и преобразование существующих данных

Приложение развивается: раньше у человека было поле fullName, теперь нужны name и city. Простого изменения JavaScript-объекта недостаточно, потому что у пользователей уже есть старая база. Новая версия описывает целевую схему, а функция upgrade преобразует существующие записи в рамках обновления.

Учебная сцена специально использует отдельную базу. Кнопка сначала создаёт вариант первой версии, записывает вымышленное имя, закрывает соединение и открывает вторую версию. Вы видите JSON до и после: ключ сохраняется, имя переносится, город добавляется. Повторный запуск пересоздаёт только эту экспериментальную базу. Основные контакты, задачи и черновик мастерской остаются нетронутыми.

В настоящем приложении нельзя удалять пользовательскую базу ради удобного обновления. Сохраняйте историю необходимых версий и проверяйте миграцию на копиях разных старых наборов. Пустая новая установка и обновление уже заполненного хранилища — разные проверки. При открытых старых вкладках обновление может ждать освобождения соединения; интерфейс должен объяснить человеку, что требуется перезагрузка.

db.version(1).stores({ people: 'id,fullName' });
db.version(2).stores({ people: 'id,name,city' })
  .upgrade(tx => tx.table('people').toCollection().modify(row => {
    row.name = row.fullName;
    row.city = 'Москва';
    delete row.fullName;
  }));

Пример 9. Экспорт и восстановление согласованного снимка

Экспорт помогает перенести учебные результаты в другой браузер или сохранить копию перед экспериментом. В мастерской снимок содержит контакты, задачи, остаток, журнал и черновик. Таблицы читаются в одной транзакции, чтобы набор соответствовал согласованному состоянию базы. Служебная отметка начального заполнения не является пользовательскими данными и восстанавливается самой программой.

Кнопка подготовки создаёт JSON и показывает постоянную ссылку скачивания. Пользователь сам запускает загрузку и видит имя файла. Для восстановления нужно вставить документ, подтвердить замену и пройти проверку. Неверная схема, повторные идентификаторы, превышение размера или нарушение учебного баланса отклоняются до очистки таблиц.

Проверенный набор записывается в единой транзакции. Если запись одной таблицы не удалась, прежнее состояние восстанавливается автоматически откатом. Для больших баз экспорт в JSON может потреблять много памяти; здесь объём намеренно ограничен. Производственный перенос требует отдельного формата, версии, контроля размера и понимания, какие поля допустимо выдавать пользователю в файл.

const snapshot = await db.transaction('r', db.tables, async () => ({
  schema: 1,
  contacts: await db.contacts.toArray(),
  tasks: await db.tasks.toArray()
}));
const json = JSON.stringify(snapshot, null, 2);
const url = URL.createObjectURL(new Blob([json], {
  type: 'application/json;charset=utf-8'
}));
downloadLink.href = url;
downloadLink.download = 'local-data.json';
// Отозвать URL при замене файла или уходе со страницы.

Проверка данных: схема, ограничения и бизнес-правила

Браузерная форма помогает человеку заполнить поля, но импорт JSON обходит её ограничения. Поэтому модель повторно проверяет строки, целые числа, перечень городов и допустимые свойства. Email нормализуется перед уникальным индексом. Неизвестное поле в контакте отклоняется: опечатка не должна тихо создавать данные, которые приложение никогда не покажет.

Для снимка проверяются не только отдельные записи, но и связь таблиц. Учебный склад начинался со ста комплектов, значит остаток плюс сумма резервов должны давать сто. Такой инвариант выявляет повреждённый или вручную изменённый документ. В реальном бизнесе правила будут другими: доступный кредит, допустимый переход статуса, связь документа с владельцем.

Ограничения мастерской установлены специально: двести контактов, сто задач и операций, пятьдесят строк одного импорта, две тысячи символов черновика. Это не пределы Dexie. Они делают эксперимент предсказуемым и не позволяют случайно вставить огромный набор. На сервере авторизованные операции нужно проверять заново: локальная валидация улучшает интерфейс, но не устанавливает доверие.

function score(value) {
  if (!Number.isInteger(value) || value < 0 || value > 100) {
    throw new Error('Баллы должны быть целым числом от 0 до 100');
  }
  return value;
}

Ошибки хранилища и понятные сообщения пользователю

Операция может не завершиться по разным причинам: повторный уникальный ключ, недоступное хранилище, исчерпанная квота или закрытое соединение. Пользователю нужна причина и следующий шаг. Сообщение «такой email уже существует» полезнее сырого объекта исключения. При запрете хранилища можно предложить проверить настройки сайта или открыть мастерскую отдельной вкладкой.

Не показывайте успех до разрешения Promise. Отключайте кнопку на время записи и обязательно возвращайте её в рабочее состояние после ошибки. Для разрушающего действия сохраняйте дополнительное условие: кнопка восстановления должна снова стать недоступной, если отметка согласия снята после успешной операции. Простое безусловное включение всех кнопок в finally может нарушить эту логику.

Ошибки подписки тоже требуют обработки. Если liveQuery перестал получать данные, старые числа не должны выглядеть гарантированно актуальными. Для производственного кабинета добавьте журнал технических деталей без секретов и понятное состояние повторной попытки. При этом автоматическое повторение разрушительных действий допустимо только с продуманной защитой от дублирования.

try {
  await db.contacts.add(contact);
  showSuccess('Контакт сохранён');
} catch (error) {
  showError(error.name === 'ConstraintError'
    ? 'Этот email уже есть в адресной книге'
    : 'Не удалось сохранить контакт');
}

Несколько вкладок: обновления, закрытие соединения и конфликты

Две вкладки одного сайта могут обращаться к одной локальной базе. Это удобно для сводки и редактора, но требует аккуратных операций. Чтение текущего значения и его изменение лучше объединять, если результат зависит от предыдущего состояния. При простом сохранении полной формы одна вкладка может перезаписать более свежую правку другой; транзакция сама по себе не определяет намерения пользователя.

Для важных документов добавляют версию записи или время изменения и сравнивают ожидаемую версию перед сохранением. Если документ изменился, интерфейс предлагает перечитать его или разобраться с различиями. Аналогичная идея нужна и при отправке на сервер, только проверка выполняется на доверенной стороне.

Когда другая вкладка обновляет схему IndexedDB, старое соединение должно освободить базу. Dexie поддерживает событие versionchange; мастерская закрывает соединение и объясняет необходимость перезагрузки. Подписки при уходе со страницы отключаются. Такой жизненный цикл особенно важен в приложениях, где пользователь держит кабинет открытым несколько дней.

db.on('versionchange', () => db.close());
db.on('blocked', () => {
  showError('Закройте другие вкладки приложения и повторите открытие');
});

Квоты, очистка браузера и хранение секретов

Объём доступного хранилища зависит от браузера, устройства, свободного места и политики очистки. Универсального обещания «IndexedDB всегда сохраняет столько-то гигабайт» давать нельзя. Пользователь может удалить данные сайта, приватный режим меняет жизненный цикл, а система может освободить место по собственным правилам. Поэтому локальная база не должна быть единственной копией критически важных сведений.

Данные IndexedDB доступны JavaScript-коду соответствующего происхождения. Это не защищённое хранилище паролей. Уязвимость XSS, скомпрометированная зависимость или лишний сторонний скрипт способны затронуть локальные записи. Не помещайте туда секретные ключи сервера, пароль базы и другие данные, которые вообще не должны попадать клиенту.

Для проекта с пользовательскими документами продумайте экспорт, серверные копии и политику удаления. Метод оценки хранилища показывает приблизительное использование и квоту, а запрос постоянного хранения зависит от решения браузера. Такие API полезны для диагностики, но не заменяют резервное копирование и не гарантируют вечное сохранение после любых действий пользователя.

const estimate = await navigator.storage?.estimate();
console.log({ usedBytes: estimate?.usage, quotaBytes: estimate?.quota });
// Значения приблизительные и зависят от браузера.

Работа без сети: где заканчивается ответственность Dexie

Локальная база продолжает обслуживать записи уже загруженного приложения без сетевого запроса. Однако это не означает, что сама страница обязательно откроется после отключения интернета. HTML, JavaScript, CSS и изображения тоже должны быть доступны. Для управляемого автономного запуска обычно добавляют service worker и стратегию кеширования оболочки приложения.

В этой мастерской service worker не включён. Мы демонстрируем операции с данными, а не обещаем полностью автономную установку. Разница важна при переносе идеи в кабинет: человек может потерять связь во время работы и продолжить редактирование, но последующее холодное открытие страницы потребует отдельно подготовленного кеша.

Для очереди отправки на сервер нужны дополнительные состояния: локальный черновик, ожидает отправки, подтверждён сервером, конфликт, ошибка. Каждая операция получает стабильный идентификатор, чтобы повтор после обрыва связи не создавал дубль. После подтверждения сервером локальная очередь обновляется. Dexie удобна как место хранения такой очереди, но протокол обмена и правила конфликтов разрабатывает приложение.

Как связать локальную базу с PHP или Node.js API

Распространённая схема состоит из двух этапов. Сначала браузер получает проверенный набор с API и разбирает ответ. Затем короткая транзакция записывает подготовленные объекты в IndexedDB. Не помещайте сетевое ожидание внутрь транзакции: соединение с сервером может зависнуть, а IndexedDB завершает неактивные транзакции по собственному жизненному циклу.

Для PHP и Node.js принцип одинаковый. Сервер проверяет сессию, права, поля и бизнес-правила, а клиент использует локальную базу для быстрого чтения и черновиков. При смене пользователя обязательно решите, что делать с предыдущими локальными данными. Общая база одного происхождения не понимает автоматически, что человек вышел из аккаунта.

Ниже приведён каркас загрузки, а не готовый защищённый обработчик. Endpoint должен принадлежать вашему приложению, возвращать ожидаемую схему и ограниченный объём. Перед записью требуется собственная проверка ответа. Для отправки правок добавьте серверную идемпотентность, обработку конфликтов и явное подтверждение сохранения; успешная запись в IndexedDB подтверждает только локальный этап.

const response = await fetch('/api/contacts', {
  credentials: 'same-origin'
});
if (!response.ok) throw new Error('HTTP ' + response.status);
const rows = validateServerContacts(await response.json());
await db.transaction('rw', db.contacts, async () => {
  await db.contacts.bulkPut(rows);
});

Запуск на Windows: распаковка, сборка и локальный адрес

После скачивания архива распакуйте его в отдельный каталог и откройте PowerShell в папке проекта. Проверьте установленную версию Node.js. Команды с npm.cmd позволяют запускать npm в Windows без зависимости от разрешения выполнения PowerShell-скрипта обёртки. После установки зависимостей выполните тесты и сборку, затем запустите локальный сервер.

Адрес по умолчанию — http://127.0.0.1:8161/. Сервер слушает только локальный интерфейс и не предназначен для публикации в интернете. Не открывайте страницу двойным щелчком через file://: HTTP нужен для предсказуемого происхождения, модулей и загрузки исходников. Если порт занят, задайте другой через переменную окружения.

Помните, что смена порта меняет происхождение. Контакты, созданные на одном порту, не появятся на другом автоматически. Это ожидаемая изоляция браузера. Для переноса используйте подготовку JSON и восстановление. В распакованном проекте исходники уже находятся рядом; кнопка скачивания ZIP рассчитана на размещённый рядом архив опубликованной мастерской.

Expand-Archive -LiteralPath .\dexie-project.zip -DestinationPath .\dexie-demo
Set-Location .\dexie-demo\dexie-project
node --version
npm.cmd ci
npm.cmd test
npm.cmd run build
npm.cmd start
# При занятом порте:
# $env:PORT = '8162'
# npm.cmd start

Linux: разработка и проверка готовой статики

На Linux используется тот же исходный проект и lockfile. Установите поддерживаемую версию Node.js 24 своим обычным способом, распакуйте архив и выполните команды в каталоге проекта. Никаких компонентов IndexedDB на операционную систему ставить не нужно: хранилище предоставляет браузер, а Node.js нужен только для сборки, тестов и локального сервера.

Тестовая среда Node.js использует fake-indexeddb. Это удобно для проверки модели, но не проверяет поведение настоящего браузерного профиля, квоты или ограничения iframe. После тестов откройте страницу в браузере и выполните пользовательские сценарии. Для просмотра готовой папки можно использовать уже установленный PHP со встроенным сервером: серверная бизнес-логика при этом не появляется.

Если проект разрабатывается на удалённом сервере, не открывайте демонстрационный Node-сервер на все сетевые интерфейсы без необходимости. Для просмотра используйте SSH-туннель либо полноценный веб-сервер с доменом и HTTPS. В production достаточно готовых файлов public; переносить весь каталог зависимостей ради раздачи этой мастерской не требуется.

unzip dexie-project.zip -d dexie-demo
cd dexie-demo/dexie-project
node --version
npm ci
npm test
npm run build
npm start
# Альтернатива для уже собранных файлов при установленном PHP:
# php -S 127.0.0.1:8161 -t public

Размещение на VPS: домен, HTTPS и статический каталог

Для отдельного домена подготовьте каталог с содержимым public. Веб-серверу нужны права чтения файлов и прохода по каталогам. Node.js на VPS не обязателен, если сборка выполнена заранее. В примере Caddy раздаёт статику, включает сжатие и обслуживает домен по HTTPS при корректных DNS и сетевом доступе. Подставьте своё имя сайта и путь.

Перед применением конфигурации проверьте её синтаксис, затем выполните reload существующего сервиса. Убедитесь, что HTML, JavaScript и CSS возвращаются с корректными типами содержимого. Кнопка исходников использует относительный адрес ../dexie-project.zip; при самостоятельной публикации положите архив по соответствующему URL или измените ссылку на свой раздел загрузок.

Переезд с локального HTTP на HTTPS-домен создаёт другое происхождение и другое хранилище. Сервер не получает прежние данные автоматически. Для учебного переноса есть JSON; для рабочих пользователей нужен продуманный процесс миграции. При встраивании в существующий PHP-сайт удобно оставить мастерскую отдельной страницей или iframe, сохранив изоляцию стилей от темы сайта.

dexie.example.com {
    root * /var/www/dexie/public
    encode zstd gzip
    file_server
}

# После сохранения конфигурации, в терминале Linux:
# sudo caddy validate --config /etc/caddy/Caddyfile
# sudo systemctl reload caddy
# curl -I https://dexie.example.com/app.js

Производительность: измеряйте выборки и стоимость интерфейса

Индекс полезен только тогда, когда приложение действительно использует его для отбора. Чтение всей таблицы через toArray и последующая фильтрация допустимы для маленького учебного набора, но плохо масштабируются. Для каталога начните с типичных запросов, выберите подходящие ключи и ограничьте количество отображаемых записей. Отдельно измеряйте время чтения и время создания DOM.

Для пакетной записи используйте bulk-операции и короткие транзакции. При большом импорте продумайте порции, прогресс и возможность отмены. Разделение на порции меняет атомарность: уже завершённые части не откатятся вместе с последней. Пользователь должен понимать, весь импорт выполняется целиком или допускает частичное завершение.

Живая сводка тоже имеет стоимость. Несколько подписок, каждая из которых перечитывает огромную таблицу, способны перегрузить слабое устройство. Сначала задайте реалистичный объём данных, затем проверьте реальные действия на мобильной ширине и при повторном обновлении. Число строк в демонстрации не является обещанием производительности на любом устройстве.

const started = performance.now();
const rows = await db.contacts.where('city').equals('Москва').limit(50).toArray();
console.log('Чтение, мс:', performance.now() - started);
renderContacts(rows); // отдельно измеряйте отрисовку

Тестирование: что проверяется автоматически и что нужно открыть руками

В проект включены тесты настоящей Dexie поверх fake-indexeddb. Они проверяют уникальность email, составной индекс, переключение задач, откат после исключения, атомарность пакетного добавления, экспорт и восстановление. Отдельные проверки проходят миграцию между версиями, обновление liveQuery и изоляцию учебных баз. Такой набор ловит ошибки логики до открытия интерфейса.

Эмуляция IndexedDB в Node.js не заменяет браузерную проверку. В настоящем браузере нужно посмотреть сохранение после перезагрузки, работу двух вкладок, загрузку JSON, ограничения хранилища и поведение при изменении схемы. Визуальная проверка подтверждает, что человек может найти действие, увидеть ошибку и завершить сценарий, а не только получить правильное число из функции.

На мобильной ширине проверьте формы, длинные email, JSON-редактор, журнал и кнопки миграции. Код может прокручиваться внутри блока, но вся страница не должна уезжать вбок. Физический телефон дополнительно проверяет экранную клавиатуру, касания и особенности браузера; сужение окна на компьютере покрывает только часть этих условий.

npm test
npm run build
npm start
# Затем вручную: CRUD → reload → rollback → export → restore.

Как устроен удобный интерфейс локального инструмента

Локальное приложение должно объяснять место хранения столь же ясно, как обычный сервис объясняет отправку на сервер. В мастерской это видно в верхнем уведомлении и подписях сцен. Слова «сохранено в IndexedDB» не подменяют подтверждение отправленной заявки. Для читателя сразу доступен переход к живому примеру, а подробный код находится ниже и в архиве.

На узком экране поля выстраиваются в один столбец. Подписи остаются над контролами, длинные строки переносятся, действия имеют заметную область нажатия. После операции показывается текстовый результат, поэтому цвет не является единственным способом понять успех или ошибку. Для клавиатуры предусмотрен видимый фокус, а кнопки удаления получают подпись с названием записи.

Сброс и восстановление требуют явной отметки, потому что меняют сразу несколько таблиц. Обычные учебные действия выполняются без лишних диалогов. Такой баланс помогает экспериментировать и сохраняет понимание последствий. В собственном кабинете добавьте подтверждение именно там, где потеря данных существенна, и не перегружайте каждое простое действие одинаковыми предупреждениями.

Что переносить в собственный проект и где читать дальше

Начните с одной полезной задачи: черновика формы, локального списка или кешируемого справочника. Скопируйте идею модели и транзакций, выберите своё имя базы и определите ответственность данных. Затем добавьте явные состояния сохранения, проверку входа и экспорт. Для серверного обмена отдельно спроектируйте авторизацию, повторные запросы, конфликты и подтверждённый статус.

Архив содержит HTML5, Bootstrap, исходные модули, готовую статику, README, lockfile и тесты. Собственный код мастерской распространяется по MIT; лицензии Dexie и Bootstrap включены в поставку. Вы можете адаптировать проект под рабочий интерфейс, сохраняя необходимые уведомления лицензий и изменяя учебные ограничения под свою задачу.

Официальная документация полезна как продолжение практики: сначала устройство схемы, затем транзакции, наблюдение и миграции. Материалы MDN объясняют ограничения браузерного хранения и квоты. При обновлении версии сверяйте используемые методы с актуальной документацией и повторяйте тесты на существующих данных, чтобы развитие приложения сохраняло результаты пользователей.