作为REST API客户端的Web应用程序:如何处理资源标识符


21

当我尝试实施REST时,与REST相关的几个概念就在我的脑海中。

我有一个包含业务逻辑的REST-ful后端API系统,以及一个提供UI的Web应用程序。从有关REST的各种资源(特别是REST在实践中:超媒体和系统体系结构)中,我知道我不应该公开实体的原始标识符,而应该返回带有的超链接rel="self"

考虑这个例子。REST api具有返回一个人的资源:

<Person>
  <Links>
    <Link rel="self" href="http://my.rest.api/api/person/1234"/>
  </Links>
  <Pets>
    <Link rel="pet" href="http://my.rest.api/api/pet/678"/>
  </Pets>
</Person>

Web应用程序出现问题。假设它返回的页面包含浏览器的超链接:

<body class="person">
  <p>
    <a href="http://my.web.app/pet/???????" />
  </p>
</body>

我应该在href属性中添加什么?当用户打开目标页面时,如何将API实体URL保留在Web应用程序中以便能够获取该实体?

要求似乎有冲突:

  1. 超链接href应指向Web应用程序,因为它是承载UI的系统
  2. href应该有实体的一些ID,因为Web应用程序必须能够在目标页打开时,解决实体
  3. 提到的书说,Web应用程序不应解析/构造REST URL,因为它不是基于REST的。

URI对消费者应该是不透明的。只有URI的颁发者知道如何解释它并将其映射到资源。

因此,我不能仅仅1234取自API响应URL,因为作为RESTful客户端,我应该将其视为http://my.rest.api/api/AGRIDd~ryPQZ^$RjEL0j。另一方面,我必须提供一些URL,这些URL可以引到我的Web应用程序,并且足以使该应用程序以某种方式还原API的原始URL,并使用该URL来访问API资源。

最简单的方法可能只是使用资源的API URL作为它们的字符串标识符。但是网页网址http://my.web.app/person/http%3A%2F%2Fmy.rest.api%2Fapi%2Fperson%2F1234很丑陋。

对于台式机应用程序或单页javascript应用程序而言,这似乎非常容易。由于它们持续存在,因此它们可以将URL与服务对象一起保留在内存中,以延长应用程序的生命周期,并在必要时使用它们。

使用Web应用程序,我可以想象几种方法,但是所有方法似乎都很奇怪:

  1. 替换API URL中的主机,并仅保留结果。巨大的缺点是,它需要Web应用程序处理API生成的任何URL,这意味着怪异的耦合。而且,它不再是RESTful的,因为我的Web应用程序开始解释URL。
  2. 公开REST API中的原始ID及其链接,使用它们来构建Web App的URL,然后使用Web App Server上的ID在API中找到所需的资源。这样比较好,但是会影响Web应用程序服务器的性能,因为Web应用程序必须通过REST服务导航来发出一系列形式的获取ID请求的链接,以处理来自浏览器的任何请求。对于有些嵌套的资源,这可能会很昂贵。
  3. self将api返回的所有URL 存储在Web应用服务器上的持久(DB?)映射中。为它们生成一些ID,使用这些ID构建Web应用程序页面URL并获取REST服务资源的URL。也就是说,我http://my.rest.api/pet/678用一个新的密钥将URL 保留在某处3,然后将网页URL生成为http://my.web.app/pet/3。这看起来像某种HTTP Cache实现。我不知道为什么,但是对我来说似乎很奇怪。

还是所有这些都意味着RESTful API不能用作Web应用程序的后端?


1
目前尚不清楚您要完成什么,这可能是因为您的简单意图被覆盖在要放在彼此之上的体系结构层之下,因此很难说“ RESTful API”是否真的可以为您提供帮助。据我对您的问题的了解,选择2是一个简单且可行的解决方案。这里的“问题”是“ RESTful API”所固有的。RestIsJustSqlReinvented,当您尝试从任何RDBMS检索足够复杂的子图时,确实会遇到相同的问题。使用针对查询优化的缓存或表示形式。
back2dos

Answers:


5

编辑以解决问题更新,删除了先前的答案

