Как хранить неограниченное количество данных в браузере с IndexedDB

Как хранить неограниченное количество данных в браузере с IndexedDB Без рубрики

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

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

Зачем хранить данные в браузере?

Практично хранить большинство данных, созданных пользователями, на сервере, но есть исключения:

  • зависящие от устройства настройки, такие как параметры пользовательского интерфейса, светлый / темный режим и т. д.
  • кратковременные данные, такие как захват нескольких фотографий перед выбором одной для загрузки
  • автономные данные для последующей синхронизации, возможно, в областях с ограниченными возможностями подключения
  • прогрессивные веб-приложения (PWA), которые работают в автономном режиме по практическим соображениям или соображениям конфиденциальности
  • кэширование ресурсов для повышения производительности

Могут подойти три основных API браузера:

1. Веб-хранилище

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

2. Cache API

Хранилище для пар объектов HTTP-запроса и ответа. API обычно используется сервисными работниками для кэширования сетевых ответов, поэтому прогрессивное веб-приложение может работать быстрее и работать в автономном режиме. Браузеры различаются, но Safari на iOS выделяет 50 МБ.

3. IndexedDB

Клиентская база данных NoSQL, которая может хранить данные, файлы и большие двоичные объекты. Браузеры различаются, но для каждого домена должен быть доступен не менее 1 ГБ, и он может достигать 60% оставшегося дискового пространства.

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

IndexedDB Введение

IndexedDB впервые появился в браузерах в 2011 году. API стал стандартом W3C в январе 2015 года и был заменен API 2.0 в январе 2018 года. API 3.0 находится в разработке. Таким образом, IndexedDB имеет хорошую поддержку браузера и доступен в стандартных скриптах и Web Workers. Разработчики-мазохисты могут попробовать это даже в IE10.

В этой статье упоминаются следующие термины базы данных и IndexedDB

В этой статье упоминаются следующие термины базы данных и IndexedDB:

  • база данных: магазин верхнего уровня. Может быть создано любое количество баз данных IndexedDB, хотя большинство приложений определяют одну. Доступ к базе данных ограничен страницами в одном домене; исключены даже поддомены. Пример: вы можете создать notebookбазу данных для своего приложения для создания заметок.
  • Хранилище объектов: хранилище имен / значений для связанных элементов данных, концептуально аналогично коллекциям в MongoDB или таблицам в базах данных SQL. В вашей notebookбазе данных может быть noteхранилище объектов для хранения записей, каждая с идентификатором, заголовком, телом, датой и массивом тегов.
  • ключ: уникальное имя, используемое для ссылки на каждую запись (значение) в хранилище объектов. Он может быть автоматически сгенерирован или установлен в значение в записи. Идентификатор идеально подходит для использования в качестве noteключа магазина.
  • autoIncrement: значение определенного ключа может автоматически увеличиваться каждый раз, когда запись добавляется в хранилище.
  • index: сообщает базе данных, как организовать данные в хранилище объектов. Для поиска с использованием этого элемента данных в качестве критерия необходимо создать индекс. Например, заметки dateможно проиндексировать в хронологическом порядке, чтобы можно было найти заметки в течение определенного периода.
  • схема: определение хранилищ объектов, ключей и индексов в базе данных.
  • версия: номер версии (целое число), присвоенный схеме, чтобы при необходимости можно было обновить базу данных.
  • операция: действие базы данных, такое как создание, чтение, обновление или удаление (CRUD) записи.
  • транзакция: оболочка для одной или нескольких операций, которая гарантирует целостность данных. База данных либо выполнит все операции в транзакции, либо ни одну из них: некоторые из них не будут выполняться, а другие не будут выполняться.
  • курсор: способ перебирать множество записей без необходимости загружать все сразу в память.
  • асинхронное выполнение: операции IndexedDB выполняются асинхронно. Когда операция запускается, например получение всех заметок, это действие выполняется в фоновом режиме, а другой код JavaScript продолжает выполняться. Функция вызывается, когда результаты готовы.

В приведенных ниже примерах хранятся записи заметок — например, следующие — в noteхранилище объектов в базе данных с именем notebook:

{
  id: 1,
  title: "My first note",
  body: "A note about something",
  date: <Date() object>,
  tags: ["#first", "#note"]
}

API IndexedDB немного устарел и полагается на события и обратные вызовы. Он напрямую не поддерживает синтаксическую привлекательность ES6, такую ​​как Promises и async/ await. Доступны библиотеки-оболочки, такие как idb, но этот учебник идет до самого конца.

