Как реализовать клиент глубокой связи поверх сервера HATEOAS?

На 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-адреса.

Есть ли у кого-нибудь опыт в этом? Есть ли способ удовлетворить эти, казалось бы, противоречивые критерии?


person Mark E. Haase    schedule 13.03.2015    source источник
comment
Шесть лет спустя я борюсь с той же проблемой. В сети нет хороших примеров реализации. Вы решили свою проблему за это время? Есть ли у вас какие-либо ссылки или учебные пособия для вашего решения?   -  person Raman    schedule 30.06.2021


Ответы (2)


Я, вероятно, что-то упускаю, но, по-видимому, для разных типов ресурсов у вас разные представления/страницы. Поэтому, когда вы загружаете сотрудников, он использует представление employee.html, а когда вы переходите к сотруднику, вы используете представление employee.html. Так почему это должно быть больше похоже на это?

//user clicks employees
pushState( "employees.html#https://api.hypothetical.com/employees/" )

//user clicks on an employeed
pushState( "employee.html#https://api.hypothetical.com/employees/12345/" )

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

Я вижу pushState, но на самом деле его, вероятно, правильнее рассматривать как pushViewState.

person Chris DaMour    schedule 02.04.2015
comment
если у вас разные страницы для разных ресурсов, это уже не SPA - person gyoder; 10.04.2015
comment
Я предполагаю, что он имел в виду частичные или маршруты, так что это SPA. - person redben; 12.07.2018
comment
Наличие URL-адреса внутри другого URL-адреса выглядит некрасиво, имхо. Но похоже, что это лучшее решение на данный момент. - person user1614543; 24.05.2019
comment
красивые URL-адреса не имеют ничего общего с RESTful - person Chris DaMour; 25.05.2019

Я тоже боролся с этой концепцией. Ненавистный API предназначен для отделения клиента от сервера, поэтому я думаю, что жесткое кодирование карты «URL-адрес клиента -> API-адрес uri» в локальное хранилище является шагом назад по причинам, которые вы упомянули, и многим другим. При этом нет ничего общего в том, чтобы иметь отдельные доменные языки для клиента и сервера. У вас могут быть определенные действия на странице (например, щелчок по сотруднику # _), запускающие push-состояние с чем угодно, например, «/employees/#». Когда приложение загружается в другой браузер путем обмена ссылками, какая-то служба будет знать, как прочитать DSL «/employees/#» и быстро перенаправить ваше приложение в соответствующее состояние. В случае, когда разрешения различаются, у вас есть какое-то сообщение, объясняющее, почему вы не можете получить доступ к сотруднику # _, и вместо этого перечислите сотрудников, которых у вас есть разрешение видеть. Это может стать довольно громоздким, если у вас есть много подобных представлений с глубокими ссылками, но такова цена представления сложного объекта состояния с плоской иерархией URL-адресов.

Другой подход, который я рассматривал, — это явная кнопка «поделиться этим». При нажатии на кнопку сервер попросит создать URL-адрес в своем собственном DSL, чтобы, когда другой пользователь посещает этот URL-адрес общего ресурса, сервер мог передать соответствующую информацию о состоянии клиенту. Клиент должен быть настроен для обработки этого сценария, возможно, с некоторыми условиями вокруг «если этот ресурс содержит свойство« глубокая ссылка », сделайте что-то особенное.

В любом случае решить эту проблему не так-то просто.

person Clev3r    schedule 18.07.2015
comment
Почти два года спустя: какие-нибудь новые мысли по этому поводу? Я новичок в REST, и у меня такая же проблема, когда достаточно легко создать клиент, который переходит по ссылкам глубже в серверный API, но глубокая ссылка на клиенте кажется, что должен знать о схеме URL-адреса сервера, иначе для решения этой проблемы потребуется какая-то третья служба кэширования глубоких ссылок... - person Dan Passaro; 17.05.2017