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

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

localStorage API

localStorage - один из самых популярных вариантов хранения данных в браузере, к которому обращаются многие разработчики. Данные хранятся между сеансами, никогда не передаются серверу и доступны для всех страниц в рамках одного протокола и домена. Хранилище ограничено ~ 5 МБ.

Удивительно, но команда Google Chrome не рекомендует использовать эту опцию, поскольку она блокирует основной поток и недоступна для веб-воркеров и сервис-воркеров. Они запускали эксперимент - KV Storage, как лучшую версию, но это была всего лишь пробная версия, которая, похоже, еще не вышла.

API localStorage доступен как window.localStorage и может сохранять только строки формата UTF-16. Прежде чем сохранять данные в localStorage, необходимо убедиться, что они преобразованы в строки. Основные три функции:

  • setItem ('key', 'value'),
  • getItem ('key'),
  • removeItem ('key')

key - ключ, value - значение.

Все эти функции синхронны, что упрощает работу, но блокирует основной поток.

Стоит упомянуть, что у localStorage есть двойник, называемый sessionStorage. Единственная разница в том, что данные, хранящиеся в sessionStorage, будут храниться только в текущей сессии, но API останется прежним.

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

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

Следующий пример загружает имена Покемонов из PokéAPI. Мы отправляем запрос GET с помощью fetch и перечисляем все имена в элементе ul. Получив ответ, мы кэшируем его в localStorage, поэтому наше следующее посещение может быть намного быстрее или даже работать в автономном режиме. Мы должны использовать JSON.stringify для преобразования данных в строку и JSON.parse для чтения из кэша.

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

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

IndexedDB API

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

По сравнению с localStorage, IndexedDB требует намного больше кода. В примерах использована нативная API с оболочкой Promise, но настоятельно рекомендуется использовать сторонние библиотеки. Одна из лучших - localForage, потому что она использует тот же API localStorage, но реализует его методом постепенного улучшения, то есть, если ваш браузер поддерживает IndexedDB, он будет его использовать, а если нет, он вернется к localStorage.

Давайте перейдем к нашему примеру с пользовательскими предпочтениями:

idb - это оболочка Promise, которую мы используем вместо работы с низкоуровневым API на основе событий. Первое, что следует заметить, это то, что каждый доступ к базе данных является асинхронным, то есть мы не блокируем основной поток. По сравнению с localStorage это серьезное преимущество.

Нам нужно открыть соединение с нашей базой данных, чтобы она была доступна во всем приложении для чтения и записи. Мы даем нашей базе данных имя my-db, версию 1 и функцию обновления для применения изменений между версиями. Это очень похоже на миграцию базы данных. Схема нашей базы данных проста: только одно хранилище объектов - "preferences". Хранилище объектов - это эквивалент таблицы SQL. Чтобы писать или читать из базы данных, мы должны использовать транзакции. Это утомительная часть использования IndexedDB. Использование транзакций можно увидеть в функциях save и load в примере.

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

Вариант использования IndexedDB для отображения имён Покемонов:

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

Реализация страниц удалена, поскольку в этом случае IndexedDB не представляет никакой разницы по сравнению с localStorage. Даже с IndexedDB пользователь по-прежнему не будет делиться выбранной страницей с другими или добавлять ее в закладки для использования в будущем.

Cookies

Уникальным способом хранения данных является использование файлов cookie. Это единственное хранилище, которое используется совместно с сервером. Файлы cookie отправляются как часть каждого запроса. Происходит это, когда пользователь просматривает страницы в нашем приложении или когда отправляет ajax-запросы. Это позволяет нам создавать общее состояние между клиентом и сервером, а также разделять состояние между несколькими приложениями в разных поддоменах, что невозможно при использовании предыдущих типов хранения данных. Одно предостережение: файлы cookie отправляются с каждым запросом, а это означает, что мы должны сохранять наши файлы cookie небольшими, чтобы поддерживать адекватный размер запроса.

Чаще всего файлы cookie используются для аутентификации, что выходит за рамки данной статьи. Как и localStorage, файлы cookie могут хранить только строки. Файлы cookie объединяются в одну строку, разделенную точкой с запятой, и отправляются в заголовке запроса. Также вы можете установить множество атрибутов для каждого файла cookie, например срок действия, разрешенные домены, разрешенные страницы и многое другое.

В примерах показано, как управлять файлами cookie на стороне клиента, но их также можно изменить в приложении на стороне сервера.

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

Чтение и запись файлов cookie с помощью JavaScript не такое простое, как вы думаете. Чтобы сохранить новый файл cookie, вам необходимо установить document.cookie (взгляните на функцию save в примере выше). В примере устанавливается cookie dark_theme и добавляется атрибут max-age, чтобы убедиться, что срок его действия не истечет при закрытии вкладки. Также добавляются атрибуты SameSite и Secure. Это необходимо, поскольку CodePen использует iframe для запуска примеров, но в большинстве случаев они вам не понадобятся. Для чтения файла cookie необходимо проанализировать строку файла cookie.