查看您对问题的更改,我认为我了解您所面临的问题。由于在您的资源(没有链接)上没有作为标识符的字段,因此您无法在GUI中引用该特定资源(即指向描述特定宠物的页面的链接)。

首先要确定的是,没有主人的宠物是否有意义。如果我们可以养一只没有任何主人的宠物,那我想说我们需要在宠物上有某种独特的属性,我们可以用它来指代它。我不认为这不会违反不直接公开ID的事实,因为实际资源ID仍会隐藏在REST客户端无法解析的链接中。考虑到这一点,我们的宠物资源可能看起来像:

<Entity type="Pet">
    <Link rel="self" href="http://example.com/pets/1" />
    <Link rel="owner" href="http://example.com/people/1" />
    <UniqueName>Spot</UniqueName>
</Entity>

现在,我们可以将该宠物的名称从Spot更新为Fido,而不必在整个应用程序中弄乱任何实际的资源ID。同样,我们可以在GUI中通过以下方式引用该宠物:

http://example.com/GUI/pets/Spot

如果没有主人的宠物没有任何意义(或没有主人的宠物在系统中是不允许的),那么我们可以将主人用作系统中宠物“身份”的一部分:

http://example.com/GUI/owners/John/pets/1(John 列表中的第一只宠物)

一个小小的注意事项,如果“宠物”和“人”可以彼此分开存在,那么我不会将API的入口点称为“人”资源。相反,我将创建一个更通用的资源,其中包含指向“人与宠物”的链接。它可以返回如下所示的资源:

<Entity type="ResourceList">
    <Link rel="people" href="http://example.com/api/people" />
    <Link rel="pets" href="http://example.com/api/pets" />
</Entity>

因此,仅了解API的第一个入口点,而不处理任何URL来找出系统标识符,我们可以执行以下操作:

用户登录到应用程序。REST客户端访问可用的人力资源的完整列表,如下所示:

<Entity type="Person">
    <Link rel="self" href="http://example.com/api/people/1" />
    <Pets>
        <Link rel="pet" href="http://example.com/api/pets/1" />
        <Link rel="pet" href="http://example.com/api/pets/2" />
    </Pets>
    <UniqueName>John</UniqueName>
</Entity>
<Entity type="Person">
    <Link rel="self" href="http://example.com/api/people/2" />
    <Pets>
        <Link rel="pet" href="http://example.com/api/pets/3" />
    </Pets>
    <UniqueName>Jane</UniqueName>
</Entity>

GUI将遍历每个资源,并使用UniqueName作为“ id”为每个人打印出一个列表项:

<a href="http://example.com/gui/people/1">John</a>
<a href="http://example.com/gui/people/2">Jane</a>

在执行此操作时,它还可以处理找到的每个带有“ pet”的链接,并获取pet资源,例如:

<Entity type="Pet">
    <Link rel="self" href="http://example.com/api/pets/1" />
    <Link rel="owner" href="http://example.com/api/people/1" />
    <UniqueName>Spot</UniqueName>
</Entity>

使用它可以打印链接,例如:

<!-- Assumes that a pet can exist without an owner -->
<a href="http://example.com/gui/pets/Spot">Spot</a>

要么

<!-- Assumes that a pet MUST have an owner -->
<a href="http://example.com/gui/people/John/pets/Spot">Spot</a>

如果我们使用第一个链接,并假设我们的条目资源具有一个链接为“ pets”的链接,则控制流在GUI中将如下所示:

  1. 打开页面,并请求宠物点。
  2. 从API入口点加载资源列表。
  3. 加载与术语“宠物”相关的资源。
  4. 浏览“宠物”响应中的每个资源,找到与Spot匹配的资源。
  5. 显示现场信息。

使用第二个链接将是类似的事件链,除了People是API的入口点,我们首先将获得系统中所有人员的列表,找到匹配的人员,然后找到所有属于的宠物给那个人(再次使用rel标签)并找到一个名为Spot的人,这样我们就可以显示与之相关的特定信息。


