Изучаем IndexedDB

Spread the love

IndexedDB – это NoSQL база данных, которую можно использовать внутри любого браузера для хранения большого количества данных, как если бы вы работали с базой данных, такой как MongoDB. Если вы создаете веб-приложение или расширение для браузера, в котором хранится много данных, IndexedDB – это то, что нужно!

В этом руководстве мы рассмотрим основы использования IndexedDB и создадим простое веб-приложение для заметок в качестве обзора концепций IndexedDB. Более подробнее о принципах использования IndexedDB можно почитать тут.

Зачем использовать IndexedDB в своем веб-приложении?

Основные отличия, IndexedDB от localStorage:

  • Нет ограничений по размеру; если ваше приложение работает с большим количеством данных, больше, чем несколько мегабайт.
  • IndexedDB предлагает NoSQL структурированное хранилище; объекты можно хранить в хранилищах объектов IndexedDB и запрашивать их, используя запросы к БД.
  • Все операции с данными в БД производятся через атомарные транзакции операций. Которые позволяются откатиться в исходное состояние при сбое в одной из операции.

Типичная схема работы с базой

Обычная последовательность шагов при работе с IndexedDB :

  1. Открыть базу данных.
  2. Создать хранилище объектов в базе данных, над которой будут выполняться наши операции. 
  3. Запустить транзакцию и выдать запрос на выполнение какой-либо операции с базой данных, например, добавление или извлечение данных.
  4. Ждать завершения операции, “слушая” событие DOM, на которое должен быть установлен наш обработчик.
  5. Сделать что-то с результатами (которые могут быть найдены в возвращаемом по нашему запросу объекте ).

Теперь, получив общее представление, переходим к более конкретным деталям.

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

Давайте начнем делать наше приложение! Для начала создадим файл index.html, со следующем содержимым!

<!DOCTYPE html>
<html>
  <head><title>IndexedDB notes</title></head>
  <body>
    <div id="app"><h1>IndexedDB notes</h1></div>
    <script src="app.js"></script>
  </body>
</html>

Создадим рядом файл с JavaScript содержимым app.js:

let db;
let dbReq = indexedDB.open('myDB', 1);

dbReq.onupgradeneeded = (event) => {
  // Зададим переменной db ссылку на базу данных
  db = event.target.result;

  // Создадим хранилище объектов с именем notes.
  let notes = db.createObjectStore('notes', {autoIncrement: true});
}
dbReq.onsuccess = (event) => {
  db = event.target.result;
}

dbReq.onerror = (event) => {
  alert('error opening database ' + event.target.errorCode);
}

Теперь в Chrome перейдите к developer tools, перейдите на вкладку application, а затем нажмите IndexedDB на левой панели, вы увидите, что база данных была создана!

Screenshot of our IndexedDB panel with our database and object store created

Здорово! У нас есть база данных с именем myDB, и есть хранилище объектов (коллекция элементов, похожая на таблицу SQL или коллекцию в MongoDB) с именем notes. Рассмотрим подробнее что тут происходит?

В первой паре строк

let db;
let dbReq = indexedDB.open('myDB', 1);

Мы открываем версию 1 базы данных с именем myDB. Если база данных не существует она будет создана. indexedDB.open не возвращает базу данных, он возвращает запрос к базе данных, так как IndexedDB является асинхронным API. Код IndexedDB работает за кулисами, поэтому, если у нас в базе данных будет тысяч элементов, остальная часть веб-приложения не перестанет работать, ожидая завершения операции. Остальная часть кода ожидает, когда наша база данных будет готова с event listeners.

Второй параметр метода open – это версия базы данных. Это не версия API в браузере. Эта версия определяет схему базы данных – хранилище объектов  и их структуру. Если база данных еще не существует, то она создается операцией open, затем срабатывает триггер события onupgradeneeded и после этого  ваш обработчик этого события создает схему базы данных. Если же база данных уже существует, а вы указываете новый номер версии, то сразу же срабатывает триггер события onupgradeneeded, позволяя вам обновить схему базы данных в обработчике. Как это работает на практике вы увидите позже.