Строка cookie выглядит так:

key1=value1;key2=value2;key3=value3

Итак, сначала нам нужно разделить строку в местах точек с запятой. Теперь у нас есть массив файлов cookie в виде key1 = value1, и теперь нам нужно найти правильный элемент в массиве. Мы разделяем каждый элемент массива по знаку равенства и получаем последний элемент в новом массиве. Немного утомительно, но как только вы реализуете функцию getCookie (или скопируете ее из примера), дальше о подобных манипуляциях можете забыть.

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

Пример разбивки на страницы также не подходит для файлов cookie, как и localStorage и IndexedDB. Текущая страница - это временное состояние, которым мы хотели бы поделиться с другими, и ни один из этих методов не позволяет его достичь.

URL storage

URL сам по себе не является хранилищем, но это отличный способ создать общедоступное состояние. На практике это означает добавление параметров запроса к текущему URL-адресу, которые можно использовать для воссоздания текущего состояния. Лучшим примером могут служить поисковые запросы и фильтры. Если мы будем искать термин flexbox на нашем сайте, URL-адрес будет обновлен на https://fruntend.com/search-results?search=flexbox. Посмотрите, как легко поделиться поисковым запросом, если мы используем URL-адрес! Еще одно преимущество заключается в том, что вы можете просто нажать кнопку обновления, чтобы получить новые результаты вашего запроса или даже добавить его в закладки.

Мы можем сохранять только строки в URL-адресе, а его максимальная длина ограничена, поэтому у нас не так много места. Поэтому нужно позаботиться, чтобы такое хранилище было всегда минимальным. Никто не любит длинные и пугающие URL-адреса.

Поскольку CodePen использует iframe для запуска примеров, вы не сможете увидеть, что URL-адрес действительно меняется. Однако не волнуйтесь, в коде есть все мелочи, так что вы можете использовать его где угодно.

Мы можем получить доступ к строке запроса через window.location.search и, к счастью, ее можно проанализировать с помощью класса URLSearchParams. Больше не нужно применять сложный синтаксический анализ строк. Когда мы хотим прочитать текущее значение, мы можем использовать функцию get. Когда мы хотим записать данные, используем set. Недостаточно просто установить значение; нам также необходимо обновить URL-адрес. Это можно сделать с помощью history.pushState или history.replaceState, в зависимости от поведения, которое мы хотим достичь.

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

Как и файлы cookie, мы не можем сохранять данные приложения в URL-адресе, поскольку у нас мало места. И даже если нам удастся их сохранить, URL-адрес будет длинным и не будет привлекательным для нажатия. Это может выглядеть как своего рода фишинговая атака.

Как и в нашем примере с разбивкой на страницы, временное состояние приложения лучше всего подходит для строки запроса URL. Опять же, вы не можете видеть изменения URL-адреса (из-за iframe), но URL-адрес обновляется с параметром запроса ?page=x каждый раз, когда вы нажимаете на страницу. Когда веб-страница загружается, она ищет этот параметр запроса и соответственно выбирает нужную страницу. Теперь мы можем поделиться этим URL-адресом с нашими друзьями, чтобы они могли насладиться нашим любимым покемоном.

Cache API

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

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

Чтобы узнать больше о Cache API, команда Google Chrome подготовила отличное руководство.

Бонус: расширения браузера

Если вы создаете расширение для браузера (browser extension), у вас появляется ещё один способ хранения ваших данных. Он доступен через chrome.storage или browser.storage, если вы используете полифил от Mozilla. Однако не забудьте запросить разрешение на хранение в своем манифесте, чтобы получить доступ.

Есть два варианта хранения данных: локальное и синхронизируемое. Локальное хранилище говорит само за себя; это означает, что оно никому не доступно и хранится локально. Второй вариант - хранилище синхронизируется как часть учетной записи Google, и где бы вы ни устанавливали расширение с той же учетной записью, это хранилище будет синхронизироваться. Довольно крутая функция. У обоих одинаковый API, поэтому при необходимости переключаться между ними очень просто. Это асинхронное хранилище, поэтому оно не блокирует основной поток, как localStorage. Для демонстрации этого варианта хранилища требуется создавать/устанавливать расширение для браузера, однако, в двух словах, это довольно просто в использовании и почти похоже на localStorage. Дополнительные сведения о реализации можно почитать в документации Chrome.

Заключение

В браузере есть множество опций, которые мы можем использовать для хранения наших данных. Следуя советам команды Chrome, нашим хранилищем должно быть IndexedDB. Это асинхронное хранилище, в котором достаточно места для хранения всего, что мы захотим. localStorage не рекомендуется, но его проще использовать, чем IndexedDB. Файлы cookie - отличный способ поделиться состоянием клиента с сервером, но в основном они используются для аутентификации.

Если вы хотите создать страницы с общим состоянием, например страницу поиска, используйте строку запроса URL для хранения этой информации. Наконец, если вы создаете расширение, не забудьте прочитать о chrome.storage.