Для создания комфортной офлайн-среды вашему PWA необходимо управление хранилищем. В главе, посвящённой кэшированию , вы узнали, что кэширование — один из вариантов сохранения данных на устройстве. В этой главе мы покажем вам, как управлять офлайн-данными, включая сохранение данных, ограничения и доступные инструменты.
Хранилище
Хранилище — это не только файлы и ресурсы, но и другие типы данных. Во всех браузерах, поддерживающих PWA, доступны следующие API для хранения данных на устройстве:
- IndexedDB : вариант хранения объектов NoSQL для структурированных данных и двоичных данных.
- WebStorage: способ хранения строковых пар «ключ/значение» с использованием локального хранилища или хранилища сеансов. Недоступно в контексте сервис-воркера. Этот API синхронный, поэтому не рекомендуется для хранения сложных данных.
- Хранилище кэша: как описано в модуле «Кэширование» .
Вы можете управлять всеми хранилищами устройств с помощью API Storage Manager на поддерживаемых платформах. API Cache Storage и IndexedDB обеспечивают асинхронный доступ к постоянному хранилищу для PWA и доступны из основного потока, веб-воркеров и сервис-воркеров. Оба играют важную роль в обеспечении надежной работы PWA в условиях нестабильной сети или ее отсутствия. Но когда следует использовать каждый из них?
Используйте API кэш-хранилища для сетевых ресурсов, к которым вы получаете доступ, запрашивая их по URL-адресу, например HTML, CSS, JavaScript, изображения, видео и аудио.
Используйте IndexedDB для хранения структурированных данных. К ним относятся данные, которые должны быть доступны для поиска или комбинирования подобно NoSQL, а также другие данные, например, пользовательские данные, которые не обязательно соответствуют URL-запросу. Обратите внимание, что IndexedDB не предназначен для полнотекстового поиска.
IndexedDB
Чтобы использовать IndexedDB , сначала откройте базу данных. Это создаст новую базу данных, если она не существует. IndexedDB — это асинхронный API, но он принимает обратный вызов вместо возврата Promise. В следующем примере используется библиотека idb Джейка Арчибальда — небольшая обёртка Promise для IndexedDB. Вспомогательные библиотеки не обязательны для использования IndexedDB, но если вы хотите использовать синтаксис Promise, библиотека idb
является вариантом.
В следующем примере создается база данных для хранения кулинарных рецептов.
Создание и открытие базы данных
Чтобы открыть базу данных:
- Используйте функцию
openDB
для создания новой базы данных IndexedDB с именемcookbook
. Поскольку базы данных IndexedDB имеют версии, необходимо увеличивать номер версии при каждом изменении структуры базы данных. Второй параметр — это версия базы данных. В данном примере она равна 1. - Объект инициализации, содержащий функцию обратного вызова
upgrade()
передаётся вopenDB()
. Функция обратного вызова вызывается при первой установке базы данных или при её обновлении до новой версии. Эта функция — единственное место, где могут выполняться действия. Действия могут включать создание новых хранилищ объектов (структур, используемых IndexedDB для организации данных) или индексов (по которым требуется выполнять поиск). Здесь же должна выполняться миграция данных. Как правило, функцияupgrade()
содержит операторswitch
без операторовbreak
, что позволяет выполнять каждый шаг по порядку, основанному на старой версии базы данных.
import { openDB } from 'idb';
async function createDB() {
// Using https://github.com/jakearchibald/idb
const db = await openDB('cookbook', 1, {
upgrade(db, oldVersion, newVersion, transaction) {
// Switch over the oldVersion, *without breaks*, to allow the database to be incrementally upgraded.
switch(oldVersion) {
case 0:
// Placeholder to execute when database is created (oldVersion is 0)
case 1:
// Create a store of objects
const store = db.createObjectStore('recipes', {
// The `id` property of the object will be the key, and be incremented automatically
autoIncrement: true,
keyPath: 'id'
});
// Create an index called `name` based on the `type` property of objects in the store
store.createIndex('type', 'type');
}
}
});
}
В этом примере создается хранилище объектов внутри базы данных cookbook
с именем recipes
, при этом свойство id
задается как ключ индекса хранилища, а также создается еще один индекс с именем type
, основанный на свойстве type
.
Давайте посмотрим на только что созданное хранилище объектов. После добавления рецептов в хранилище объектов и открытия DevTools в браузерах на базе Chromium или Web Inspector в Safari вы увидите следующее:
Добавление данных
IndexedDB использует транзакции. Транзакции группируют действия, выполняя их как единое целое. Они обеспечивают постоянную согласованность базы данных. Они также критически важны, если у вас запущено несколько копий приложения, для предотвращения одновременной записи одних и тех же данных. Чтобы добавить данные:
- Запустить транзакцию в
mode
чтенияreadwrite
. - Получите хранилище объектов, куда вы будете добавлять данные.
- Вызовите метод
add()
с сохраняемыми данными. Метод получает данные в виде словаря (в виде пар «ключ/значение») и добавляет их в хранилище объектов. Словарь должен быть клонируемым с помощью структурированного клонирования . Если вы хотите обновить существующий объект, вместо этого вызовите методput()
.
У транзакций есть обещание done
, которое разрешается, когда транзакция успешно завершается или отклоняется с ошибкой транзакции .
Как объясняется в документации библиотеки IDB , при записи в базу данных событие tx.done
сигнализирует об успешной фиксации данных в базе данных. Однако полезно дождаться завершения отдельных операций, чтобы увидеть любые ошибки, приводящие к сбою транзакции.
// Using https://github.com/jakearchibald/idb
async function addData() {
const cookies = {
name: "Chocolate chips cookies",
type: "dessert",
cook_time_minutes: 25
};
const tx = await db.transaction('recipes', 'readwrite');
const store = tx.objectStore('recipes');
store.add(cookies);
await tx.done;
}
После добавления файлов cookie рецепт появится в базе данных вместе с другими рецептами. Идентификатор автоматически устанавливается и увеличивается с помощью indexedDB. Если вы выполните этот код дважды, вы получите две одинаковые записи в файлах cookie.
Извлечение данных
Вот как получить данные из IndexedDB:
- Начните транзакцию и укажите хранилище или хранилища объектов, а также, при необходимости, тип транзакции.
- Вызовите
objectStore()
из этой транзакции. Убедитесь, что вы указали имя хранилища объектов. - Вызовите
get()
с ключом, который хотите получить. По умолчанию хранилище использует этот ключ в качестве индекса.
// Using https://github.com/jakearchibald/idb
async function getData() {
const tx = await db.transaction('recipes', 'readonly')
const store = tx.objectStore('recipes');
// Because in our case the `id` is the key, we would
// have to know in advance the value of the id to
// retrieve the record
const value = await store.get([id]);
}
Менеджер по хранению
Знание того, как управлять хранилищем PWA, особенно важно для правильного хранения и потоковой передачи сетевых ответов.
Емкость хранилища распределяется между всеми вариантами хранения, включая Cache Storage, IndexedDB, Web Storage и даже файл service worker с его зависимостями. Однако объем доступного хранилища варьируется от браузера к браузеру. Вряд ли оно закончится; в некоторых браузерах сайты могут хранить мегабайты и даже гигабайты данных. Например, Chrome позволяет браузеру использовать до 80% от общего дискового пространства, а отдельный источник может использовать до 60% от всего дискового пространства. В браузерах, поддерживающих Storage API, вы можете узнать, сколько хранилища еще доступно для вашего приложения, его квоту и его использование. В следующем примере Storage API используется для получения оценки квоты и использования, а затем рассчитывается процент использования и оставшиеся байты. Обратите внимание, что navigator.storage
возвращает экземпляр StorageManager
. Существует отдельный интерфейс Storage
, и их легко спутать.
if (navigator.storage && navigator.storage.estimate) {
const quota = await navigator.storage.estimate();
// quota.usage -> Number of bytes used.
// quota.quota -> Maximum number of bytes available.
const percentageUsed = (quota.usage / quota.quota) * 100;
console.log(`You've used ${percentageUsed}% of the available storage.`);
const remaining = quota.quota - quota.usage;
console.log(`You can write up to ${remaining} more bytes.`);
}
В Chromium DevTools вы можете увидеть квоту своего сайта и объем используемого хранилища с разбивкой по типам использования, открыв раздел «Хранилище» на вкладке «Приложение» .
Firefox и Safari не предлагают сводного экрана для просмотра всех квот хранилища и использования для текущего источника.
Сохранение данных
Вы можете запросить у браузера постоянное хранилище на совместимых платформах, чтобы избежать автоматического удаления данных после бездействия или при нехватке места на диске. Если разрешение предоставлено, браузер никогда не удалит данные из хранилища. Эта защита распространяется на регистрацию сервис-воркеров, базы данных IndexedDB и файлы в кэше. Обратите внимание, что пользователи всегда несут ответственность за данные и могут удалить хранилище в любое время, даже если браузер предоставил постоянное хранилище.
Чтобы запросить постоянное хранилище, вызовите метод StorageManager.persist()
. Как и прежде, доступ к интерфейсу StorageManager
осуществляется через свойство navigator.storage
.
async function persistData() {
if (navigator.storage && navigator.storage.persist) {
const result = await navigator.storage.persist();
console.log(`Data persisted: ${result}`);
}
Вы также можете проверить, предоставлено ли постоянное хранилище в текущем источнике, вызвав StorageManager.persisted()
. Firefox запрашивает у пользователя разрешение на использование постоянного хранилища. Браузеры на базе Chromium предоставляют или запрещают предоставление постоянного хранилища на основе эвристического анализа, определяющего важность контента для пользователя. Одним из критериев для Google Chrome является, например, наличие установленного PWA. Если пользователь установил значок PWA в операционной системе, браузер может предоставить постоянное хранилище.