Наше портфолио
на основных мобильных площадках
Создание базы IndexedDB
xcode logo

IndexedDB - это встроенная база данных, гораздо более мощная, чем localStorage.

Эта мощность обычно чрезмерна для традиционных клиент-серверных приложений. IndexedDB предназначен для автономных приложений, которые можно комбинировать с ServiceWorkers и другими технологиями.

Нативный интерфейс IndexedDB, описанный в спецификации https://www.w3.org/TR/IndexedDB, основан на событиях.

Мы также можем использовать async / await с помощью обертки на основе обещаний, например https://github.com/jakearchibald/idb. Это довольно удобно, но оболочка не идеальна, она не может заменить события для всех случаев. Итак, мы начнем с событий, а затем, когда мы получим понимание IndexedDb, мы будем использовать обертку.

Открытая база данных

Чтобы начать работать с IndexedDB, нам сначала нужно открыть базу данных. Синтаксис:

let openRequest = indexedDB.open(name, version);

name – a string, the database name. version – a positive integer version, by default 1 (explained below). We can have many databases with different names, but all of them exist within the current origin (domain/protocol/port). Different websites can’t access databases of each other. After the call, we need to listen to events on openRequest object: success: database is ready, there’s the “database object” in openRequest.result, that we should use it for further calls. error: opening failed. upgradeneeded: database is ready, but its version is outdated (see below).

name - строка, имя базы данных. version - положительная целочисленная версия, по умолчанию 1 (поясняется ниже).

У нас может быть много баз данных с разными именами, но все они существуют в текущем источнике (домен / протокол / порт). Разные сайты не могут получить доступ к базам данных друг друга. После вызова нам нужно прослушать события на объекте openRequest:

IndexedDB имеет встроенный механизм «управления версиями схемы», отсутствующий в базах данных на стороне сервера.

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

Если версия локальной базы данных меньше указанной в open, то запускается специальное событие upgradeeeeded, и мы можем сравнивать версии и обновлять структуры данных по мере необходимости.

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

Когда мы впервые публикуем наше приложение, мы открываем его с версией 1 и выполняем инициализацию в обработчике upgradedeneeded:

