Есть аналогичный вопрос по 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-адреса.

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

11
Mark E. Haase 13 Мар 2015 в 22:24

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

1
Chris DaMour 2 Апр 2015 в 06:26

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

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

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

4
Clev3r 18 Июл 2015 в 17:00