谢谢,迈克。我已经更新了我的问题,使其更加清晰。您回答的问题是,我不同意REST客户端可以解析URL。如果是这样,那么它将与URL耦合。这违反了REST的核心思想之一:客户端应使用rels来选择链接,但不应假定对URL的结构有任何了解。REST断言只要rels保持不变,API可以随意更改URL 。解析URL使我们更接近SOAP,而不是REST。
Pavel Gatilov

再次感谢你。您已经描述了到目前为止我们采用的方法。在某种程度上,我们确实公开了标识符。唯一的是,我们尽可能地公开自然标识符。
Pavel Gatilov

6

这是否意味着RESTful API不能用作Web应用程序的后端?

我挑战区分REST API和Web应用程序是否值得。您的“ Web应用程序”应该只是相同资源的备用(HTML)表示-也就是说,我不理解您期望如何或为什么访问http://my.rest.api/...http://my.web.app/...并且它们同时是相同和不同的。

在这种情况下,您的“客户端”是浏览器,并且可以理解HTML和JavaScript。我认为那 Web应用程序。现在,您可能会不同意并认为您使用foo.com访问了该Web应用程序,并通过api.foo.com公开了其他所有内容-但是您不得不问,foo.com是如何为我提供资源表示的?foo.com的“后端”完全能够理解如何从api.foo.com发现资源。您的Web应用程序仅成为了代理-与您一起(与其他人)与另一个API进行交谈没有什么不同。

因此,您的问题可以概括为:“如何使用其他系统中存在的自己的URI描述资源?” 当您认为不是客户端(HTML / JavaScript)必须了解如何执行此操作时,这是微不足道的,而是服务器。如果您同意我的第一个挑战,那么您可以简单地将Web应用程序视为代理或委托给另一个REST API的单独REST API。

因此,当您的客户端访问my.web.app/pets/1它时,它知道呈现pet接口,因为这是服务器端模板返回的内容,或者是对其他表示形式(例如JSON或XML)的异步请求,因此content-type标头告诉它。

提供此服务的服务器是负责了解什么是宠物以及如何在远程系统上发现宠物的服务器。如何执行此操作取决于您-您可以简单地获取ID并生成另一个URI(您认为不合适),或者您可以拥有自己的数据库来存储远程URI并代理请求。存储此URI很好-相当于添加书签。您要做的只是拥有一个单独的域名。老实说,我不知道为什么要这么做-您的REST API URI也必须具有书签功能。

您已经在问题中提到了大部分内容,但是我觉得您以一种确实不承认这是做自己想做的事的实际方式(根据我的感觉是任意约束-API和应用程序是分开的)。通过询问REST API是否不能作为Web应用程序的后端,并暗示性能会成为问题,我认为您将所有精力都放在了错误的事情上。这就像说您无法创建混搭。这就像说网络无法正常工作。


我不希望Web应用程序简单地代表api。它可能有很多差异,例如,在单个页面上显示几个子资源以及一些根资源。我不希望Web应用程序的URL包含api数据存储的内部ID,如果您是说我希望两个系统相同,则表示不希望这样做。我不关心这里的性能,这不是问题。问题实际上是“如何在my.web.app/pets/3不解析REST API Urls的情况下放入3 ”?
Pavel Gatilov 2013年

更正自己的措辞:“如何在my.web.app/pets/3不解析相应REST API资源的URL的情况下放入3 my.rest.api/v0/persons/2/pets/3?还是我放在那儿?
Pavel Gatilov 2013年

我认为您将客户端的状态与确定状态的表示相混淆。您不输入内容3app/pets/3因为app/pets/3它是不透明的,它指向您的Web应用程序想要的任何资源。如果这是其他几种资源的组合视图(在其他系统中-您的API是其中的一个),则由您决定将指向这些系统的超链接存储在Web应用服务器中,然后检索它们,将它们解析为它们的表示形式(例如JSON或XML),然后将其作为响应的一部分。
2013年

以这种方式思考-忘记您的API和应用程序。假设您想创建一个网站,使人们可以收集自己喜欢的Facebook和Twitter帖子。这些是远程系统。您不会尝试通过自己的通道将URI隧道化或模板化到这些系统。您将创建一个“板”资源,并且服务器将知道board/1指向该资源,facebook.com/post/123并且twitter.com/status/789-当您提供板的表示形式时,必须将这些URI解析为可以使用的表示形式。在需要的地方缓存。
Doug

