IndexedDB является стандартом хранения структурированных данных на стороне клиента. Эта статья – гайд по применению данной технологии.
Это хранилище ключей/значений (база данных noSQL), которое считается окончательным решением для хранения данных в браузерах. IndexedDB является асинхронным API. Это означает, что выполнение приоритетных операций не будет блокировать поток пользовательского интерфейса. IndexedDB может хранить неопределенный объем данных, который зависит от пользователя.
Поддерживается на всех современных браузерах.
Он поддерживает транзакции, управление версиями и обеспечивает хорошую производительность.
Внутри браузера мы также можем использовать:
- Cookies, которые могут содержать небольшое количество строк.
- DOM-хранилище (или Web-хранилище) – термин, который обычно определяет localStorage и sessionStorage два хранилища типа ключ/значение. sessionStorage не сохраняет данные, которые очищаются после завершения сеанса, а localStorage сохраняет.
Локальное/сеансовое хранилище имеет недостаток, заключающийся в ограничении доступного пространства: от 2 Мб до 10 Мб места на сайт.
В прошлом был Web SQL – оболочка для SQLite. Но теперь Web SQL устарел и не поддерживается в некоторых современных браузерах. Он никогда не был общепризнанным стандартом, поэтому его не следует использовать, однако 83% пользователей все же имеют эту технологию на своих устройствах в соответствии с Can I Use.
Хоть и технически можно создать несколько баз данных для сайта, обычно создается одна. Внутри этой базы данных можно создать несколько хранилищ. База данных является частной для домена, поэтому один сайт не может получить доступ к хранилищам IndexedDB другого.
Каждое хранилище содержит множество элементов, например:
- строки
- числа
- объекты
- массивы
- даты
К примеру, может существовать два хранилища: одно будет содержать посты, другое – комментарии.
Хранилище содержит ряд элементов, имеющих уникальный ключ, который представляет способ идентификации объекта.
Эти хранилища можно изменять с помощью транзакций, выполняя операции добавления, изменения и удаления, а также проходя по содержащимся в них элементам.
С появлением Promises в ES2015 и последующим переходом API на использование promises, API IndexedDB кажется немного устаревшим.
Создание базы данных IndexedDB
Импортируем библиотеку idb
, используя: yarn add idb
А затем подключим её к странице, используя Webpack, Browserify или любую другую систему сборки, или просто: <script src="./node_modules/idb/lib/idb.js"></ script>
.
Перед использованием API IndexedDB всегда проверяйте наличие поддержки в браузере. Вы никогда не знаете, какой браузер использует пользователь:
(() => { 'use strict' if (!('indexedDB' in window)) { console.warn('IndexedDB not supported') return } //...IndexedDB code })()
Как создать базу данных
Используем idb.open()
.
const name = 'mydbname' const version = 1 //versions start at 1 idb.open(name, version, upgradeDb => {})
Первые два параметра очевидны. Третий параметр, который является необязательным, – это обратный вызов, вызываемый только в том случае, если номер версии выше текущей установленной версии базы данных. В теле функции обратного вызова можно обновить структуру (хранилища и индексы) БД.
Мы используем upgradeDB для обратного вызова, чтобы определить, что пришло время обновить базу данных, если это необходимо.
Создание хранилища объектов
Хранилище объектов создается или обновляется в обратном вызове с помощью: db.createObjectStore('storeName', options)
.
const dbPromise = idb.open('mydb', 1, (upgradeDB) => { upgradeDB.createObjectStore('store1') }) .then(db => console.log('success'))
Если установлена предыдущая версия, обратный вызов позволяет выполнить миграцию:
const dbPromise = idb.open('keyval-store', 3, (upgradeDB) => { switch (upgradeDB.oldVersion) { case 0: // no db created before // a store introduced in version 1 upgradeDB.createObjectStore('store1') case 1: // a new store in version 2 upgradeDB.createObjectStore('store2', { keyPath: 'name' }) } }) .then(db => console.log('success'))
createObjectStore()
, как вы можете видеть в case 1
, принимает второй параметр, который указывает индексный ключ базы данных. Это очень полезно при хранении объектов: вызовы put()
не требуют второго параметра, но могут просто принимать значение (объект), и ключ будет сопоставлен со свойством объекта с таким именем.
Индекс дает вам возможность получить значение по этому определенному ключу, и он должен быть уникальным (каждый элемент должен иметь уникальный ключ).
Ключ можно настроить на автоматическое увеличение, поэтому вам не нужно отслеживать его на клиентской стороне. Если вы не укажете ключ, IndexedDB создаст его “прозрачно” для нас:
upgradeDb.createObjectStore('notes', { autoIncrement: true })
Также можно указать конкретное поле значения объекта для автоматического приращения:
upgradeDb.createObjectStore('notes', { keyPath: 'id', autoIncrement: true })
Как правило, используется автоматическое увеличение, если значения уже содержат уникальный ключ (например, адрес электронной почты пользователей).
Индексы
Использование индексов – это способ извлечения данных из хранилища объектов. Индекс определяется вместе с созданием базы данных в idb.open()
следующим образом:
const dbPromise = idb.open('dogsdb', 1, (upgradeDB) => { const dogs = upgradeDB.createObjectStore('dogs') dogs.createIndex('name', 'name', { unique: false }) })
unique
определяет, должно ли значение индекса быть уникальным, и никакие повторяющиеся значения не могут быть добавлены.
Вы можете получить доступ к уже созданному хранилищу объектов с помощью метода upgradeDb.transaction.objectStore()
.
const dbPromise = idb.open('dogsdb', 1, (upgradeDB) => { const dogs = upgradeDB.transaction.objectStore('dogs') dogs.createIndex('name', 'name', { unique: false }) })
Проверка на существование хранилища
Можно проверить, существует ли хранилище объектов, вызвав метод objectStoreNames()
:
if (!upgradeDb.objectStoreNames.contains('store3')) { upgradeDb.createObjectStore('store3') }
Удаление
Удаление базы данных
idb.delete('mydb').then(() => console.log('done'))
Удаление хранилища объектов
Хранилище объектов может быть удалено в обратном вызове только при открытии БД, и этот обратный вызов выполняется только в том случае, если указывается версия, превышающая установленную в настоящий момент:
const dbPromise = idb.open('dogsdb', 2, (upgradeDB) => { upgradeDB.deleteObjectStore('old_store') })
Чтобы удалить данные в хранилище объектов, используйте эту транзакцию:
const key = 232 dbPromise.then((db) => { const tx = db.transaction('store', 'readwrite') const store = tx.objectStore('store') store.delete(key) return tx.complete }) .then(() => { console.log('Item deleted') })
Добавление элемента в базу данных
Вы можете использовать метод put()
хранилища объектов, но сначала необходимо получить ссылку на него, которую можно достать из upgradeDB.createObjectStore()
.
При использовании put значение является первым аргументом, а ключ – вторым. Это сделано специально, потому что если вы задаете keyPath
при создании хранилища объектов, вам не нужно вводить имя ключа в каждом запросе put. Можно просто написать значение.
Этот код заполняет store0
, как только мы создаем хранилище:
idb.open('mydb', 1, (upgradeDB) => { keyValStore = upgradeDB.createObjectStore('store0') keyValStore.put('Hello world!', 'Hello') })
Чтобы позже добавить элементы, необходимо создать транзакцию. Это обеспечивает целостность базы данных (в случае сбоя операции выполняется откат всех операций в транзакции, и состояние возвращается в прежнее).
Для этого используйте ссылку на объект dbPromise
, полученный при вызове idb.open()
, и запустите:
dbPromise.then((db) => { const val = 'hey!' const key = 'Hello again' const tx = db.transaction('store1', 'readwrite') tx.objectStore('store1').put(val, key) return tx.complete }) .then(() => { console.log('Transaction complete') }) .catch(() => { console.log('Transaction failed') })
API IndexedDB также предлагает метод add()
, но поскольку put()
позволяет как добавлять, так и обновлять элементы, проще просто использовать его.
Получение элементов из хранилища
Получение определённого элемента из хранилища с использованием get()
:
dbPromise.then(db => db.transaction('objs') .objectStore('objs') .get(123456)) .then(obj => console.log(obj))
Получение всех элементов с использованием getAll()
:
dbPromise.then(db => db.transaction('store1') .objectStore('store1') .getAll()) .then(objects => console.log(objects))
Итерация по всем элементам с помощью курсора через openCursor()
:
dbPromise.then((db) => { const tx = db.transaction('store', 'readonly') const store = tx.objectStore('store') return store.openCursor() }) .then(function logItems(cursor) { if (!cursor) { return } console.log('cursor is at: ', cursor.key) for (const field in cursor.value) { console.log(cursor.value[field]) } return cursor.continue().then(logItems) }) .then(() => { console.log('done!') })
Итерация по подмножеству элементов с использованием границ и курсоров:
const searchItems = (lower, upper) => { if (lower === '' && upper === '') { return } let range if (lower !== '' && upper !== '') { range = IDBKeyRange.bound(lower, upper) } else if (lower === '') { range = IDBKeyRange.upperBound(upper) } else { range = IDBKeyRange.lowerBound(lower) } dbPromise.then((db) => { const tx = db.transaction(['dogs'], 'readonly') const store = tx.objectStore('dogs') const index = store.index('age') return index.openCursor(range) }) .then(function showRange(cursor) { if (!cursor) { return } console.log('cursor is at:', cursor.key) for (const field in cursor.value) { console.log(cursor.value[field]) } return cursor.continue().then(showRange) }) .then(() => { console.log('done!') }) } searchDogsBetweenAges(3, 10)
Комментарии