Лучшие практики использования IndexedDB

Изучите лучшие практики синхронизации состояния приложения между IndexedDB — популярной библиотекой управления состоянием.

Когда пользователь впервые загружает веб-сайт или приложение, ему часто приходится выполнять изрядный объем работы по созданию исходного состояния приложения, которое используется для визуализации пользовательского интерфейса. Например, иногда приложению необходимо выполнить аутентификацию пользователя на стороне клиента, а затем выполнить несколько запросов API, прежде чем оно получит все данные, необходимые для отображения на странице.

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

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

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

Сохранение предсказуемости вашего приложения

Многие сложности вокруг IndexedDB связаны с тем, что существует множество факторов, которые вы (разработчик) не можете контролировать. В этом разделе рассматриваются многие проблемы, которые следует учитывать при работе с IndexedDB.

Не все можно хранить в IndexedDB на всех платформах.

Если вы храните большие файлы, созданные пользователем, например изображения или видео, вы можете попытаться сохранить их как объекты File или Blob . Это будет работать на некоторых платформах, но не работает на других. Safari на iOS, в частности, не может хранить Blob в IndexedDB.

К счастью, преобразовать Blob в ArrayBuffer и наоборот не так уж сложно. Хранение ArrayBuffer в IndexedDB очень хорошо поддерживается.

Однако помните, что Blob имеет тип MIME, а ArrayBuffer — нет. Вам нужно будет сохранить тип рядом с буфером, чтобы правильно выполнить преобразование.

Чтобы преобразовать ArrayBuffer в Blob вы просто используете конструктор Blob .

function arrayBufferToBlob(buffer, type) {
  return new Blob([buffer], { type: type });
}

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

function blobToArrayBuffer(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.addEventListener('loadend', () => {
      resolve(reader.result);
    });
    reader.addEventListener('error', reject);
    reader.readAsArrayBuffer(blob);
  });
}

Запись в хранилище может завершиться неудачей

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

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

Вы можете обнаружить ошибки в операциях IndexedDB, добавив обработчик событий для событий error при каждом создании объекта IDBDatabase , IDBTransaction или IDBRequest .

const request = db.open('example-db', 1);
request.addEventListener('error', (event) => {
  console.log('Request error:', request.error);
};

Сохраненные данные могли быть изменены или удалены пользователем.

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

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

Сохраненные данные могут быть устаревшими

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

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

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

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

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

Как правило, объем операций чтения и записи в IndexedDB не должен превышать объем, необходимый для доступа к данным.

Хотя IndexedDB позволяет хранить большие вложенные объекты как одну запись (и это, по общему признанию, весьма удобно с точки зрения разработчика), такой практики следует избегать. Причина в том, что когда IndexedDB сохраняет объект, она должна сначала создать структурированный клон этого объекта, и процесс структурированного клонирования происходит в основном потоке. Чем больше объект, тем дольше будет время блокировки.

Это создает некоторые проблемы при планировании сохранения состояния приложения в IndexedDB, поскольку большинство популярных библиотек управления состоянием (например, Redux ) работают, управляя всем вашим деревом состояний как одним объектом JavaScript.

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

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

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

Как и большинство передовых практик, это не правило «все или ничего». В тех случаях, когда невозможно разбить объект состояния и просто записать минимальный набор изменений, разбиение данных на поддеревья и запись только их по-прежнему предпочтительнее, чем всегда писать все дерево состояний. Небольшие улучшения лучше, чем отсутствие улучшений вообще.

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

Выводы

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

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

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