IndexDB DevTools Отладка

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

Во всех браузерах на базе Chrome есть вкладка » Приложение «, на которой вы можете проверить объем хранилища, искусственно ограничить его емкость и стереть все данные:

Во всех браузерах на базе Chrome есть вкладка

Запись IndexedDB в дереве хранилища позволяет вам проверять, обновлять и удалять хранилища объектов, индексы и отдельные записи:

Запись IndexedDB в дереве хранилища позволяет вам проверять

(В Firefox есть аналогичная панель с названием Storage.)

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

Проверьте поддержку IndexedDB

window.indexedDBоценивает, trueподдерживает ли браузер IndexedDB:

if ('indexedDB' in window) {

  // indexedDB supported

}
else {
  console.log('IndexedDB is not supported.');
}

Редко можно встретить браузер без поддержки IndexedDB. Приложение может вернуться к более медленному хранилищу на базе сервера, но большинство из них предложит пользователю обновить приложение десятилетней давности!

Читайте также:  C ++ shared_ptr

Проверить оставшееся место для хранения

API StorageManager на основе Promise предоставляет оценку оставшегося места для текущего домена:

(async () => {

  if (!navigator.storage) return;

  const
    required = 10, // 10 MB required
    estimate = await navigator.storage.estimate(),

    // calculate remaining storage in MB
    available = Math.floor((estimate.quota - estimate.usage) / 1024 / 1024);

  if (available >= required) {
    console.log('Storage is available');
    // ...call functions to initialize IndexedDB
  }

})();

Этот API не поддерживается в IE или Safari (пока), поэтому будьте осторожны, если navigator.storageне удается вернуть ложное значение.

Свободное пространство, приближающееся к 1000 мегабайт, обычно доступно, если на диске устройства не заканчивается. Safari может предложить пользователю согласиться на большее количество ресурсов, хотя PWA в любом случае выделяется 1 ГБ.

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

  • удалить старые временные данные
  • попросить пользователя удалить ненужные записи, или
  • передавать на сервер менее используемую информацию (для действительно неограниченного хранения!)

Откройте соединение с IndexedDB

Соединение IndexedDB инициализируется с помощью indexedDB.open(). Пройдено:

  • имя базы данных и
  • необязательное целое число версии
const dbOpen = indexedDB.open('notebook', 1);

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

Когда эта база данных встречается впервые, должны быть созданы все хранилища объектов и индексы. onupgradeneededФункция обработчик события получает объект соединения с базой данных ( dbOpen.result) и выполняет такие методы, как по createObjectStore()мере необходимости:

dbOpen.onupgradeneeded = event => {

  console.log(`upgrading database from ${ event.oldVersion } to ${ event.newVersion }...`);

  const db = dbOpen.result;

  switch( event.oldVersion ) {

    case : {
      const note = db.createObjectStore(
        'note',
        { keyPath: 'id', autoIncrement: true }
      );

      note.createIndex('dateIdx', 'date', { unique: false });
      note.createIndex('tagsIdx', 'tags', { unique: false, multiEntry: true });
    }

  }

};

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

createIndex() Метод определяет два новых индексов для магазина объекта:

  1. dateIdxна dateкаждой записи
  2. tagsIdxв tagsмассиве в каждой записи ( multiEntryиндекс, расширяющий отдельные элементы массива в индекс)

Есть вероятность, что у нас могут быть две заметки с одинаковыми датами или тегами, поэтому uniqueустановлено значение false.

Примечание: этот оператор switch кажется немного странным и ненужным, но он станет полезным при обновлении схемы.

onerror Обработчик сообщает об ошибках подключения базы данных:

dbOpen.onerror = err => {
  console.error(`indexedDB error: ${ err.errorCode }`);
};

Наконец, onsuccessпосле установления соединения запускается обработчик. Connection ( dbOpen.result) используется для всех дальнейших операций с базой данных, поэтому его можно определить как глобальную переменную или передать другим функциям (например, как main()показано ниже):

dbOpen.onsuccess = () => {

  const db = dbOpen.result;

  // use IndexedDB connection throughout application
  // perhaps by passing it to another function, e.g.
  // main( db );

};

Создать запись в хранилище объектов

