Храним состояние в URL
Обновленная и дополненная версия от 2022 года
Если вы разрабатываете веб-приложение, то рано или поздно столкнетесь с проблемой сохранения текущего состояния системы для пользователя.
Например, вы продаете через интернет элитный картофель. Покупатель заходит на сайт и первым делом настраивает условия отбора картофелин:
- строго из Боливии или ЮАР,
- урожая 2022 года,
- размер клубня от 3 до 7 см,
- желательно в форме морского тюленя.
Получает список из 300 позиций (да, в ЮАР очень популярна картоха в форме тюленя), нарезанный на 6 страниц по 50 элементов каждая. Переходит на третью страницу, открывает карточку картофелины и застывает в немом восхищении на несколько секунд. А потом случайно нажимает на рефреш страницы. Как поступит ваше приложение?
Как хранить состояние
JavaScript-приложения традиционно работают с локальным состоянием в виде объектов в памяти. Это удобно, потому что нет ограничений на объем, поддерживаются сложные структуры данных, и не приходится заниматься сериализацией-десериализацией. Но подойдет ли это нашему картофельному магазину? Давайте разбираться.
Не хранить вовсе
Допустим, вы не стали заморачиваться с состоянием и держите его в памяти. Тогда при обновлении страницы текущий контекст благополучно потеряется, а пользователя перекинет на главную страницу, где он с негодованием уставится на аршинный заголовок «ЭЛИТНЫЙ КАРТОФЕЛЬ».
Так раньше вел себя гугл-календарь. Как бы вы не перемещались по календарю, какие бы фильтры не накладывали — урл всегда сохранял свой изначальный вид:
https://www.google.com/calendar/render
Обновляете страницу — и календарь радостно сбрасывает вас на текущую неделю. Удобненько.
Хранить локально
Большинство сервисов понимает, что терять контекст при обновлении страницы нехорошо. Они запоминают состояние на клиенте (в локал-сторадже или других браузерных хранилищах). Это решает проблему с рефрешем страницы, но поставить закладку все равно не получится.
Плюс такой подход создает проблему с конфликтующими изменениями состояния. Я открыл две вкладки браузера, зашел на ваш картофельный сайт, и в одной вкладке ищу «картофель молодой», а в другой — «ботва разлапистая». Какой из запросов сохраним?
Хранить набор URL-параметров
Еще со времен, когда динамическая природа веб-сайтов ограничивалась тегом <blink>
, хорошим тоном считалось хранить состояние в урле («URL-параметры», также известны как «GET-параметры» или «строка запроса»). Такой URL хоть в почту, хоть в закладки — восстановить контекст по нему не проблема:
https://potato.shop/catalog/?search=wild+potatoes&country=bo,za&size=3-7&page=5
Состояние в урле делает каждую вкладку браузера полностью автономной. Нет общих данных в локальном хранилище — нет и конфликтов. Это упрощает жизнь разработчику, а у пользователя не вскипает мозг от загадочных глюков системы.
Хранить сериализованное состояние
URL-параметры отлично справляются со скалярными значениями (строками, числами, логическими). Но меньше подходят для коллекций и более сложных структур. Поэтому программисты иногда делают так:
- представляют состояние в виде словаря;
- сериализуют его в Base64;
- записывают одним параметром в URL.
Например, для такого состояния:
{
"search": "wild potatoes",
"country": ["bo", "za"],
"size": { "min": 3, "max": 7 },
"page": 5
}
Получится такой URL:
https://potato.shop/catalog/?state=eyJzZWFyY2giOiJ3aWxkIHBvdGF0b2VzIiwiY291bnRyeSI6WyJibyIsInphIl0sInNpemUiOnsibWluIjozLCJtYXgiOjd9LCJwYWdlIjo1fQ==
Гибридные подходы
Можно сохранять в урле только основные параметры, а дополнительные — в локальном хранилище:
https://potato.shop/catalog/?search=wild+potatoes&page=10
Можно основные параметры передавать явно, а дополнительные — сериализовать:
https://potato.shop/catalog/?search=wild+potatoes&state=eyJjb3VudHJ5IjpbImJvIiwiemEiXSwic2l6ZSI6eyJtaW4iOjMsIm1heCI6N30sInBhZ2UiOjV9
Встречаются и более творческие варианты.
Например, хранить состояние списка локально, а при переходе к конкретному элементу открывать его в новой вкладке — чтобы не заморачиваться с возвратом к списку.
Или реализовать «короткие ссылки» по принципу bit.ly. Сохранять полное состояние на сервере, генерить для него уникальную ссылку вроде https://potato.shop/catalog/xKda7
, ее и показывать ее на клиенте.
С вариантами разобрались, теперь давайте рассмотрим один из них подробнее.
Как хранить состояние в URL-параметрах
На первый взгляд, хранить состояние в виде набора параметров URL легко и приятно.
Было:
{
"search": "wild potatoes",
"page": 5
}
Стало (здесь и далее примеры без url-кодирования):
?search=wild+potatoes&page=5
Действительно, строки и числа передать в параметрах несложно. С другими типами данных интереснее.
Логическое значение
Обычно используют true
/ false
, либо 1
/ 0
.
Было:
{
"search": "potatoes",
"popular": true
}
Стало:
?search=potatoes&popular=true
?search=potatoes&popular=1
Дата и время
Обычно используют RFC3339 (2020-01-02T10:11:12Z
) либо Unix Time (секунды от полночи 01.01.1970).
Было (поскольку пример в JSON, привожу дату в RFC3339; у вашего языка есть «родное» представление):
{
"search": "potatoes",
"after": "2020-01-02T10:11:12Z"
}
Стало:
?search=potatoes&after=2020-01-02T10:11:12Z
?search=potatoes&after=1577959872
Реже используют unix time в миллисекундах:
?search=potatoes&after=1577959872000
Пустое значение
Если какое-то свойство не задано, его обычно передают пустым, либо не передают вовсе.
Было:
{
"search": "potatoes",
"country": null
}
Стало:
?search=potatoes&country=
?search=potatoes
Иногда передают специальное значение (например, null
):
?search=potatoes&country=null
Список (массив)
До сих пор мы имели дело со скалярными значениями. Со списками становится веселее.
{
"search": "potatoes",
"country": ["bo", "za"]
}
Классический вариант — повторять название свойства для каждого значения, как диктует RFC 6570:
?search=potatoes&country=bo&country=za
Иногда к названию добавляют []
, чтобы показать, что это список:
?search=potatoes&country[]=bo&country[]=za
Бывает, что и индекс указывают:
?search=potatoes&country[0]=bo&country[1]=za
Любители краткой записи указывают название свойства один раз, а значения перечисляют через запятую:
?search=potatoes&country=bo,za
Словарь (карта)
Список перечислял набор значений. Словарь же содержит вложенные свойства:
{
"search": "potatoes",
"size": { "min": 3, "max": 7 }
}
Интернет-стандарты не предусматривают передачу составных объектов в URL. Поэтому разработчики изобрели свои варианты.
Чаще всего название основного свойства дублируют, а названия вложенных указывают в []
:
?search=potatoes&size[min]=3&size[max]=7
Реже используют .
-нотацию:
?search=potatoes&size.min=3&size.max=7
Вложенность больше двух уровней обычно не используют, потому что развернуть такие URL-параметры обратно в объект совсем уж нетривиально.
Итого
Основные способы сохранения локального состояния:
- не хранить вовсе;
- локальное хранилище;
- набор URL-параметров;
- сериализованный объект.
Память и локальное хранилище отлично подходят для «приватного» состояния — настроек, кешей, истории. URL-параметры — для «публичного» состояния, чтобы ссылку с состоянием можно было сохранить или переслать. URL с параметрами получается наглядным и позволяет передавать достаточно сложные структуры данных.
Главное, какой бы подход вы не выбрали — сохраняйте и восстанавливайте контекст прозрачно для пользователя.
★ Подписывайтесь на новые заметки.