在 上一节 中,您了解了如何使用 Spring Data REST 建立后端工资单服务来存储员工数据。它缺少的一个关键功能是使用超媒体控件和链接导航。相反,它硬编码了查找数据的路径。
随意从这个存储库中 获取代码 并继续操作。此会话基于上一个会话的应用程序,添加了额外的东西。
一开始是数据……然后是 REST。
我对将任何基于 HTTP 的接口称为 REST API 的人数感到沮丧。今天的示例是 SocialSite REST API。那就是RPC。它尖叫着 RPC……需要做些什么来使 REST 架构风格在超文本是一种约束的概念上变得清晰?换句话说,如果应用程序状态引擎(以及 API)不是由超文本驱动的,那么它就不能是 RESTful,也不能是 REST API。时期。是否有一些损坏的手册需要修复?
— 罗伊·T·菲尔丁
那么,究竟什么是超媒体控件,即超文本,您如何使用它们?为了找出答案,让我们退后一步,看看 REST 的核心使命。
REST 的概念是借用使网络如此成功的想法并将它们应用于 API。尽管 web 规模巨大、动态特性和客户端(即浏览器)更新率低,但 web 取得了惊人的成功。 Roy Fielding 试图使用它的一些约束和特性,看看是否可以提供类似的 API 生产和消费扩展。
约束之一是限制动词的数量。对于 REST,主要的是 GET、POST、PUT、DELETE 和 PATCH。还有其他的,但我们不会在这里讨论它们。
- GET - 在不改变系统的情况下获取资源的状态
- POST - 创建一个新资源而不说在哪里
- PUT - 替换现有资源,覆盖已经存在的任何其他资源(如果有的话)
- DELETE - 删除现有资源
- PATCH - 部分改变现有资源
这些是具有良好编写规范的标准化 HTTP 动词。通过挑选和使用已经创造的 HTTP 操作,我们不必发明一种新语言和教育行业。
REST 的另一个约束是使用媒体类型来定义数据格式。与其每个人都写自己的方言来交换信息,不如开发一些媒体类型是明智的。最流行的一种被接受的是HAL,媒体类型application/hal+json。它是 Spring Data REST 的默认媒体类型。一个敏锐的价值是 REST 没有集中的、单一的媒体类型。相反,人们可以开发媒体类型并将其插入。尝试一下。随着不同需求的出现,行业可以灵活移动。
REST 的一个关键特性是包含指向相关资源的链接。例如,如果您正在查看订单,RESTful API 将包含指向相关客户的链接、指向商品目录的链接,可能还包括指向下订单的商店的链接。在本课程中,您将介绍分页,并了解如何使用导航分页链接。
从后台开启分页
要开始使用前端超媒体控件,您需要打开一些额外的控件。 Spring Data REST 提供分页支持。要使用它,只需调整存储库定义:
src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
您的界面现在扩展了
PagingAndSortingRepository
,它添加了额外的选项来设置页面大小,还添加了导航链接以从一个页面跳到另一个页面。后端的其余部分是相同的(除了一些
额外的预加载数据
以使事情变得有趣)。
重新启动应用程序 (
./mvnw spring-boot:run
) 并查看它是如何工作的。
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
默认页面大小为 20,因此要查看实际效果,应用
?size=2
。正如预期的那样,只列出了两名员工。此外,还有一个
first
、
next
和
last
链接。还有一个
自我
链接,没有上下文,
包括页面参数
。
如果导航到 下一个 链接,您还会看到 上 一个链接:
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
笔记 | 在 URL 查询参数中使用“&”时,命令行认为这是一个换行符。用引号将整个 URL 包起来以绕过它。 |
这看起来很整洁,但是当你更新前端以利用它时它会更好。
按关系导航
就是这样!无需对后端进行更多更改即可开始使用 Spring Data REST 提供的开箱即用的超媒体控件。您可以切换到在前端工作。 (这是 Spring Data REST 的优点之一。没有混乱的控制器更新!)
笔记 | 需要指出的是,这个应用程序不是“特定于 Spring Data REST 的”。相反,它使用 HAL 、 URI Templates 和其他标准。这就是使用 rest.js 如此简单的原因:该库带有 HAL 支持。 |
在上一个会话中,您将路径硬编码到
/api/employees
。相反,您应该硬编码的唯一路径是根目录。
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
使用一个方便的小
follow()
函数
,您现在可以从根开始并导航到您需要的地方!
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
在上一个会话中,加载是直接在
componentDidMount()
内部完成的。在此会话中,我们可以在更新页面大小时重新加载整个员工列表。为此,我们已将内容移至
loadFromServer()
中。
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
loadFromServer
与之前的会话非常相似,但 if 使用的
follow()
:
-
follow() 函数的第一个参数是用于进行 REST 调用的
client
对象。 - 第二个参数是开始的根 URI。
- 第三个参数是要导航的关系数组。每个都可以是字符串或对象。
关系数组可以像
["employees"]
一样简单,这意味着在进行第一次调用时,在
_links
中查找名为
employees
的关系(或
rel
)。找到它的
href
并导航到它。如果数组中还有其他关系,则冲洗并重复。
有时,一个 rel 本身是不够的。在这段代码中,它还插入了一个查询参数 ?size=<pageSize> 。还可以提供其他选项,您将在后面看到。
抓取 JSON 模式元数据
使用基于大小的查询导航到
员工
后,
employeeCollection
就在您的指尖。在上一个会话中,我们将其称为 day 并在
<EmployeeList />
中显示该数据。今天,您正在执行另一个调用以获取一些
JSON 模式元数据
,发现
/api/profile/employees
。
可以自己看数据:
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
笔记 | /profile/employees 中元数据的默认形式是 ALPS。不过,在本例中,您使用内容协商来获取 JSON 模式。 |
通过在 `<App />` 组件的状态中捕获此信息,您可以在以后构建输入表单时充分利用它。
创造新记录
配备此元数据后,您现在可以向 UI 添加一些额外的控件。创建一个新的 React 组件
<CreateDialog />
。
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
这个新组件既有
handleSubmit()
函数,也有预期的
render()
函数。
让我们以相反的顺序深入研究这些函数,首先看一下
render()
函数。
渲染
您的代码映射在
attributes
属性中找到的 JSON 架构数据,并将其转换为
<p><input></p>
元素的数组。
- React 再次需要 key 来区分多个子节点。
- 这是一个简单的基于文本的输入字段。
- 占位符 是我们可以向用户显示字段的地方。
- 您可能习惯于使用 名称 属性,但这不是必需的。对于 React, ref 是获取特定 DOM 节点的机制(您很快就会看到)。
这表示组件的动态特性,由从服务器加载数据驱动。
在该组件的顶级
<div>
中是一个锚标记和另一个
<div>
。锚标记是打开对话框的按钮。嵌套的
<div>
是隐藏的对话框本身。在此示例中,您使用的是纯 HTML5 和 CSS3。根本没有 JavaScript!您可以
看到用于显示/隐藏对话框的 CSS 代码
。我们不会在这里深入探讨。
位于
<div id="createEmployee">
内的是一个表单,您的动态输入字段列表被注入其中,然后是
创建
按钮。该按钮有一个
onClick={this.handleSubmit}
事件处理程序。这是注册事件处理程序的 React 方式。
笔记 | React 不会在每个 DOM 元素上创建一大堆事件处理程序。相反,它有一个 性能更高、更复杂的 解决方案。关键是您不必管理该基础架构,而是可以专注于编写功能代码。 |
处理用户输入
handleSubmit()
函数首先阻止事件向上冒泡。然后它使用相同的 JSON Schema attribute 特性来查找每个
<input>
使用
React.findDOMNode(this.refs[attribute])
。
this.refs
是一种通过名称获取特定 React 组件的方法。从这个意义上说,您只是获得了虚拟 DOM 组件。要获取实际的 DOM 元素,您需要使用
React.findDOMNode()
。
在遍历每个输入并构建
newEmployee
对象后,我们调用回调到新员工的
onCreate()
。这个函数位于
App.onCreate
的顶部,并作为另一个属性提供给这个 React 组件。查看顶级函数的运行方式:
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
再次使用
follow()
函数导航到执行 POST 操作的
员工
资源。在这种情况下,不需要应用任何参数,因此基于字符串的 rels 数组就可以了。在这种情况下,将返回 POST 调用。这允许下一个
then()
子句处理 POST 的结果。
新记录通常添加到数据集的末尾。由于您正在查看某个页面,因此期望新员工记录不在当前页面上是合乎逻辑的。要处理此问题,您需要获取一批应用了相同页面大小的新数据。
done()
中的最后一个子句返回该承诺。
由于用户可能希望看到新创建的员工,因此您可以使用超媒体控件并导航到 最后一个 条目。
这在我们的 UI 中引入了分页的概念。让我们接下来解决这个问题!
第一次使用基于承诺的 API? Promises 是一种启动异步操作的方式,然后注册一个函数以在任务完成时做出响应。承诺被设计成链接在一起以避免“回调地狱”。看下面的流程:
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
有关更多详细信息,请查看 有关 promises 的教程 。
promises 要记住的秘密是
then()
函数
需要
返回一些东西,无论是一个值还是另一个 promise。
done()
函数不会返回任何东西,你也不会在它后面链接任何东西。如果您还没有注意到,
client
(它是 rest.js 中
rest
的一个实例)以及
follow
函数返回承诺。
通过数据分页
您在后端设置了分页,并且在创建新员工时已经开始利用它。
在 上一节 中,您使用页面控件跳转到 最后 一页。将其动态应用到 UI 并让用户根据需要进行导航会非常方便。根据可用的导航链接动态调整控件会很棒。
首先,让我们检查一下您使用的
onNavigate()
函数。
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
这是在
App.onNavigate
内部的顶部定义的。同样,这是为了允许在顶级组件中管理 UI 的状态。将
onNavigate()
向下传递给
<EmployeeList />
React 组件后,将编写以下处理程序来处理某些按钮的点击:
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
这些函数中的每一个都会拦截默认事件并阻止它冒泡。然后它使用适当的超媒体链接调用
onNavigate()
函数。
现在根据
EmployeeList.render
的超媒体链接中出现的链接有条件地显示控件:
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
与上一个会话一样,它仍然将
this.props.employees
转换为
<Element />
组件的数组。然后它构建了一个
navLinks
数组,一个 HTML 按钮数组。
笔记 |
因为 React 基于 XML,所以不能将“<”放在
<button>
元素中。您必须改用编码版本。
|
然后您可以看到
{navLinks}
插入到返回的 HTML 的底部。
删除现有记录
删除条目要容易得多。获取其基于 HAL 的记录并将 DELETE 应用于其 自身 链接。
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
这个更新版本的 Employee 组件在行的末尾显示了一个额外的条目,一个删除按钮。它注册为在单击时调用
this.handleDelete
。然后
handleDelete()
函数可以调用向下传递的回调,同时提供上下文重要的
this.props.employee
记录。
重要的 |
这再次表明在一个地方管理顶级组件中的状态是最容易的。情况可能并非
总是
如此,但通常情况下,在一个地方管理状态可以使保持正直和简单变得更容易。通过使用特定于组件的详细信息 (
this.props.onDelete(this.props.employee)
) 调用回调,可以很容易地协调组件之间的行为。
|
将
onDelete()
函数追溯到
App.onDelete
的顶部,您可以看到它是如何运行的:
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
使用基于页面的 UI 删除记录后应用的行为有点棘手。在这种情况下,它会从服务器重新加载全部数据,并应用相同的页面大小。然后它显示第一页。
如果你是删除最后一页的最后一条记录,它会跳转到第一页。
调整页面大小
了解超媒体真正闪耀的一种方法是更新页面大小。 Spring Data REST 根据页面大小流畅地更新导航链接。
在
ElementList.render
的顶部有一个 HTML 元素:
<input ref="pageSize" defaultValue={this.props.pageSize} onInput={this.handleInput}/>
。
-
ref="pageSize"
可以很容易地通过 this.refs.pageSize 获取该元素。 -
defaultValue
使用状态的 pageSize 初始化它。 -
onInput
注册一个处理程序,如下所示。
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
它阻止事件冒泡。然后它使用
<input>
的
ref
属性找到 DOM 节点并提取它的值,所有这些都是通过 React 的
findDOMNode()
辅助函数完成的。它通过检查输入是否是一串数字来测试输入是否真的是一个数字。如果是这样,它会调用回调,将新页面大小发送到
App
React 组件。如果不是,则刚刚输入的字符会从输入中剥离。
App
在获取
updatePageSize()
时会做什么?一探究竟:
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
因为新的页面大小会导致所有导航链接发生变化,所以最好重新获取数据并从头开始。
把它们放在一起
有了所有这些不错的添加,您现在拥有了一个真正焕然一新的 UI。
您可以在顶部看到页面大小设置,在每行上看到删除按钮,在底部看到导航按钮。导航按钮说明了超媒体控件的强大功能。
在下方,您可以看到
CreateDialog
以及插入 HTML 输入占位符的元数据。
这确实展示了使用超媒体与域驱动元数据(JSON 模式)相结合的力量。网页不必知道哪个字段是哪个。相反,用户可以
看到
它并知道如何使用它。如果您将另一个字段添加到
Employee
域对象,此弹出窗口将自动显示它。
审查
在本届会议中:
- 您打开了 Spring Data REST 的分页功能。
- 您抛弃了硬编码的 URI 路径并开始使用根 URI 结合关系名称或“rels”。
- 您更新了 UI 以动态使用基于页面的超媒体控件。
- 您添加了创建和删除员工以及根据需要更新 UI 的功能。
- 您可以更改页面大小并让 UI 灵活响应。
问题?
你使网页动态。但是打开另一个浏览器选项卡并将其指向同一个应用程序。一个选项卡中的更改不会更新另一个选项卡中的任何内容。
这是我们可以在下届会议上解决的问题。在那之前,祝你编码愉快!
Greg Turnquist 的教程。