Для добавления записей в магазин используется следующий процесс:

  1. Создайте объект транзакции, который определяет одно хранилище объектов (или массив хранилищ объектов) и тип доступа «readonly»(только выборка данных — по умолчанию) или «readwrite»(обновление данных).
  2. Используется objectStore()для получения хранилища объектов (в рамках транзакции).
  3. Запустить любое количество add()(или put()) методов и отправить данные в магазин:
const

  // lock store for writing
  writeTransaction = db.transaction('note', 'readwrite'),

  // get note object store
  note = writeTransaction.objectStore('note'),

  // insert a new record
  insert = note.add({
    title: 'Note title',
    body: 'My new note',
    date: new Date(),
    tags: [ '#demo', '#note' ]
  });

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

Функции обработки ошибок и успеха определяют результат:

insert.onerror = () => {
  console.log('note insert failure:', insert.error);
};

insert.onsuccess = () => {
  // show value of object store's key
  console.log('note insert success:', insert.result);
};

Если какая-либо функция не определена, она перейдет к транзакции, а затем к обработчикам базы данных (которые можно остановить с помощью event.stopPropagation()).

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

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

Обновление записи в хранилище объектов

add()Метод потерпит неудачу при попытке вставить запись с существующим ключом. put()добавит запись или заменит существующую при передаче ключа. Следующий код обновляет записку с idиз 1(или вставляет его в случае необходимости):

const

  // lock store for writing
  updateTransaction = db.transaction('note', 'readwrite'),

  // get note object store
  note = updateTransaction.objectStore('note'),

  // add new record
  update = note.put({
    id: 1,
    title: 'New title',
    body: 'My updated note',
    date: new Date(),
    tags: [ '#updated', '#note' ]
  });

// add update.onsuccess and update.onerror handler functions...

Примечание: если магазин объект был не keyPathопределен, который реферировано id, как add()и put()методы обеспечивают второй параметр, чтобы указать ключ. Например:

update = note.put(
  {
    title: 'New title',
    body: 'My updated note',
    date: new Date(),
    tags: [ '#updated', '#note' ]
  },
  1 // update the record with the key of 1
);

Чтение записей из хранилища объектов по ключу

Отдельную запись можно получить, передав ее ключ .get()методу. onsuccessОбработчик получает данные или undefinedкогда совпадение не найдено:

const

  // new transaction
  reqTransaction = db.transaction('note', 'readonly'),

  // get note object store
  note = reqTransaction.objectStore('note'),

  // get a single record by id
  request = note.get(1);

request.onsuccess = () => {
  // returns single object with id of 1
  console.log('note request:', request.result);
};

request.onerror = () => {
  console.log('note failure:', request.error);
};

Аналогичный getAll()метод возвращает массив совпадающих записей.

Читайте также:  Будущее Интернета вещей для телекоммуникаций

Оба метода принимают аргумент KeyRange для дальнейшего уточнения поиска. Например, IDBKeyRange.bound(5, 10)возвращает все записи idот 5 до 10 включительно:

request = note.getAll( IDBKeyRange.bound(5, 10) );

Ключевые варианты диапазона включают:

  • IDBKeyRange.lowerBound(X): ключи больше или равны X
  • IDBKeyRange.upperBound(X): ключи меньше или равны Y
  • IDBKeyRange.bound(X,Y): ключи между Xи Yвключительно
  • IDBKeyRange.only(X): соответствие одного ключа X

У методов lower, upper и bound есть необязательный эксклюзивный флаг. Например:

  • IDBKeyRange.lowerBound(5, true): ключи больше, чем 5(но не 5сам)
  • IDBKeyRange.bound(5, 10, true, false): ключи больше 5(но не 5сами) и меньше или равны10

Другие методы включают:

  • .getKey(query): вернуть соответствующий ключ (а не значение, присвоенное этому ключу)
  • .getAllKeys(query): вернуть массив совпадающих ключей
  • .count(query): вернуть количество совпадающих записей

Чтение записей из хранилища объектов по индексированному значению

Индекс должен быть определен для поиска полей в записи. Например, чтобы найти все заметки, сделанные в течение 2021 года, необходимо выполнить поиск по dateIdxиндексу:

const

  // new transaction
  indexTransaction = db.transaction('note', 'readonly'),

  // get note object store
  note = indexTransaction.objectStore('note'),

  // get date index
  dateIdx = note.index('dateIdx'),

  // get matching records
  request = dateIdx.getAll(
    IDBKeyRange.bound(
      new Date('2021-01-01'), new Date('2022-01-01')
    )
  );

