Сравнение с нативным IndexedDB API

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

Несмотря на широкие возможности, нативный API IndexedDB считается одним из самых сложных браузерных интерфейсов хранения данных. Основные причины связаны с асинхронной событийной моделью, громоздким синтаксисом и необходимостью ручного управления большим количеством низкоуровневых деталей.

Библиотека Dexie.js создавалась как надстройка над IndexedDB, которая сохраняет всю мощность движка базы данных, но предоставляет гораздо более удобный интерфейс программирования.


Общий подход к работе

Нативный IndexedDB строится вокруг событий:

const request = indexedDB.open("AppDB", 1);

request.onupgradenee ded = event => {
    const db = event.target.result;

    db.createObjectStore("users", {
        keyPath: "id"
    });
};

request.onsucc ess = event => {
    const db = event.target.result;

    const tx = db.transaction("users", "readwrite");

    const store = tx.objectStore("users");

    store.add({
        id: 1,
        name: "Alex"
    });
};

Даже простая операция требует нескольких уровней вложенности и большого количества служебного кода.

Тот же пример на Dexie.js выглядит значительно компактнее:

const db = new Dexie("AppDB");

db.version(1).stores({
    users: "id"
});

await db.users.add({
    id: 1,
    name: "Alex"
});

Вместо работы через события используется современная модель Promise и синтаксис async/await.


Создание базы данных

Нативный IndexedDB

Создание базы осуществляется через функцию indexedDB.open().

const request = indexedDB.open("StoreDB", 1);

Если версия увеличивается, запускается обработчик обновления схемы:

request.onupgradenee ded = event => {
    const db = event.target.result;

    db.createObjectStore("products", {
        keyPath: "id"
    });
};

Разработчик самостоятельно контролирует процесс создания хранилищ, индексов и миграций.


Dexie.js

В Dexie структура базы описывается декларативно:

const db = new Dexie("StoreDB");

db.version(1).stores({
    products: "id"
});

Описание схемы становится компактным и читаемым.

Для сложных проектов схема базы выглядит как единый конфигурационный блок:

db.version(1).stores({
    users: "++id,name,email",
    orders: "++id,userId,date",
    products: "++id,title,category"
});

Определение индексов

IndexedDB

Создание индексов выполняется вручную:

request.onupgradenee ded = event => {
    const db = event.target.result;

    const store = db.createObjectStore("users", {
        keyPath: "id"
    });

    store.createIndex("email", "email", {
        unique: true
    });

    store.createIndex("name", "name");
};

Для большого количества полей код быстро разрастается.


Dexie.js

Индексы описываются строкой схемы:

db.version(1).stores({
    users: "++id,&email,name"
});

Обозначения:

Символ Назначение
++ Автоинкремент
& Уникальный индекс
* Мультииндекс
[] Составной индекс

Пример:

db.version(1).stores({
    users: "++id,&email,[firstName+lastName]"
});

Схема становится самодокументируемой.


Добавление данных

IndexedDB

Запись объекта требует создания транзакции и получения хранилища:

const tx = db.transaction(
    "users",
    "readwrite"
);

const store = tx.objectStore("users");

store.add({
    id: 1,
    name: "John"
});

Для отслеживания результата обычно добавляются обработчики:

const request = store.add(user);

request.onsucc ess = () => {
    console.log("Saved");
};

request.oner ror = () => {
    console.log("Error");
};

Dexie.js

Добавление выполняется одной командой:

await db.users.add({
    name: "John"
});

Получение идентификатора:

const id = await db.users.add({
    name: "John"
});

Код становится значительно короче.


Получение записи

IndexedDB

const tx = db.transaction("users");

const store = tx.objectStore("users");

const request = store.get(1);

request.onsucc ess = event => {
    console.log(event.target.result);
};

Dexie.js

const user = await db.users.get(1);

console.log(user);

Разница особенно заметна при большом количестве запросов.


Получение всех записей

IndexedDB

Современные браузеры поддерживают метод:

store.getAll();

Однако требуется обработка событий:

const request = store.getAll();

request.onsucc ess = event => {
    console.log(event.target.result);
};

Dexie.js

const users = await db.users.toArray();

Результат сразу возвращается в виде массива.


Поиск по индексу

IndexedDB

const index = store.index("email");

const request = index.get(
    "user@mail.com"
);

request.onsucc ess = event => {
    console.log(
        event.target.result
    );
};

Dexie.js

const user = await db.users
    .where("email")
    .equals("user@mail.com")
    .first();

Синтаксис напоминает работу с ORM и SQL-подобными запросами.


Фильтрация данных

IndexedDB

Для сложных выборок часто приходится использовать курсоры:

const request = store.openCursor();

request.onsucc ess = event => {
    const cursor = event.target.result;

    if (cursor) {
        if (cursor.value.age > 18) {
            console.log(cursor.value);
        }

        cursor.continue();
    }
};

Dexie.js

const adults = await db.users
    .filter(user => user.age > 18)
    .toArray();

Логика становится более декларативной.


Курсоры

Курсоры являются одной из самых сложных частей IndexedDB.

IndexedDB

const request = store.openCursor();