因此,由于您希望自己的API与您的应用程序有显着不同(我仍然认为这是有问题的)-像对待远程系统一样对待它也没有什么不同。您说性能不是问题,但您在问题中也说类似的事情会“影响性能”。
2013年

5

前言

该答案专门解决了以下问题:如何管理自己的URL方案,包括后端REST API不会显式公开其标识符的资源的唯一可标记书签的URL,并且不解释API提供的URL。


可发现性需要一定的知识,所以这是我对现实世界的看法:

假设我们想要一个搜索页面,http://my.web.app/person其中的结果包括每个人的详细信息页面的链接。为了完全执行任何操作,前端代码必须知道的一件事是其REST数据源的基本URL :http://my.rest.api/api。对此URL的GET请求的响应可能是:

<Links>
    <Link ref="self" href="http://my.rest.api/api" />
    <Link rel="person" href="http://my.rest.api/api/person" />
    <Link rel="pet" href="http://my.rest.api/api/pet" />
</Links>

由于我们的意图是显示人员列表,因此我们接下来GETperson链接href 向href 发送请求,该请求可能返回:

<Links>
    <Link ref="self" href="http://my.rest.api/api/person" />
    <Link rel="search" href="http://my.rest.api/api/person/search" />
</Links>

我们想要显示搜索结果,因此我们将通过向链接href 发送GET请求来使用搜索服务,该search链接可能返回:

<Persons>
    <Person>
        <Links>
            <Link rel="self" href="http://my.rest.api/api/person/1"/>
        </Links>
        <Pets>
            <Link rel="pet" href="http://my.rest.api/api/pet/10"/>
        </Pets>
    </Person>
    <Person>
        <Links>
            <Link rel="self" href="http://my.rest.api/api/person/2"/>
        </Links>
        <Pets>
            <Link rel="pet" href="http://my.rest.api/api/pet/20"/>
        </Pets>
    </Person>
</Persons>

我们终于有了结果,但是我们如何构建前端URL?

让我们剥离我们确定的部分:API基本URL,然后将其余部分用作我们的前端标识符:

  • 已知的API基础: http://my.rest.api/api
  • 给定单个实体的URL: http://my.rest.api/api/person/1
  • 唯一身份: /person/1
  • 我们的基本网址: http://my.web.app
  • 我们生成的前端URL: http://my.web.app/person/1

我们的结果可能如下所示:

<ul>
    <li><a href="http://my.web.app/person/1">A person</a></li>
    <li><a href="http://my.web.app/person/2">A person</a></li>
</ul>

一旦用户点击该前端链接到详细信息页面,我们将把该GET特定细节的请求发送到哪个URL person?我们知道将后端URL映射到前端URL的方法,因此我们将其反向:

  • 前端网址: http://my.web.app/person/1
  • 我们的基本网址: http://my.web.app
  • 唯一身份: /person/1
  • 已知的API基础: http://my.rest.api/api
  • 生成的API URL: http://my.rest.api/api/person/1

如果REST API更改person为现在http://my.rest.api/api/different-person-base/person/1已URL,并且以前有人添加了书签http://my.web.app/person/1,则REST API应该(至少一段时间)通过使用重定向到新URL的旧URL来提供向后兼容性。所有生成的前端链接将自动包括新结构。

您可能已经注意到,要浏览API,我们需要了解几件事:

  • API基本网址
  • person关系
  • search关系

我不认为这有什么问题。我们在任何时候都不会采用特定的URL结构,因此实体URL的结构http://my.rest.api/api/person/1可能会发生变化,并且只要API提供向后兼容性,我们的代码仍然可以使用。


您问我们的路由逻辑如何分辨两个前端URL之间的区别:

  • http://my.rest.api/api/person/1
  • http://my.rest.api/api/pet/3