Далее описывается обработчик события onupgradeneeded

dbReq.onupgradeneeded = (event) => {
  db = event.target.result;
  let notes = db.createObjectStore('notes', {autoIncrement: true});
}

При первом открытие myDB ранее не существовала, поэтому она будет автоматически создана, и запустится событие onupgradeneeded. Только в этом событие можно создать хранилища объектов. Итак, во-первых, с помощью db = event.target.result, мы устанавливаем переменную db для хранения нашей базы данных. Затем мы создаем хранилище объектов с именем notes.

Хранилище объектов – это основная концепция IndexedDB. В других базах данных это «таблицы» или «коллекции». Здесь хранятся данные. В базе данных может быть множество хранилищ: одно для пользователей, другое для товаров и так далее.

В событие onsuccess срабатывает после завершения onupgradeneeded, а также срабатывает, после обновление страницы.

dbReq.onsuccess = (event) => {
  db = event.target.result;
}

Поэтому мы также запускаем db = event.target.result, чтобы получить ссылку на нашу базу данных.

Наконец, если что-то идет не так в любом запросе IndexedDB, срабатывает событие onerror, так что в нем можно обработать ошибку. Мы же просто выводим предупреждение.

dbReq.onerror = (event) => {
  alert('error opening database ' + event.target.errorCode);
}

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

Создадим некоторые данные в базе данных

Далее напишем функцию, для добавление заметки!

const addStickyNote = (db, message) => {
  // Запустим транзакцию базы данных и получите хранилище объектов Notes
  let tx = db.transaction(['notes'], 'readwrite');
  let store = tx.objectStore('notes');

  // Добаляем заметку в хранилище объектов
  let note = {text: message, timestamp: Date.now()};
  store.add(note);

  // Ожидаем завершения транзакции базы данных
  tx.oncomplete = () => {
    console.log('stored note!')
  }
  tx.onerror = (event) => {
    alert('error storing note ' + event.target.errorCode);
  }
}

Поместим набор из трех вызовов функции addStickyNote в dbReq.onsuccess, чтобы они выполнялись, когда база данных будет готова:

dbReq.onsuccess = (event) => {
  db = event.target.result;

  addStickyNote(db, 'Hello world first time!');
  addStickyNote(db, 'Hello world second time!');
  addStickyNote(db, 'Hello world third time!');
}

Теперь обновите index.html в браузере, и снова зайдите в Application> IndexedDB в Developer tools, нажмите на хранилище объектов, и давайте посмотрим наши данные!

Screenshot of our IndexedDB panel with our objects stored

Теперь у нас есть некоторые данные! И, как вы можете видеть, наши заметки в хранилище объектов хранятся в виде объектов JavaScript. Рассмотрим что же происходит в этом коде?

let tx = db.transaction(['notes'], 'readwrite');
let store = tx.objectStore('notes');

Сначала мы запускаем транзакцию в базе данных для записи данных в хранилище объектов notes, а затем извлекаем это хранилище объектов из транзакции.

let note = {text: message, timestamp: Date.now()};
store.add(note);

Далее мы создаем заметку note как объект JavaScript и сохраняем ее в хранилище объектов, используя функцию store.add.

tx.oncomplete = () => { console.log('stored note!') }
tx.onerror = (event) => {
  alert('error storing note ' + event.target.errorCode);
}

Наконец, как и в случае с запросом открытия базы данных, эта транзакция так же имеет event listeners; поэтому мы добавляем обработчики oncomplete и onerror.

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

let notes = db.createObjectStore('notes', {autoIncrement: true});

опция autoIncrement говорит, что мы хотим, чтобы у каждого объекта в хранилище был ключ, который рассчитывает. Вы также можете создать хранилища объектов со строковыми ключами, если для вас имеет больше смысла хранить и извлекать объекты по уникальному строковому ID (например, UUID).

Общий синтаксис для создания хранилища объектов:

