На SO есть похожий вопрос, но он плохо сформулирован и не содержит подробностей. . Поэтому я пытаюсь написать лучший вопрос.
Меня интересует, как реализовать HATEOAS с одностраничным приложением (SPA), которое использует pushState
. Я хочу сохранить глубокие ссылки, чтобы пользователи могли добавлять URL-адреса в закладки в SPA и повторно посещать их позже или делиться ими с другими пользователями.
Для конкретности приведу гипотетический пример. Мое одностраничное приложение размещено по адресу https://www.hypothetical.com/
. Когда пользователь посещает этот URL-адрес в браузере, он загружает SPA и загружается. SPA просматривает текущий location.href
браузера, чтобы выяснить, какой ресурс API нужно получить и отобразить. В случае с корневым URL-адресом он запрашивает https://api.hypothetical.com/
, что приводит к следующему ответу:
{
"employees": "https://api.hypothetical.com/employees/",
"offices": "https://api.hypothetical.com/offices/"
}
Я умалчиваю о некоторых деталях, таких как accept
и content-type
, но давайте предположим, что этот гипотетический API поддерживает согласование содержимого и другие преимущества RESTful.
Теперь SPA отображает пользовательский интерфейс, который отображает эти две связи для пользователя, и пользователь может щелкнуть кнопку «Сотрудники», чтобы просмотреть сотрудников, или «Офисы», чтобы просмотреть офисы. Допустим, пользователь нажимает «Сотрудники». SPA должен pushState()
добавить новый href, иначе это навигационное решение не появится в истории, и пользователь не сможет использовать кнопку «Назад», чтобы вернуться на первый экран.
Это представляет собой небольшую дилемму: какой href должен использовать SPA? Очевидно, что он не может использовать https://api.hypothetical.com/employees/
. Мало того, что это недопустимый ресурс в SPA, он даже не находится в том же источнике, и pushState()
выдает исключения, если новый href находится в другом источнике.
Дилемма [возможно] легко разрешима: SPA знает об отношении ссылки, называемом employees
, поэтому SPA может жестко закодировать URL для этого ресурса: pushState(...,'https://www.hypothetical.com/employees')
. Затем он использует отношение ссылки https://api.hypothetical.com/employees/
для получения коллекции сотрудников. API возвращает такой результат:
{
"employees": [
{
"name": "John Doe",
"url": "https://api.hypothetical.com/employees/123",
},
{
"name": "Jane Doe",
"url": "https://api.hypothetical.com/employees/234",
},
...
]
}
Результатов больше двух, но я сократил их многоточием.
SPA хочет отображать эти данные в таблице, где каждое имя сотрудника является гиперссылкой, чтобы пользователь мог просмотреть более подробную информацию о конкретном сотруднике. Пользователь нажимает на «Джон Доу». SPA теперь должен отображать информацию о Джоне Доу. Он может легко получить ресурс, используя отношение ссылки, и может получить что-то вроде этого:
{
"name": "John Doe",
"phone_number": "2025551234",
"office": {
"location": "Washington, DC",
"url": "https://api.hypothetical.com/offices/1"
},
"supervisor": {
"name": "Jane Doe",
"url": "https://api.hypothetical.com/employees/234"
},
"url": "https://api.hypothetical.com/employees/123"
}
Но теперь снова возникает та же дилемма: какой URL-адрес должен выбрать SPA для представления этого нового внутреннего состояния? Это та же дилемма, что и выше, за исключением того, что на этот раз невозможно жестко закодировать один URL-адрес SPA, потому что имеется произвольное количество сотрудников.
Я думаю, что это нетривиальный вопрос, но давайте сделаем что-нибудь хакерское, чтобы мы могли двигаться дальше: SPA создает URL-адрес, заменяя «api» в имени хоста на «www». Это ужасно некрасиво, но не нарушает HATEOAS (URL-адрес SPA используется только на стороне клиента), и теперь SPA может pushState(...,'https://www.hypothetical.com/employees/123'
. Обобщая этот подход, SPA может отображать параметры навигации для любого отношения ссылки, а пользователь может исследовать связанные ресурсы: где находится офис этого человека? Какой номер телефона у куратора?
Это по-прежнему не решает проблемы с внешними ссылками. Что, если пользователь сделает закладку https://www.hypothetical.com/employees/123
, закроет браузер, а затем повторно откроет эту закладку позже? Теперь SPA не помнит, каким был базовый ресурс API. Мы не можем отменить замену (например, заменить «www» на «api»), потому что это не HATEOAS.
Мышление HATEOAS, похоже, заключается в том, что SPA должен снова запросить https://api.hypothetical.com/
и перейти по ссылкам обратно к профилю сотрудника Джона Доу, но нет фиксированного отношения ссылки для перехода от employees
как коллекции к John Doe
как конкретному сотруднику, так что это не сработает.
Другой подход HATEOAS заключается в том, что приложение может добавлять в закладки URL-адреса, которые оно обнаружило. Например, он может хранить хеш-таблицу, которая сопоставляет ранее просмотренные URL-адреса SPA с URL-адресами API:
{
"https://www.hypothetical.com/employees/123": "https://api.hypothetical.com/employees/123"
}
Это позволит SPA найти базовый ресурс и отобразить пользовательский интерфейс, но для этого требуется постоянное состояние между сеансами. Например. если мы сохраним этот хэш в хранилище HTML5, а пользователь очистит свое хранилище HTML5, то все закладки сломаются. Или, если пользователь отправит эту закладку другому пользователю, у этого другого пользователя не будет этого сопоставления URL-адреса SPA с URL-адресом API.
Итог: внедрение SPA с глубокими ссылками поверх HATEOAS API кажется мне очень неудобным. В моем текущем проекте я прибегал к тому, чтобы SPA создавал почти все URL-адреса. Вследствие этого решения API должен отправлять уникальные идентификаторы (а не только URL-адреса) для отдельных ресурсов, чтобы SPA мог создавать для них правильные URL-адреса.
Есть ли у кого-нибудь опыт в этом? Есть ли способ удовлетворить эти, казалось бы, противоречивые критерии?