// get results
request.onsuccess = () => {
  console.log('note request:', request.result);
};

Чтение записей из хранилища объектов с помощью курсоров

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

В этом примере выполняется «#note«поиск всех записей, содержащих тег, в индексированном tagsмассиве. Вместо того чтобы использовать.getAll(), он запускает .openCursor()метод, который передается диапазон и дополнительно строку направления ( «next», «nextunique», «prev», или «preunique»):

const

  // new transaction
  cursorTransaction = db.transaction('note', 'readonly'),

  // get note object store
  note = cursorTransaction.objectStore('note'),

  // get date index
  tagsIdx = note.index('tagsIdx'),

  // get a single record
  request = tagsIdx.openCursor('#note');

request.onsuccess = () => {

  const cursor = request.result;

  if (cursor) {

    console.log(cursor.key, cursor.value);
    cursor.continue();

  }

};

onsuccess Обработчик возвращает результат в месте расположения курсора, обрабатывает его и запускает .continue()метод, чтобы перейти к следующей позиции в наборе данных. .advance(N)Способ также может быть использован для перемещения вперед Nзаписей.

При желании запись в текущей позиции курсора может быть:

  • обновлено с помощью cursor.update(data), или
  • удалено с помощью cursor.delete()

Удаление записей из хранилища объектов

Помимо удаления записи в текущей точке курсора, .delete()методу хранилища объектов можно передать значение ключа или KeyRange. Например:

const

  // lock store for writing
  deleteTransaction = db.transaction('note', 'readwrite'),

  // get note object store
  note = deleteTransaction.objectStore('note'),

  // delete record with an id of 5
  remove = note.delete(5);

remove.onsuccess = () => {
  console.log('note deleted');
};

Более радикальный вариант — .clear()стирать каждую запись из хранилища объектов.

Обновить схему базы данных

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

При onupgradeneededопределении версии 1 схемы записной книжки выполнялась функция:

const dbOpen = indexedDB.open('notebook', 1);

dbOpen.onupgradeneeded = event => {

  console.log(`upgrading database from ${ event.oldVersion } to ${ event.newVersion }...`);

  const db = dbOpen.result;

  switch( event.oldVersion ) {

    case : {
      const note = db.createObjectStore(
        'note',
        { keyPath: 'id', autoIncrement: true }
      );

      note.createIndex('dateIdx', 'date', { unique: false });
      note.createIndex('tagsIdx', 'tags', { unique: false, multiEntry: true });
    }

  }

};

Предположим, что для заголовков заметок требовался другой указатель indexedDB.open()Версия должна изменяться от 1до 2:

const dbOpen = indexedDB.open('notebook', 2);

Индекс заголовка можно добавить в новый case 1блок onupgradeneededобработчика switch():

dbOpen.onupgradeneeded = event => {

  console.log(`upgrading database from ${ event.oldVersion } to ${ event.newVersion }...`);

  const db = dbOpen.result;

  switch( event.oldVersion ) {

    case : {
      const note = db.createObjectStore(
        'note',
        { keyPath: 'id', autoIncrement: true }
      );

      note.createIndex('dateIdx', 'date', { unique: false });
      note.createIndex('tagsIdx', 'tags', { unique: false, multiEntry: true });
    }

    case 1: {
      const note = dbOpen.transaction.objectStore('note');
      note.createIndex('titleIdx', 'title', { unique: false });
    }

  }

};

Обратите внимание на пропуск обычного breakв конце каждого caseблока. Когда кто-то обращается к приложению в первый раз, case 0блок запускается, а затем он переходит во case 1все последующие блоки. Любой, у кого уже есть версия 1, запустит обновления, начиная с case 1блока.

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

  • .createIndex()
  • .deleteIndex()
  • .createObjectStore()
  • .deleteObjectStore()
  • .deleteDatabase()

Таким образом, все пользователи будут использовать одну и ту же версию базы данных… если только у них не запущено приложение на двух или более вкладках!

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

// version change handler
db.onversionchange = () => {

  db.close();
  alert('The IndexedDB database has been upgraded.\nPlease reload the page...');
  location.reload();

};

Низкий уровень IndexedDB

IndexedDB — один из наиболее сложных API-интерфейсов браузера, и вы пропустите использование Promises и async/ await. Если требования вашего приложения не просты, вам нужно создать собственный уровень абстракции IndexedDB или использовать предварительно созданный вариант, например idb.

Оцените статью
bestprogrammer.ru
Добавить комментарий