db.createObjectStore(name[, keyOptions]);

Обратите внимание, что операция является синхронной, использование await не требуется.

  • name – это название хранилища, например "books" для книг,
  • keyOptions – это необязательный объект с одним или двумя свойствами:
    • keyPath – путь к свойству объекта, которое IndexedDB будет использовать в качестве ключа, например id.
    • autoIncrement – если true, то ключ будет формироваться автоматически для новых объектов, как постоянно увеличивающееся число.

Если при создании хранилища не указать keyOptions, то нам потребуется явно указать ключ позже, при сохранении объекта.

Например, это хранилище объектов использует свойство id как ключ:

db.createObjectStore('items', {keyPath: 'id'});

Далее удалим вызовы addStickyNote в обработчике dbReq.onsuccess и добавим функцию addStickyNote в нашем веб-приложение, так чтобы пользователь мог создать и сохранить новую заметку. Для этого добавим соответствующую разметку в index.html:

<div id="textbox">
  <textarea id="newmessage"></textarea>
  <button onclick="submitNote()">Add note</button>
</div>

И добавим эту функцию submitNote в app.js, которая будет запускаться каждый раз, когда пользователь нажмет на кнопку Add note:

const submitNote = () => {
  let message = document.getElementById('newmessage');
  addStickyNote(db, message.value);
  message.value = '';
}

Введем что-нибудь в нашу текстовую область, и при нажатие на кнопке «Add note» мы увидим, что заметка будет сохранена в IndexedDB!

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

Транзакции являются главными элементами в IndexedDB

Как вы видели в нашем последнем примере, чтобы получить доступ к нашему хранилищу объектов notes, нам пришлось использовать db.transaction, чтобы создать транзакцию, которая представляет собой набор из одного или нескольких запросов к базе данных. Все в IndexedDB происходит через транзакции. Хранение заметок, открытие базы данных и получение заметок – все это запросы, которые обрабатываются внутри транзакций.

Мы также можете запустить более одного запроса в одной транзакции, например, если вы храните много элементов в одном хранилище объектов, все запросы store.add могут быть выполнены в одной транзакции, например:

const addManyNotes = (db, messages) => {
  let tx = db.transaction(['notes'], 'readwrite');
  let store = tx.objectStore('notes');

  for (let i = 0; i < messages.length; i++) {
    // Все запросы, сделанные из store.add, будут частью одной транзакция
    store.add({text: messages[i], timestamp: Date.now()});
  }

  //  Когда все эти запросы будут завершены, завершится и транзакция
  tx.oncomplete = () => {console.log('transaction complete')};
}

Подобно тому, как запросы имеют обработчики событий onsuccess и onerror, транзакции тоже имеют обработчики событий oncomplete, onerror и onabort, которые мы можем использовать для обработки ответа на завершение транзакции.

Но что именно мы получаем, помещая каждый запрос в транзакцию? Помните, что IndexedDB – это асинхронное API, поэтому возможно одновременное выполнение множества запросов. Скажем, у нас в хранилище заметок была записка с надписью «Sloths are awesome», и нам нужно запустить два запроса, один чтобы переделать все буквы в заглавные makeAllCaps, а другой чтобы добавить восклицательный знак addExclamation. Без транзакций мы могли бы получить такую ситуацию:

Two database actions running on the same sticky note without transactions; because there's no transactions, the two actions read the sticky note from the object store before it is updated, and then run their updates, overwriting each other
исходник: https://thepracticaldev.s3.amazonaws.com/i/0kc2mtuag1kalksavh6x.png

Мы запускаем одновременно makeAllCaps и addExclamation, и они оба получают исходную заметку «Sloths are awesome». В какой момент addExclamation сохраняет заметку с восклицательным знаком. Но допустим makeAllCaps занимает больше времени и в какой момент сохраняет примечание «SLOTHS AREWOME» без восклицательного знака. Обновление makeAllCaps полностью удаляет обновление из addExclamation!

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