let openRequest = indexedDB.open("store", 1); openRequest.onupgradeneeded = function() { // triggers if the client had no database // ...perform initialization... }; openRequest.onerror = function() { console.error("Error", openRequest.error); }; openRequest.onsuccess = function() { let db = openRequest.result; // continue to work with database using db object };let openRequest = indexedDB.open("store", 2); openRequest.onupgradeneeded = function() { // the existing database version is less than 2 (or it doesn't exist) let db = openRequest.result; switch(db.version) { // existing db version case 0: // version 0 means that the client had no database // perform initialization case 1: // client had version 1 // update } };

Проблема параллельного обновления

Поскольку мы говорим о версии, давайте рассмотрим небольшую связанную проблему.

Допустим, посетитель открыл наш сайт на вкладке браузера с базой данных версии 1.

Затем мы выкатили обновление, и этот же посетитель открыл наш сайт в другой вкладке. Таким образом, есть две вкладки, обе с нашим сайтом, но одна имеет открытое соединение с БД версии 1, а другая пытается обновить ее в обработчике upgradedeneeded.

Проблема заключается в том, что база данных совместно используется двумя вкладками, так как это один и тот же сайт одного происхождения. И это не может быть как версия 1, так и версия 2. Чтобы выполнить обновление до версии 2, все подключения к версии 1 должны быть закрыты.

Чтобы организовать это, событие versionchange запускает открытый объект базы данных при попытке параллельного обновления. Мы должны послушать его, чтобы закрыть базу данных (и, возможно, предложить посетителю перезагрузить страницу, чтобы загрузить обновленный код).

Если мы не закроем его, то второе, новое соединение будет заблокировано с заблокированным событием вместо успеха.

Вот код для этого:let openRequest = indexedDB.open("store", 2); openRequest.onupgradeneeded = ...; openRequest.onerror = ...; openRequest.onsuccess = function() { let db = openRequest.result; db.onversionchange = function() { db.close(); alert("Database is outdated, please reload the page.") }; // ...the db is ready, use it... }; openRequest.onblocked = function() { // there's another open connection to same database // and it wasn't closed after db.onversionchange triggered for them };

Объектный магазин (Object store)

Чтобы хранить что-то в IndexedDB, нам нужно хранилище объектов.

Хранилище объектов является основной концепцией IndexedDB. Аналоги в других базах данных называются «таблицами» или «коллекциями». Это место, где хранятся данные. База данных может иметь несколько хранилищ: одно для пользователей, другое для товаров и т. Д.

Несмотря на то, что они называются «хранилищем объектов», примитивы также могут храниться.

Мы можем хранить практически любое значение, включая сложные объекты.

IndexedDB использует стандартный алгоритм сериализации для клонирования и хранения объекта. Это как JSON.stringify, но более мощный, способный хранить гораздо больше типов данных.

Пример объекта, который не может быть сохранен: объект с круговыми ссылками. Такие объекты не сериализуемы. JSON.stringify также не работает для таких объектов.

Для каждого значения в магазине должен быть уникальный ключ.

Ключ должен иметь тип один из: число, дата, строка, двоичный файл или массив. Это уникальный идентификатор: мы можем искать / удалять / обновлять значения по ключу.

Хранилище объектов может быть создано / изменено только при обновлении версии БД в обработчике upgradedeneeded.

Это техническое ограничение. Вне обработчика мы сможем добавлять / удалять / обновлять данные, но хранилища объектов можно создавать / удалять / изменять только во время обновления версии.

Для обновления версии базы данных существует два основных подхода:

Мы можем реализовать функции обновления для каждой версии: от 1 до 2, от 2 до 3, от 3 до 4 и т. Д. Затем, при обновлении с необходимостью, мы можем сравнивать версии (например, старые 2, теперь 4) и запускать пошаговые обновления для каждой версии. для каждой промежуточной версии (от 2 до 3, затем от 3 до 4). Или мы можем просто изучить базу данных: получить список существующих хранилищ объектов в виде db.objectStoreNames. Этот объект является DOMStringList, который предоставляет метод содержит (name) для проверки существования. И тогда мы можем делать обновления в зависимости от того, что существует, а что нет.

Для небольших баз данных второй вариант может быть проще.

Вот демонстрация второго подхода:

let openRequest = indexedDB.open("db", 2); // create/upgrade the database without version checks openRequest.onupgradeneeded = function() { let db = openRequest.result; if (!db.objectStoreNames.contains('books')) { // if there's no "books" store db.createObjectStore('books', {keyPath: 'id'}); // create it } };

Операции (Transactions)

Термин «транзакция» является общим и используется во многих видах баз данных.

Транзакция - это групповая операция, которая должна быть либо успешной, либо неудачной.

Например, когда человек что-то покупает, нам нужно:

Вычтите деньги со своего счета. Добавьте предмет в инвентарь.

Было бы очень плохо, если бы мы завершили 1-ю операцию, а затем что-то пошло не так, например, гаснет, и мы не можем сделать 2-й. Оба должны либо преуспеть (покупка завершена, хорошо!), Либо оба провалились (по крайней мере, человек сохранил свои деньги, чтобы они могли повторить попытку).

Транзакции могут это гарантировать.

Все операции с данными должны выполняться внутри транзакции в IndexedDB.

Чтобы начать транзакцию:

db.transaction(store[, type]);

Производительность является причиной, по которой транзакции должны быть помечены как readonly и readwrite.

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

После создания транзакции мы можем добавить элемент в магазин, например так:

let transaction = db.transaction("books", "readwrite"); // (1) // get an object store to operate on it let books = transaction.objectStore("books"); // (2) let book = { id: 'js', price: 10, created: new Date() }; let request = books.add(book); // (3) request.onsuccess = function() { // (4) console.log("Book added to the store", request.result); }; request.onerror = function() { console.log("Error", request.error); };

Автокомиссия транзакций

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

Короткий ответ: мы этого не делаем.

В следующей версии 3.0 спецификации, вероятно, будет ручной способ завершить транзакцию, но сейчас в 2.0 нет.

Когда все запросы на транзакции завершены, а очередь микрозадач пуста, она фиксируется автоматически.

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

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

Принцип автоматической фиксации транзакций имеет важный побочный эффект. Мы не можем вставить асинхронную операцию, такую ​​как fetch, setTimeout, в середине транзакции. IndexedDB не заставит транзакцию ждать, пока они не будут выполнены.

В приведенном ниже коде запрос2 в строке (*) завершается неудачно, поскольку транзакция уже зафиксирована, и не может выполнить в ней какой-либо запрос:

let request1 = books.add(book); request1.onsuccess = function() { fetch('/').then(response => { let request2 = books.add(anotherBook); // (*) request2.onerror = function() { console.log(request2.error.name); // TransactionInactiveError }; }); };

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

Авторы спецификации IndexedDB считают, что транзакции должны быть недолговечными. В основном из соображений производительности.

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

Так что делать?

В приведенном выше примере мы могли бы сделать новую транзакцию db.transaction прямо перед новым запросом (*).

Но было бы еще лучше, если бы мы хотели объединить операции в одной транзакции, чтобы разделить транзакции IndexedDB и «другие» асинхронные элементы.

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

Чтобы определить момент успешного завершения, мы можем прослушать transaction.oncomplete event:

let transaction = db.transaction("books", "readwrite"); // ...perform operations... transaction.oncomplete = function() { console.log("Transaction is complete"); };
Список статей
Реализованные проекты
Больше проектов
Среди наших клиентов
СВЯЖИТЕСЬ С НАМИ
Мы верим, что мобильные решения помогают бизнесу работать эффективнее. Наша компания делает мобильную разработку доступной для бизнеса. Сделать шаг к мобильности бизнеса еще никогда не было так просто!
г. Москва, Севастопольский пр. д56/40
Телефон: 8 (800) 201 6-48-6
E-mail: support@s-m-system.ru
Наше портфолио
ВСЁ ПРОСТО