Разработка сайтов в Ирмино, ЛНР. Как хранить неограниченные данные в браузере с помощью IndexedDB

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

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

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

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

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

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

автономные данные для последующей синхронизации, возможно, в районах с ограниченным подключением

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

кэширование ресурсов для повышения производительности

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

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

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

Кэш API

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

ИндекседБД

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

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

Введение в IndexedDB

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

Данные о поддержке функции indexeddb в основных браузерах с сайта caniuse.com.

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

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

хранилище объектов: хранилище имен/значений для связанных элементов данных, концептуально подобное коллекциям в MongoDB или таблицам в базах данных SQL. Ваша notebookбаза данных может иметь noteхранилище объектов для хранения записей, каждая из которых имеет идентификатор, заголовок, тело, дату и массив тегов.

key: уникальное имя, используемое для ссылки на каждую запись (значение) в хранилище объектов. Он может быть сгенерирован автоматически или установлен на значение в записи. Идентификатор идеально подходит для использования в качестве noteключа магазина.

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

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

схема: определение хранилищ объектов, ключей и индексов в базе данных.

version: номер версии (целое число), назначенный схеме, чтобы база данных могла обновляться при необходимости.

операция: действие базы данных, такое как создание, чтение, обновление или удаление (CRUD) записи.

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

курсор: способ перебирать множество записей без необходимости загружать все в память сразу.

асинхронное выполнение: операции IndexedDB выполняются асинхронно. Когда запускается операция, например извлечение всех заметок, эта активность выполняется в фоновом режиме, а другой код JavaScript продолжает выполняться. Функция вызывается, когда результаты готовы.

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

{

id: 1,

title: «My first note»,

body: «A note about something»,

date: ,

tags: [«#first», «#note»]

}

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

Отладка IndexDB DevTools

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

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

Панель приложений DevTools

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

Хранилище DevTools IndexedDB

(В Firefox есть аналогичная панель под названием «Хранилище «.)

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

Проверить наличие поддержки IndexedDB

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

if ('indexedDB’ in window) {

// indexedDB supported

}

else {

console.log ('IndexedDB is not supported.') ;

}

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

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

StorageManager API на основе 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 0: {

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 () индекса для хранилища объектов:

dateIdxв dateкаждой записи

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

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

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

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

dbOpen.onerror = err => {

console.error (`indexedDB error: ${ err.errorCode }`) ;

};

Наконец, onsuccessобработчик запускается, когда соединение установлено. Соединение (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) ;

};

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

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

Создайте объект транзакции, который определяет одно хранилище объектов (или массив хранилищ объектов) и тип доступа «readonly» (только выборка данных — по умолчанию) или «readwrite» (обновление данных).

Используйте objectStore () для получения хранилища объектов (в рамках транзакции).

Запустите любое количество методов 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 () добавит запись или заменит существующую при передаче ключа. Следующий код обновляет примечание с помощью idof 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

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

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 0: {

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 0: {

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

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

Делитесь нашими материалами с друзьями!

 

 

Заказать разработку сайта