Two database actions running on the same sticky note with transactions; because we have transactions, the addExclamation transaction does not start untill after the makeAllCaps transaction completes. Therefore, the when the addExclamation transaction reads the sticky note, it has the modification from makeAllCaps
исходник: https://thepracticaldev.s3.amazonaws.com/i/ipqobuo50g20la4ll55u.png

Сначала выполняется транзакция makeAllCaps, но поскольку addExclamation использует то же хранилище объектов, что и makeAllCaps, она не запускается до тех пор, пока не завершится makeAllCaps. 🎉

Прежде чем мы продолжим, есть еще одна вещь, которую нужно знать, это то, что транзакции в одном и том же хранилище объектов отрабатываются по одному, только если они добавляют, изменяют или удаляют данные; другими словами, они являются транзакциями readwrite, которые создаются следующим образом:

let tx = db.transaction(['notes', 'someOtherStore'], 'readwrite');

Здесь мы запускаем транзакцию с перезаписью и говорим, что она влияет на notes и someOtherStore. Так как это readwrite, она не может запуститься, пока не будет выполнена любая другая транзакция, касающаяся любого из этих хранилищ объектов.

В то время как транзакции readwrite выполняются одна за другой, также есть транзакции доступны readonly; и вы можете запустить одновременно несколько транзакций в одним хранилище объектов, поскольку они не будут мешать друг другу! Например:

// Эти транзакции могут запускаться одновременно, даже в
// одном хранилище объектов!
let tx = db.transaction(['notes', 'someOtherStore'], 'readonly');
let tx2 = db.transaction(['notes'], 'readonly');
let tx3 = db.transaction(['someOtherStore'], 'readonly');

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

Получение одной заметки

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

// Настройка хранилища объектов и транзакции
let tx = db.transaction(['notes'], 'readonly');
let store = tx.objectStore('notes');

// Настройте запрос, чтобы получить заметку с ключом 1
let req = store.get(1);

req.onsuccess = (event) => {
  let note = event.target.result;

  if (note) {
    console.log(note);
  } else {
    console.log("note 1 not found")
  }
}

// Если мы получим ошибку, например, заметка не существует в хранилище , мы обрабатываем ошибку в обработчике onerror
req.onerror = (event) => {
  alert('error getting note 1 ' + event.target.errorCode);
}

Тут транзакция, запрашивает заметку note из хранилища notes с ключом 1, а затем либо используем полученную note в обработчике запроса onsuccess, либо обрабатываем ошибку в обработчике ошибки onerror. Обратите внимание, что если заметка не существует, onsuccess по-прежнему срабатывает, но event.target.result будет undefined.

Этот код похож на тот что мы использовали в обработчике, для открытия базы данных; мы запускаем запрос, затем получаем результат в обработчике onsuccess или обрабатываем ошибку в обработчике onerror. Но нам нужно получить не одну заметку, нам нужно получить все заметки. Получить их все, и для этого мы используем курсор.

Получение данных с помощью курсоров и отображение заметок

Извлечение всех элементов в хранилище объектов имеет такой причудливый синтаксис:

const getAndDisplayNotes = (db) => {
  let tx = db.transaction(['notes'], 'readonly');
  let store = tx.objectStore('notes');

  // Создать запрос курсора
  let req = store.openCursor();
  let allNotes = [];

  req.onsuccess = (event) => {
    // Результатом req.onsuccess в запросах openCursor является
     // IDBCursor
    let cursor = event.target.result;

    if (cursor != null) {
      // Если курсор не нулевой, мы получили элемент.
      allNotes.push(cursor.value);
      cursor.continue();
    } else {
      // Если у нас нулевой курсор, это означает, что мы получили
       // все данные, поэтому отображаем заметки, которые мы получили.
      displayNotes(allNotes);
    }
  }

  req.onerror = (event) => {
    alert('error in cursor request ' + event.target.errorCode);
  }
}

Рассмотрим этот код подробнее:

let tx = db.transaction(['notes'], 'readonly');
let store = tx.objectStore('notes');