首先,我要指出的是,在我们的示例中,当我们为UI和REST API使用单独的基本URL时,您在注释中使用了API基础。我将继续使用单独的基准示例,但是共享基准不是问题。我们可以(或应该能够)使用请求的Accept标头中的媒体类型映射UI路由方法。

至于路由到特定的详细信息页面,如果我们严格避免避免self对API提供的URL 的结构有任何了解(即不透明的字符串ID),则无法区分这两个URL 。为了使这项工作有效,让我们在前端URL中包含另一条我们已知的信息-我们正在使用的实体类型。

以前,我们的前端URL的格式为: ${UI base}/${opaque string id}

新格式可能是: ${UI base}/${entity type}/${opaque string id}

因此,使用该/person/1示例,我们最终得到了http://my.web.app/person/person/1

使用这种格式,我们的UI路由逻辑将与一起使用/person/person/1,并且知道字符串中的第一个标记是我们自己插入的,我们可以将其拉出并路由到基于它的适当的(在此示例中为人员)详细信息页面。如果您对该网址感到不满意,可以在其中插入更多内容;也许: http://my.web.app/person/detail/person/1

在这种情况下,我们将解析出/person/detailfor路由,并将其余部分用作不透明的字符串ID。


我认为这将网络应用程序与api紧密耦合。

我猜您的意思是,由于我们生成的前端URL包含API URL的一部分,因此,如果API URL结构在不支持旧结构的情况下发生了更改,则我们需要进行代码更改才能将加书签的URL转换为API URL的新版本。换句话说,如果REST API更改了资源的ID(不透明的字符串),那么我们将无法使用旧的ID与服务器谈论该资源。我认为在这种情况下我们无法避免代码更改。

如果我希望Web应用程序的URL结构与api不同,该怎么办?

您可以使用任何所需的URL结构。归根结底,特定资源的可添加书签的URL必须包含一些内容,您可以使用这些内容来获取唯一标识该资源的API URL。如果您按照方法3的方式生成自己的标识符并使​​用API​​ URL进行缓存,则该方法将一直有效,直到有人从缓存中清除了该条目之后才尝试使用该带有书签的URL。

如果我的Web应用程序的实体未映射到api实体1-1,该怎么办?

答案取决于关系。无论哪种方式,您都需要一种将前端映射到API URL的方法。


我对这种方法有一个问题。实际上,这是我的解决方案列表中的第一名。我没有得到的是:如果Web应用程序不解释URL并将唯一的ID视为不透明的字符串(只是person/1pet/3),那么它将如何知道如果浏览器打开,http://my.rest.api/api/person/1它应该显示人员界面,以及打开http://my.rest.api/api/pet/3,然后打开宠物界面?
Pavel Gatilov

好问题!我已经用回答更新了答案。
Mike Partridge

谢谢,迈克。我认为这将网络应用程序与api紧密耦合。如果我希望Web应用程序的URL结构与api不同,该怎么办?如果我的Web应用程序的实体未映射到api实体1-1,该怎么办?我仍然认为我最好采用公开一些标识符的方法,但是敦促客户使用链接进行导航。
Pavel Gatilov

这是一个有趣的话题,所以我希望我不要错过任何东西。我已经通过回复您的评论来更新答案。我认为公开一些标识符是完全RESTful和可用性之间的良好折衷。
Mike Partridge

我在这里的主要关注点是更实际的。我使用ASP.NET MVC来实现该Web应用程序,由于一些内部规则,我必须定义该应用程序支持的网址格式。也就是说,如果定义了/ a / {id},则该应用将处理/ a / 1,但不会处理/ a / 1 / b / 2。如果REST API URL发生更改,不仅要保留加书签的URL,而且从根导航时仅使Web应用能够正常工作,这就导致需要重新编译Web应用。仅仅是因为嵌入html页面的超链接将无法正常工作。
帕维尔·加蒂洛夫

2

面对现实吧,没有神奇的解决方案。你读过理查森成熟度模型吗?它将REST体系结构的成熟度分为3个级别:资源,HTTP动词和超媒体控件。

我不应该公开实体的原始标识符,而是返回带有rel =“ self”的超链接

