如何在 HATEOAS 服务器上实现深度链接客户端?

How to implement deep linking client on top of HATEOAS server?

There's a on SO, but it's not phrased well and it lacks details. So I'm trying to write a better question.

我对如何通过使用 pushState 的单页应用程序 (SPA) 实施 HATEOAS 很感兴趣。我想保留深度 linking 以便用户可以在 SPA 中为 URL 添加书签并稍后重新访问它们或与其他用户共享它们。

为了具体起见,我将举一个假设的例子。我的单页应用程序托管在 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/"
}

我在掩饰一些细节,例如 acceptcontent-type,但我们假设这个假设的 API 支持 content-negotiation 和其他 RESTful天哪.

现在 SPA 呈现一个用户界面,向用户显示这两个 link 关系,用户可以单击 "Employees" 按钮查看员工或 "Offices" 查看办公室。假设用户点击 "Employees"。 SPA 需要 pushState() 一些新的 href,否则这个导航决定将不会出现在历史记录中,用户将无法使用返回按钮 return 到第一个屏幕。

这提出了一个小难题:SPA 应该使用什么 href? 显然,它不能推送 https://api.hypothetical.com/employees/。这不仅不是 SPA 中的有效资源,它甚至不在同一来源,如果新 href 来源不同,pushState() 会抛出异常。

这个难题[也许]很容易解决:SPA 知道一个叫做 employees 的 link 关系,所以 SPA 可以硬编码这个资源的 URL:pushState(...,'https://www.hypothetical.com/employees')。接下来,它使用 link 关系 https://api.hypothetical.com/employees/ 来获取员工 collection。 API return 的结果如下:

{
  "employees": [
    {
      "name": "John Doe",
      "url": "https://api.hypothetical.com/employees/123",
    },
    {
      "name": "Jane Doe",
      "url": "https://api.hypothetical.com/employees/234",
    },
    ...
  ]
}

有两个以上的结果,但我用省略号缩写了。

SPA 想要在 table 中显示此数据,其中每个员工姓名都是一个 hyperlink,以便用户可以查看有关特定员工的更多详细信息。用户点击 "John Doe"。 SPA 现在需​​要显示有关 John Doe 的详细信息。它可以很容易地使用 link 关系获取资源,它可能会得到这样的东西:

{
  "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"
}

但现在同样的困境再次出现:SPA应该URL选择什么来表示这个新的内部状态?这与上面的困境相同,除了这个不可能硬编码单个 SPA URL,因为有任意数量的员工。

我认为这是一个 non-trivial 问题,但让我们做一些 hacky 以便我们继续前进:SPA 通过将主机名中的 'api' 替换为 [ 来构建 URL =81=]。它非常丑陋,但它不违反 HATEOAS(SPA URL 仅在客户端使用),现在 SPA 可以 pushState(...,'https://www.hypothetical.com/employees/123'。推广这种方法,SPA 可以显示任何 link 关系的导航选项,用户可以探索相关资源:这个人的办公室在哪里?主管的 phone 号码是多少?

这还是没有解决deep linking.如果用户收藏了https://www.hypothetical.com/employees/123,关闭浏览器,稍后再访问这个书签怎么办在?现在 SPA 没有关于底层 API 资源是什么的 recollection。我们无法反转替换(例如,将 'www' 替换为 'api'),因为那不是 HATEOAS。

HATEOAS 的心态似乎是 SPA 应该再次请求 https://api.hypothetical.com/ 并跟随 link 回到 John Doe 的员工资料,但是没有固定的 link 关系可以从employees 作为 collection 到 John Doe 作为特定员工,所以这行不通。

另一种 HATEOAS 方法是应用程序可以将它发现的 URL 添加为书签。例如,它可以存储一个散列 table,将之前看到的 SPA URLs 映射到 API URLs:

{
  "https://www.hypothetical.com/employees/123": "https://api.hypothetical.com/employees/123"
}

这将允许 SPA 找到底层资源并呈现 UI,但它需要跨会话的持久状态。例如。如果我们将这个散列存储在 HTML5 存储中并且用户清除他们的 HTML5 存储,那么所有的书签都会被破坏。或者,如果用户将此书签发送给另一个用户,则该用户不会拥有 SPA URL 到 API URL.

的映射

底线:在 HATEOAS API 之上实施深度 linking SPA 对我来说感觉很尴尬。在我当前的项目中,我求助于让 SPA 构造几乎所有的 URL。作为该决定的结果,API 必须为单个资源发送唯一标识符(而不仅仅是 URLs),以便 SPA 可以为它们生成好的 URLs。

有人有这方面的经验吗?有没有办法满足这些看似矛盾的标准?

我可能遗漏了一些东西,但大概对于不同的资源类型,您有不同的 views/pages。因此,当您加载员工时,它使用 employees.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

我也曾为这个概念而苦恼。 hateoas api 就是将客户端与服务器分离,因此我认为将 "client url -> api uri" 的映射硬编码到本地存储是一种倒退,原因包括您提到的等等。话虽如此,为客户端和服务器使用单独的域语言并没有什么耦合。您可以在页面上执行某些操作(例如单击员工 # _)以任何您喜欢的方式触发推送状态,例如“/employees/#”。当应用程序通过 link-sharing 引导到另一个浏览器时,一些服务将知道如何读取 DSL '/employees/#' 并将你的应用程序快速转发到适当的状态。在权限不同的情况下,您有一些消息解释为什么您不能访问员工#_,而是列出您有权查看的员工。如果您有很多像这样的深度 linking 视图,它会变得非常麻烦,但这就是用平坦的 url 层次结构表示复杂状态对象的代价。

我考虑过的另一种方法是显式 "share this" 按钮。点击按钮会要求服务器在自己的DSL中铸造一个url,这样当其他用户访问这个共享url时,服务器可以将相关的状态信息传递给客户端。客户端必须设置为处理这种情况,可能带有一些条件“如果此资源包含 'deep link' 属性,请执行一些特殊操作。

不管怎样,这都不是一个容易解决的问题。