request.onsucc ess = event => {
    const cursor = event.target.result;

    if (cursor) {
        console.log(cursor.value);

        cursor.continue();
    }
};

Dexie.js

await db.users.each(user => {
    console.log(user);
});

Либо:

for await (const user of db.users) {
    console.log(user);
}

Работа с коллекциями становится значительно удобнее.


Обновление записей

IndexedDB

const request = store.put({
    id: 1,
    name: "Updated"
});

Необходимо контролировать состояние транзакции и результат операции.


Dexie.js

await db.users.update(
    1,
    {
        name: "Updated"
    }
);

Или:

await db.users.put({
    id: 1,
    name: "Updated"
});

Удаление данных

IndexedDB

store.delete(1);

С дополнительной обработкой событий:

const request = store.delete(1);

request.onsucc ess = () => {
    console.log("Deleted");
};

Dexie.js

await db.users.delete(1);

Код выглядит значительно проще и лучше вписывается в современные приложения.


Транзакции

IndexedDB

Работа с транзакциями требует ручного управления:

const tx = db.transaction(
    ["users", "orders"],
    "readwrite"
);

const usersStore =
    tx.objectStore("users");

const ordersStore =
    tx.objectStore("orders");

usersStore.put(user);

ordersStore.put(order);

tx.oncompl ete = () => {
    console.log("Done");
};

Dexie.js

await db.transaction(
    "rw",
    db.users,
    db.orders,
    async () => {

        await db.users.put(user);

        await db.orders.put(order);
    }
);

Транзакционный код становится линейным и хорошо читаемым.


Обработка ошибок

IndexedDB

Ошибки распределены между множеством событий:

request.oner ror = event => {
    console.error(
        event.target.error
    );
};

Для крупных приложений возникает большое количество обработчиков.


Dexie.js

Используется привычный механизм Promise:

try {
    await db.users.add(user);
}
catch (error) {
    console.error(error);
}

Логика обработки ошибок становится централизованной.


Миграции схемы

IndexedDB

При обновлении версии необходимо самостоятельно определять изменения:

request.onupgradenee ded = event => {

    const db = event.target.result;

    if (event.oldVersion < 2) {
        db.createObjectStore("orders");
    }

    if (event.oldVersion < 3) {
        const store =
            event.currentTarget
                 .transaction
                 .objectStore("users");

        store.createIndex(
            "email",
            "email"
        );
    }
};

Код быстро усложняется по мере роста проекта.


Dexie.js

Каждая версия описывается отдельно:

db.version(1).stores({
    users: "++id,name"
});

db.version(2).stores({
    users: "++id,name",
    orders: "++id,date"
});

Дополнительно поддерживаются миграции данных:

db.version(3)
    .stores({
        users: "++id,name,email"
    })
    .upgrade(tx => {

        return tx.table("users")
            .toCollection()
            .modify(user => {

                user.email = "";
            });
    });

Подход существенно упрощает сопровождение базы.


Читаемость кода

Одним из главных преимуществ Dexie является уменьшение объёма шаблонного кода.

Типичный запрос в IndexedDB:

const tx =
    db.transaction(
        "users"
    );

const store =
    tx.objectStore(
        "users"
    );

const index =
    store.index(
        "email"
    );

const request =
    index.get(
        email
    );

request.onsucc ess =
    event => {

        resolve(
            event.target.result
        );
    };

Эквивалент на Dexie:

const user =
    await db.users
        .where("email")
        .equals(email)
        .first();

Разница становится особенно заметной в проектах с десятками таблиц и сотнями запросов.


Производительность

Dexie не является отдельной системой хранения данных. Все операции в конечном итоге выполняются через IndexedDB.

По этой причине:

  • данные сохраняются в IndexedDB;
  • используются те же объектные хранилища;
  • работают те же индексы;
  • применяется тот же движок браузера;
  • сохраняются ограничения IndexedDB.

Накладные расходы библиотеки обычно минимальны и практически незаметны для большинства приложений.

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


Когда предпочтителен нативный IndexedDB

Использование чистого IndexedDB может быть оправдано в следующих случаях:

  • критически важен минимальный размер бандла;
  • требуется полный контроль над низкоуровневым API;
  • проект содержит очень ограниченное количество операций с базой данных;
  • отсутствует возможность подключения внешних библиотек;
  • необходимо использовать специфические возможности IndexedDB напрямую.

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


Когда предпочтителен Dexie.js

Dexie особенно эффективен в следующих категориях приложений:

  • офлайн-приложения;
  • Progressive Web Apps;
  • системы с большим количеством локальных данных;
  • клиентские CRM и ERP;
  • редакторы документов;
  • системы кэширования API;
  • приложения с синхронизацией данных;
  • сложные SPA на React, Vue, Angular и других фреймворках.

Главное преимущество библиотеки заключается в том, что она устраняет большую часть сложности IndexedDB, сохраняя доступ ко всем возможностям встроенной браузерной базы данных. Благодаря декларативному описанию схем, поддержке Promise, удобным запросам, встроенным транзакциям и механизмам миграции Dexie превращает IndexedDB из низкоуровневого API в полноценный и удобный инструмент хранения данных на стороне клиента.