这是超媒体控件。您真的需要吗?这种方法有一些很好的好处(您可以在此处阅读有关它们的信息)。但是,没有免费的饭菜,要想得到免费的饭菜,就得加倍努力(例如,第二种解决方案)。

这是一个平衡的问题-您是否要牺牲性能(并使您的代码更复杂),但要获得一个更灵活的系统?还是您想让事情更快,更简单,但在对api /模型进行更改时却需要稍后付款?

作为开发类似系统(业务逻辑层,Web层和Web客户端)的人,我选择了第二个选项。自从我的团队开发了所有层之后,我们决定最好进行一些耦合(通过让Web层了解实体ID和构建api url),然后获取更简单的代码。向后兼容性在我们的案例中也无关紧要。

如果该Web应用程序是由第三方开发的,或者如果向后兼容是一个问题,则我们可能选择了不同的方法,因为这样在不更改Web应用程序的情况下能够更改url结构具有很大的价值。足以证明使代码复杂化。

这是否意味着RESTful API不能用作Web应用程序的后端?

我认为这意味着您不必创建完美的REST实现。您可以选择第二种解决方案,或者公开实体ID或通过api urls。只要您了解其含义和权衡点就可以了。


0

如果你坚持类似的东西,那Atom Syndication Format我很好。

在这里,可以使用其他元素/属性来指定描述要表示的条目的元数据:

  • 根据[RFC4287],包含唯一标识条目的URI

  • 根据[RFC4287],此元素是可选的。如果包含,则它包含客户端应用于检索条目的URI。

这只是我的两分钱。


也许我没有得到什么,但是在我看来,您的答案并未解释如何生成作为REST API客户端的Web应用程序的URL,对吗?
Pavel Gatilov

0

不用担心URL,不用担心媒体类型。

参见此处(尤其是第三个要点)。

REST API应该将其几乎所有的描述工作都花在定义用于表示资源和驱动应用程序状态的媒体类型上,或在为现有标准媒体类型定义扩展关系名称和/或启用超文本的标记上。 。


在典型的Web应用程序中,客户端是人类。浏览器只是一个代理

所以锚标签就像

          <a href="example.com/foo/123">click here</a>

对应于类似

          <link type="text/html" rel="self" href="example.com/foo/123">

该URL对用户仍然是不透明的,她只关心媒体类型(例如text/html, application/pdf, application/flv, video/x-flv, image/jpeg, image/funny-cat-picture etc)。锚点(和title属性中)所包含的描述性文本只是一种以人类可理解的方式扩展关系类型的方式。

您希望URI对客户端不透明的原因是为了减少耦合(REST的主要目标之一)。服务器可以在不影响客户端的情况下更改/重组URI(只要您具有良好的缓存策略-这可能意味着根本没有缓存)。

综上所述

只需确保客户端(人或机器)关心媒体类型和关系,而不是关心URL,就可以了。


Rodrick,我的问题不是构建API,而是构建基于RESTful API的Web应用程序。我几乎不了解媒体类型如何帮助我为Web应用程序构建URL。尽管媒体类型对于服务合同和可发现性至关重要。
Pavel Gatilov

@PavelGatilov-您的网络应用程序的客户端是人类吗?
Rodrick Chapman

是的。还有一个很不熟练的人。
Pavel Gatilov

0

最简单的方法可能只是使用资源的API URL作为它们的字符串标识符。但是像http://my.web.app/person/http%3A%2F%2Fmy.rest.api%2Fapi%2Fperson%2F1234这样的网页网址很难看。

我认为您是对的,这是最简单的方法。您可以相对URL相对化,http://my.rest.api/api以减少它们的丑陋程度:

http://my.web.app/person/person%2F1234

如果API提供的URL并非恰好是相对于该URL的,它将降级为难看的形式:

http://my.web.app/person/http%3A%2F%2Fother.api.host%2Fapi%2Fperson%2F1234

要更进一步,请检查API服务器的响应以确定您要呈现的视图类型,并停止对路径段定界符和冒号进行编码:

http://my.web.app/person/1234 (best case)
http://my.web.app/http://other.api.host/api/person/1234 (ugly case)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.