В начале функции мы создаем транзакцию только для чтения в хранилище объектов notes. Затем мы получаем хранилище, а затем с помощью метода store.openCursor() получаем запрос. Это означает, что мы снова работаем с результатами запроса с его обработчиками onsuccess и onerror.

Внутри обработчика onsuccess результатом event.target.result является IDBCursor, содержащий ключ заметки, которую держит cursor, а также саму заметку в качестве значения курсора cursor.value.

let cursor = event.target.result;
if (cursor != null) {
  allNotes.push(cursor.value);
  cursor.continue();
} else {

В операторе if, если cursor не является нулевым, это означает, что у нас есть еще одна заметка, поэтому мы добавляем значение курсора в наш массив allNotes и продолжаем извлекать заметки, вызывая cursor.continue.

} else {
  displayNotes(allNotes);
}

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

Функция cursor.continue() выглядит как цикл while, но в нем нет циклического или управляющего потока. Так как именно мы зациклились? Эта строка даст вам подсказку:

req.onsuccess = (event) => {

Каждый раз, когда вы вызываете cursor.continue(), происходит событие и отправляет курсор со следующим элементом в обработчик onsuccess. Таким образом, в каждом последующем onsuccess мы получаем еще одну заметку до тех пор, пока не достигнем следующего onsuccess, где cursor будет равен нулю.

Теперь, чтобы отобразить эти заметки, в index.html, после div с id= textbox, добавим div c id=notes для хранения наших заметок:

<div id="notes"></div>

А в app.js добавим функцию для отображения заметок displayNotes:

const displayNotes = (notes) => {
  let listHTML = '<ul>';
  for (let i = 0; i < notes.length; i++) {
    let note = notes[i];
    listHTML += '<li>' + note.text + ' ' + 
      new Date(note.timestamp).toString() + '</li>';
  }

  document.getElementById('notes').innerHTML = listHTML;
}

Эта функция просто преобразует каждую заметку в тег <li> и отображает их в виде списка.

Теперь, когда у нас есть функция для отображения всех заметок, давайте добавим ее в несколько мест. Мы хотим увидеть все наши заметки при первом открытии приложения, поэтому при первом открытии базы данных у нас должен быть вызов getAndDisplayNotes в dbReq.onsuccess:

dbReq.onsuccess = (event) => {
  db = event.target.result;
  // Когда база данных будет готова, отобразите заметки, которые у нас уже есть!
  getAndDisplayNotes(db);
}

И когда мы добавляем заметку, мы сразу же хотим увидеть ее, поэтому в addStickyNote давайте изменим транзакцию при обратном вызове при завершении, чтобы вызвать getAndDisplayNotes:

tx.oncomplete = () => { getAndDisplayNotes(db); }

Теперь откройте страницу в Chrome и попробуйте добавить еще несколько заметок. Это должно выглядеть примерно так!

The web app, showing notes displaying in our list tag, and the textarea containing the message "These sticky notes display as you add them!"

И напоследок, давайте создадим режим, чтобы сначала видеть самые новые заметки и понять, почему это называется IndexedDB!

Индексы, помещенные в IndexedDB

У нас есть хранилище заметок, и мы храним заметки с отметками времени, поэтому мы можем получить все заметки за определенный промежуток времени (как и все заметки за последние 10 минут).

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

// Мы обновляем версию базы данных до 2, чтобы вызвать
// onupgradeneeded
let dbReq = indexedDB.open('myDatabase', 2);
dbReq.onupgradeneeded = (event) => {
  db = event.target.result;

  // Создадим хранилище объектов notes или получим его, если оно уже существует.
  let notes;
  if (!db.objectStoreNames.contains('notes')) {
    notes = db.createObjectStore('notes', {autoIncrement: true});
  } else {
    notes = dbReq.transaction.objectStore('notes');
  }

  // Если в notes еще нет индекса timestamp создадим его
  if (!notes.indexNames.contains('timestamp')) {
    notes.createIndex('timestamp', 'timestamp');
  }
}

Во-первых, мы обновляем версию нашей базы данных до 2, что указывает на то, что структура базы данных меняется, что вызывает событие onupgradeneeded.

Теперь у нас есть обновленная версия, где хранилище объектов notes уже существовало ранее, поэтому мы проверяем, есть ли уже хранилище с db.objectStoreNames:

if (!db.objectStoreNames.contains('notes')) {

и если это хранилище объектов уже существует, мы получаем его с помощью dbReq.transaction.objectStore:

notes = dbReq.transaction.objectStore('notes');

Наконец, мы создаем индекс с помощью createIndex:

notes.createIndex('timestamp', 'timestamp');

Первый параметр – это имя нашего индекса, а второй – это keyPath индекса (имя поля). Индекс фактически сам является хранилищем объектов, поэтому все элементы в индексе имеют ключ. Таким образом, если вы указали индекс keyPath timestamp, тогда ключом будет отметка времени каждого объекта в хранилище.

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

notes.createIndex('title', 'title', {unique: true});

Чтобы увидеть наш новый индекс, как только вы обновите onupgradeneeded, обновите index.html в Chrome (вам может понадобиться обновить БД), снова зайдите в Developer Tools > Application > IndexedDB, и вы сможете увидеть новый индекс отметки времени в вашем хранилище объектов заметок:

Screenshot of the IndexedDB panel in developer tools, looking at an index in IndexedDB. The timestamps of our sticky notes serve as keys. Indices in IndexedDB are listed in the panel under their object store.

Как видите, notes теперь перечислены c временными метками в качестве своих первичных ключей. И фактически, как хранилище объектов, индекс имеет те же методы get и openCursor, что и обычное хранилище объектов. Например, мы могли бы запросить первую заметку в этом списке с помощью вызова:

tx.objectStore('notes').index('timestamp').get(1533144673015);

Теперь, когда у нас есть индексы, давайте изменим порядок отображения заметок. Сначала в app.js добавим глобальную переменную bool:

let reverseOrder = false;

Затем в getAndDisplayNotes нам просто нужно обновить наш запрос, чтобы мы использовали наш индекс метки времени и чтобы мы выбирали, в каком направлении мы читаем заметки.

let tx = db.transaction(['notes'], 'readonly');
let store = tx.objectStore('notes');

// Получим индекс заметок, чтобы запустить наш запрос курсора;
// результаты будут упорядочены по метке времени
let index = store.index('timestamp');

// Создайте запрос open_Cursor по индексу, а не по основному
// хранилище объектов.
let req = index.openCursor(null, reverseOrder ? 'prev' : 'next');

В store.index() мы получаем индекс с именем, которое мы запрашиваем, так же, как мы получаем хранилище объектов из транзакции. Теперь мы можем определить запрос курсора для этого индекса, чтобы вернуть наши заметки, упорядоченные по метке времени.

index.openCursor имеет два необязательных параметра. Первый, если он не нулевой, позволяет нам указать диапазон элементов, которые мы хотим получить. Например, если бы мы хотели только заметки за прошедший час, мы могли бы открыть наш курсор следующим образом:

let anHourAgoInMilliseconds = Date.now() - 60 * 60 * 1000;

// IDBKeyRange - глобальная переменная для определения диапазонов для запроса
let keyRange = IDBKeyRange.lowerBound(anHourAgoInMilliseconds);
let req = index.openCursor(keyRange, 'next');

Второй параметр – это порядок, в котором мы хотим получить элементы, который может быть «prev» или «next», поэтому мы указываем наше направление, передавая reverseOrder ? ‘prev’ : ‘next’ .

Наконец, давайте посмотрим на это в действии; в index.html добавьте еще одну функцию. Она будет изменять порядок отображаемых нами заметок:

flipNoteOrder = (notes) => {
  reverseOrder = !reverseOrder;
  getAndDisplayNotes(db);
}

И чтобы использовать эту функцию flipNoteOrder из нашего пользовательского интерфейса, в index.html добавим еще одну кнопку для переключения порядка заметок.

<button onclick="flipNoteOrder()">Flip note order</button>

Обновим Chrome.

Our web app, demonstrating that the flip note order button now works. Textarea contains the message "Flipped note order"

Здорово! Теперь мы можем изменить порядок, в котором видим заметки!

Удаление выбранной записи

Для того что бы у нас появилась возможность удалять выбранную запись добавим кнопку удаления в каждую запись. Для этого немного поменяем функцию displayNotes

const displayNotes = (notes) => {
  let listHTML = '<ul>';
  for (let i = 0; i < notes.length; i++) {
    let note = notes[i];
    listHTML += '<li><button onclick="deleteNote(event)" data-id="' + note.timestamp + '">X</button>';
    listHTML += note.text + ' ' + 
      new Date(note.timestamp).toString() + '</li>';
  }

  document.getElementById('notes').innerHTML = listHTML;
}

Далее добавим функцию удаления deleteNote

const deleteNote = (event) => {
  // получаем признак выбранной записи
  const valueTimestamp = parseInt(event.target.getAttribute('data-id'));

  // открываем транзакцию чтения/записи БД, готовую к удалению данных
  const tx = db.transaction(['notes'], 'readwrite');
  // описываем обработчики на завершение транзакции
  tx.oncomplete = (event) => {
    console.log('Transaction completed.')
    getAndDisplayNotes(db);
  };
  tx.onerror = function(event) {
    alert('error in cursor request ' + event.target.errorCode);
  };

  // создаем хранилище объектов по транзакции
  const store = tx.objectStore('notes');
  const index = store.index("timestamp");
  
  // получаем ключ записи
  const req = index.getKey(valueTimestamp)
  req.onsuccess = (event) => {  
    const key = req.result;
    // выполняем запрос на удаление указанной записи из хранилища объектов
    let deleteRequest = store.delete(key);
    deleteRequest.onsuccess = (event) => {
      // обрабатываем успех нашего запроса на удаление
      console.log('Delete request successful')
    };
    
  }

В ней мы вначале получаем через атрибут data-id признак выбранной записи.

const valueTimestamp = parseInt(event.target.getAttribute('data-id'));

Далее открываем новую readwrite транзакцию и описываем ее обработчики.

Далее выбираем хранилище объектов store и по выбранному индексу index получаем запись по выбранному valueTimestamp. Далее получаем ключ записи getKey и по полученному ключу удаляем запись store.delete(key)

Заключение

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

Была ли вам полезна эта статья?
[35 / 4.8]

Spread the love
Подписаться
Уведомление о
guest
6 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Roman
Roman
4 лет назад

Хорошая статья. А каким образом происходит аутентификация? Сколько может быть экземпляров?

edteam
Администратор
4 лет назад
Reply to  Roman

IndexDB это локальная БД в браузере (аналог localStorage), к нему имеет доступ только один текущий пользователь соотвественно нет и не может быть аутентификации и может быть только один экземпляр.

Анонимно
Анонимно
4 лет назад
Reply to  edteam

Стоп. Я имею ввиду каким образом можно разграничить доступ к базе из приложения в браузере?

P. S. было бы неплохо сделать уведомления об ответах в комментариях, либо прикрутить сторонний сервис. Вот я вспомнил, что задавал вопрос и как я узнаю, что кто-то ответил 🙂

edteam
Администратор
4 лет назад

Я возможно ошибаюсь но поддержки такого функционала в API IndexedDB нет.
PS отправка уведомления хорошая идея, но пока не знаю как это сделать.

Roman
Roman
4 лет назад

Disqus, Hypercomments, да мало готовых решений для комментов. Просто, если уж решили дать возможность оставлять комментарии, то это должно быть удобно. Хотя, в 2020 кто их будет оставлять, кроме фриков 😜

Анонимно
Анонимно
3 лет назад

Эта статья совершила чудо! Еще один Восхищенный чайник… Автору всех благ!