首页 > 其他分享 >React.js 和 Spring Data REST(二)

React.js 和 Spring Data REST(二)

时间:2022-12-29 14:08:11浏览次数:39  
标签:pageSize links Spring employees REST React props employee Data

React.js 和 Spring Data REST(二)_REST

第 2 部分 - 超媒体控件

在上一节,您了解了如何使用 Spring Data REST 创建后端工资单服务来存储员工数据。它缺乏的一个关键功能是使用超媒体控件和链接导航。相反,它对路径进行硬编码以查找数据。

随意获取代码从此存储库并继续操作。本节基于上一节的应用程序,并添加了额外的内容。

一开始,有数据...然后是休息

我对将任何基于 HTTP 的接口称为 REST API 的人数感到沮丧。今天的例子是SocialSite REST API。那就是RPC。它尖叫着RPC。需要做些什么来使 REST 架构风格明确超文本是一种约束的概念?换句话说,如果应用程序状态引擎(以及 API)不是由超文本驱动的,那么它就不能是 RESTful 的,也不能是 REST API。时期。是否有一些损坏的手册需要修复?

- 罗伊·T·菲尔丁
​https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven

那么,究竟什么是超媒体控件(即超文本)以及如何使用它们?为了找到答案,我们退后一步,看看REST的核心使命。

REST的概念是借用使网络如此成功的想法,并将其应用于API。尽管网络规模庞大,动态性质和客户端(即浏览器)的更新速度低,但网络取得了惊人的成功。Roy Fielding试图利用它的一些限制和功能,看看这是否能提供类似的API生产和消费扩张。

其中一个约束是限制动词的数量。对于 REST,主要的是 GET、POST、PUT、DELETE 和 PATCH。还有其他的,但我们不会在这里讨论它们。

  • GET:在不更改系统的情况下获取资源的状态
  • POST:创建一个新资源而不说明位置
  • PUT:替换现有资源,覆盖已有的任何其他资源(如果有的话)
  • 删除:删除现有资源
  • PATCH:更改现有资源(部分更改而不是创建新资源)

这些是具有众所周知规范的标准化 HTTP 动词。通过拾取和使用已经创造的HTTP操作,我们不需要发明一种新的语言并教育行业。

REST 的另一个约束是使用媒体类型来定义数据的格式。与其每个人都写自己的方言来交换信息,不如开发一些媒体类型。最受欢迎的接受之一是 HAL,媒体类型 .它是Spring Data REST的默认媒体类型。一个关键值是 REST 没有集中的单一媒体类型。相反,人们可以开发媒体类型并插入并试用它们。随着不同的需求出现,行业可以灵活地移动。​​application/hal+json​

REST 的一个关键功能是包含指向相关资源的链接。例如,如果您正在查看订单,则 RESTful API 将包含指向相关客户的链接、指向项目目录的链接,以及指向下订单的商店的链接。在本节中,您将介绍分页,并了解如何使用导航分页链接。

从后端启用分页

若要开始使用前端超媒体控件,需要打开一些额外的控件。Spring Data REST提供分页支持。要使用它,请按如下方式调整存储库定义:

例 16。src/main/java/com/greglturnquist/payroll/EmployeeRepository.java

public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {

}

您的界面现在扩展了 ,它添加了额外的选项来设置页面大小,并添加了导航链接以从一个页面跳到另一个页面。后端的其余部分是相同的(除了一些​​PagingAndSortingRepository​​额外的预加载数据让事情变得有趣)。

重新启动应用程序 () 并查看其工作原理。然后运行以下命令(与其输出一起显示)以查看分页的运行情况:​​./mvnw spring-boot:run​

$ curl "localhost:8080/api/employees?size=2"
{
"_links" : {
"first" : {
"href" : "http://localhost:8080/api/employees?page=0&size=2"
},
"self" : {
"href" : "http://localhost:8080/api/employees"
},
"next" : {
"href" : "http://localhost:8080/api/employees?page=1&size=2"
},
"last" : {
"href" : "http://localhost:8080/api/employees?page=2&size=2"
}
},
"_embedded" : {
"employees" : [ {
"firstName" : "Frodo",
"lastName" : "Baggins",
"description" : "ring bearer",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/employees/1"
}
}
}, {
"firstName" : "Bilbo",
"lastName" : "Baggins",
"description" : "burglar",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/employees/2"
}
}
} ]
},
"page" : {
"size" : 2,
"totalElements" : 6,
"totalPages" : 3,
"number" : 0
}
}

默认页面大小为 20,但我们没有那么多数据。因此,要看到它的实际效果,我们设置了.正如预期的那样,只列出了两名员工。此外,还有 、 和链接。还有链接,它没有上下文,包括页面参数。​​?size=2​​​​first​​​​next​​​​last​​​​self​

如果导航到该链接,则还将看到一个链接。以下命令(与其输出一起显示)执行此操作:​​next​​​​prev​

$ curl "http://localhost:8080/api/employees?page=1&size=2"
{
"_links" : {
"first" : {
"href" : "http://localhost:8080/api/employees?page=0&size=2"
},
"prev" : {
"href" : "http://localhost:8080/api/employees?page=0&size=2"
},
"self" : {
"href" : "http://localhost:8080/api/employees"
},
"next" : {
"href" : "http://localhost:8080/api/employees?page=2&size=2"
},
"last" : {
"href" : "http://localhost:8080/api/employees?page=2&size=2"
}
},
...

在 URL 查询参数中使用时,命令行认为它是换行符。用引号包装整个 URL 以避免该问题。​​&​

这看起来很整洁,但是当您更新前端以利用它时会更好。

按关系导航

后端不需要更多更改即可开始使用 Spring Data REST 提供的开箱即用的超媒体控件。您可以切换到在前端工作。(这是Spring Data REST的美妙之处之一:没有凌乱的控制器更新!

需要指出的是,这个应用程序不是“Spring Data REST特有的”。相反,它使用哈尔,URI 模板等标准。这就是为什么使用 rest.js 轻而易举的原因:该库带有 HAL 支持。

在上一节中,您将路径硬编码为 。相反,您应该硬编码的唯一路径是根,如下所示​​/api/employees​

...
var root = '/api';
...

用一个方便的小follow()功能,您现在可以从根目录开始并导航到所需的位置,如下所示:

componentDidMount() {
this.loadFromServer(this.state.pageSize);
}

在上一节中,加载是直接在 内部完成的。在本节中,我们可以在页面大小更新时重新加载整个员工列表。为此,我们将内容移至 ,如下所示:​​componentDidMount()​​​​loadFromServer()​

loadFromServer(pageSize) {
follow(client, root, [
{rel: 'employees', params: {size: pageSize}}]
).then(employeeCollection => {
return client({
method: 'GET',
path: employeeCollection.entity._links.profile.href,
headers: {'Accept': 'application/schema+json'}
}).then(schema => {
this.schema = schema.entity;
return employeeCollection;
});
}).done(employeeCollection => {
this.setState({
employees: employeeCollection.entity._embedded.employees,
attributes: Object.keys(this.schema.properties),
pageSize: pageSize,
links: employeeCollection.entity._links});
});
}

​loadFromServer​​​与上一节非常相似。但是,它使用:​​follow()​

  • 函数的第一个参数是用于进行 REST 调用的对象。​​follow()​​​​client​
  • 第二个参数是要从中开始的根 URI。
  • 第三个参数是要导航的关系数组。每个都可以是字符串或对象。

关系数组可以像 一样简单,这意味着在进行第一次调用时,查找名为 的关系(或)。找到它并导航到它。如果数组中存在其他关系,请重复该过程。​​["employees"]​​​​_links​​​​rel​​​​employees​​​​href​

有时,仅靠 a 是不够的。在此代码片段中,它还插入了 的查询参数。您还可以提供其他选项,稍后您将看到。​​rel​​​​?size=<pageSize>​

抓取 JSON 架构元数据

使用基于大小的查询导航到 后,可以使用。在上一节中,我们在 中显示了该数据。在本节中,您将执行另一个调用以获取一些​​employees​​​​employeeCollection​​​​<EmployeeList />​​​JSON 架构元数据在 中找到。​​/api/profile/employees/​

您可以通过运行以下命令(与其输出一起显示)自行查看数据:​​curl​

$ curl http://localhost:8080/api/profile/employees -H "Accept:application/schema+json"
{
"title" : "Employee",
"properties" : {
"firstName" : {
"title" : "First name",
"readOnly" : false,
"type" : "string"
},
"lastName" : {
"title" : "Last name",
"readOnly" : false,
"type" : "string"
},
"description" : {
"title" : "Description",
"readOnly" : false,
"type" : "string"
}
},
"definitions" : { },
"type" : "object",
"$schema" : "https://json-schema.org/draft-04/schema#"
}

元数据的默认形式为​​/profile/employees​​阿尔卑斯山.但是,在这种情况下,您将使用内容协商来获取 JSON 架构。

通过在“<App />”组件的状态中捕获此信息,您可以稍后在构建输入表单时充分利用它。

创建新记录

有了这些元数据,现在可以向 UI 添加一些额外的控件。你可以从创建一个新的 React 组件开始,如下所示:​​<CreateDialog />​

class CreateDialog extends React.Component {

constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}

handleSubmit(e) {
e.preventDefault();
const newEmployee = {};
this.props.attributes.forEach(attribute => {
newEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim();
});
this.props.onCreate(newEmployee);

// clear out the dialog's inputs
this.props.attributes.forEach(attribute => {
ReactDOM.findDOMNode(this.refs[attribute]).value = '';
});

// Navigate away from the dialog to hide it.
window.location = "#";
}

render() {
const inputs = this.props.attributes.map(attribute =>
<p key={attribute}>
<input type="text" placeholder={attribute} ref={attribute} className="field"/>
</p>
);

return (
<div>
<a href="#createEmployee">Create</a>

<div id="createEmployee" className="modalDialog">
<div>
<a href="#" title="Close" className="close">X</a>

<h2>Create new employee</h2>

<form>
{inputs}
<button notallow={this.handleSubmit}>Create</button>
</form>
</div>
</div>
</div>
)
}

}

这个新组件同时具有函数和预期函数。​​handleSubmit()​​​​render()​

我们以相反的顺序深入研究这些函数,首先看函数。​​render()​

渲染

代码映射在属性中找到的 JSON 架构数据,并将其转换为元素数组。​​attributes​​​​<p><input></p>​

  • ​key​​再次被 React 需要来区分多个子节点。
  • 这是一个简单的基于文本的输入字段。
  • ​placeholder​​让我们向用户显示字段是哪个。
  • 您可能习惯于拥有属性,但这不是必需的。使用 React,是抓取特定 DOM 节点的机制(你很快就会看到)。​​name​​​​ref​

这表示组件的动态性质,由从服务器加载数据驱动。

此组件的顶层内部是一个锚标记和另一个 .锚标记是用于打开对话框的按钮。嵌套的是隐藏对话框本身。在此示例中,您使用的是纯 HTML5 和 CSS3。完全没有JavaScript!您可以​​<div>​​​​<div>​​​​<div>​​请参阅CSS代码用于显示和隐藏对话框。我们不会在这里深入探讨。

嵌套在窗体中的窗体中注入了输入字段的动态列表,然后是“创建”按钮。该按钮具有事件处理程序。这是注册事件处理程序的 React 方式。​​<div id="createEmployee">​​​​notallow={this.handleSubmit}​

React 不会在每个 DOM 元素上创建事件处理程序。相反,它有一个性能更高,更复杂溶液。您不需要管理该基础结构,而是可以专注于编写功能代码。

处理用户输入

该函数首先阻止事件在层次结构中进一步冒泡。然后,它使用相同的 JSON 架构属性属性来查找每个 ,方法是使用 。​​handleSubmit()​​​​<input>​​​​React.findDOMNode(this.refs[attribute])​

​this.refs​​​是一种按名称获取特定 React 组件的方法。请注意,您只获得虚拟 DOM 组件。要获取实际的 DOM 元素,您需要使用 .​​React.findDOMNode()​

在遍历每个输入并构建对象后,我们为新员工记录调用回调。这个函数在里面,并作为另一个属性提供给这个 React 组件。看看这个顶级函数是如何运作的:​​newEmployee​​​​onCreate()​​​​App.onCreate​

onCreate(newEmployee) {
follow(client, root, ['employees']).then(employeeCollection => {
return client({
method: 'POST',
path: employeeCollection.entity._links.self.href,
entity: newEmployee,
headers: {'Content-Type': 'application/json'}
})
}).then(response => {
return follow(client, root, [
{rel: 'employees', params: {'size': this.state.pageSize}}]);
}).done(response => {
if (typeof response.entity._links.last !== "undefined") {
this.onNavigate(response.entity._links.last.href);
} else {
this.onNavigate(response.entity._links.self.href);
}
});
}

再一次,我们使用该函数导航到执行 POST 操作的资源。在这种情况下,不需要应用任何参数,因此基于字符串的实例数组很好。在此情况下,将返回调用。这允许下一个子句处理 .​​follow()​​​​employees​​​​rel​​​​POST​​​​then()​​​​POST​

新记录通常会添加到数据集的末尾。由于您正在查看某个页面,因此期望新员工记录不在当前页面上是合乎逻辑的。要处理此问题,您需要获取应用了相同页面大小的新一批数据。该承诺在里面的最后一个条款中返回。​​done()​

由于用户可能希望查看新创建的员工,因此您可以使用超媒体控件并导航到该条目。​​last​

首次使用基于承诺的 API?承诺是一种启动异步操作,然后注册函数以在任务完成时响应的方法。承诺被设计为链接在一起以避免“回调地狱”。请看以程:

when.promise(async_func_call())
.then(function(results) {
/* process the outcome of async_func_call */
})
.then(function(more_results) {
/* process the previous then() return value */
})
.done(function(yet_more) {
/* process the previous then() and wrap things up */
});

有关更多详细信息,请查看这个关于承诺的教程.

关于 promise 要记住的秘密是,函数需要返回一些东西,无论是值还是另一个 promise。 函数不返回任何内容,并且您不会在 1 之后链接任何内容。如果你还没有注意到,(这是 from rest.js 的一个实例)和函数返回 promise。​​then()​​​​done()​​​​client​​​​rest​​​​follow​

分页数据

您已经在后端设置了分页,并且在创建新员工时已经开始利用它。

在上一节,您使用页面控件跳转到页面。将其动态应用于 UI 并让用户根据需要导航非常方便。根据可用的导航链接动态调整控件会很棒。​​last​

首先,让我们看看您使用的函数:​​onNavigate()​

onNavigate(navUri) {
client({method: 'GET', path: navUri}).done(employeeCollection => {
this.setState({
employees: employeeCollection.entity._embedded.employees,
attributes: this.state.attributes,
pageSize: this.state.pageSize,
links: employeeCollection.entity._links
});
});
}

这是在顶部的 .同样,这是为了允许在顶部组件中管理 UI 的状态。在传递给 React 组件后,以下处理程序被编码以处理某些按钮的单击:​​App.onNavigate​​​​onNavigate()​​​​<EmployeeList />​

handleNavFirst(e){
e.preventDefault();
this.props.onNavigate(this.props.links.first.href);
}

handleNavPrev(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.prev.href);
}

handleNavNext(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.next.href);
}

handleNavLast(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.last.href);
}

这些函数中的每一个都截获默认事件并阻止它冒泡。然后,它使用正确的超媒体链接调用该函数。​​onNavigate()​

现在,您可以根据以下超媒体链接中显示的链接有条件地显示控件:​​EmployeeList.render​

render() {
const employees = this.props.employees.map(employee =>
<Employee key={employee._links.self.href} employee={employee} notallow={this.props.onDelete}/>
);

const navLinks = [];
if ("first" in this.props.links) {
navLinks.push(<button key="first" notallow={this.handleNavFirst}><<</button>);
}
if ("prev" in this.props.links) {
navLinks.push(<button key="prev" notallow={this.handleNavPrev}><</button>);
}
if ("next" in this.props.links) {
navLinks.push(<button key="next" notallow={this.handleNavNext}>></button>);
}
if ("last" in this.props.links) {
navLinks.push(<button key="last" notallow={this.handleNavLast}>>></button>);
}

return (
<div>
<input ref="pageSize" defaultValue={this.props.pageSize} notallow={this.handleInput}/>
<table>
<tbody>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Description</th>
<th></th>
</tr>
{employees}
</tbody>
</table>
<div>
{navLinks}
</div>
</div>
)
}

与上一节一样,它仍会转换为组件数组。然后它构建一个数组作为 HTML 按钮数组。​​this.props.employees​​​​<Element />​​​​navLinks​

因为 React 是基于 XML 的,所以你不能把元素放进去。必须改用编码版本。​​<​​​​<button>​

然后,您可以看到插入到返回的HTML的底部。​​{navLinks}​

删除现有记录

删除条目要容易得多。您需要做的就是获取其基于 HAL 的记录并应用于其链接:​​DELETE​​​​self​

class Employee extends React.Component {

constructor(props) {
super(props);
this.handleDelete = this.handleDelete.bind(this);
}

handleDelete() {
this.props.onDelete(this.props.employee);
}

render() {
return (
<tr>
<td>{this.props.employee.firstName}</td>
<td>{this.props.employee.lastName}</td>
<td>{this.props.employee.description}</td>
<td>
<button onClick={this.handleDelete}>Delete</button>
</td>
</tr>
)
}
}

此更新版本的 Employee 组件在行末尾显示一个额外的条目(删除按钮)。它被注册为在单击时调用。然后,该函数可以调用传递的回调,同时提供上下文重要的记录。​​this.handleDelete​​​​handleDelete()​​​​this.props.employee​

这再次表明,在一个地方管理顶部组件中的状态是最容易的。情况可能并非总是如此。但是,通常,在一个位置管理状态可以更轻松地保持简单。通过使用特定于组件的详细信息 () 调用回调,非常容易编排组件之间的行为。​​this.props.onDelete(this.props.employee)​

通过将函数追溯到顶部,您可以看到它是如何工作的:​​onDelete()​​​​App.onDelete​

onDelete(employee) {
client({method: 'DELETE', path: employee._links.self.href}).done(response => {
this.loadFromServer(this.state.pageSize);
});
}

使用基于页面的 UI 删除记录后要应用的行为有点棘手。在这种情况下,它会从服务器重新加载整个数据,应用相同的页面大小。然后它显示第一页。

如果要删除最后一页上的最后一条记录,它将跳转到第一页。

调整页面大小

了解超媒体如何真正发光的一种方法是更新页面大小。Spring Data REST 根据页面大小流畅地更新导航链接。

在 的顶部有一个 HTML 元素: 。​​ElementList.render​​​​<input ref="pageSize" defaultValue={this.props.pageSize} onInput={this.handleInput}/>​

  • ​ref="pageSize"​​使使用 轻松获取该元素。this.refs.pageSize
  • ​defaultValue​​使用状态的 .pageSize
  • ​onInput​​注册处理程序,如下所示:


handleInput(e) {
e.preventDefault();
const pageSize = ReactDOM.findDOMNode(this.refs.pageSize).value;
if (/^[0-9]+$/.test(pageSize)) {
this.props.updatePageSize(pageSize);
} else {
ReactDOM.findDOMNode(this.refs.pageSize).value =
pageSize.substring(0, pageSize.length - 1);
}
}

它可以阻止事件冒泡。然后它使用 的属性来查找 DOM 节点并提取其值,所有这些都是通过 React 的辅助函数完成的。它通过检查输入是否是一串数字来测试输入是否真的是一个数字。如果是这样,它会调用回调,将新的页面大小发送到 React 组件。否则,刚输入的字符将从输入中删除。​​ref​​​​<input>​​​​findDOMNode()​​​​App​

当它得到一个 ?看看吧:​​App​​​​updatePageSize()​

updatePageSize(pageSize) {
if (pageSize !== this.state.pageSize) {
this.loadFromServer(pageSize);
}
}

由于新的页面大小值会导致所有导航链接发生更改,因此最好重新获取数据并从头开始。

将一切整合在一起

有了所有这些不错的补充,你现在有一个真正被吸干的 UI,如下图所示:

React.js 和 Spring Data REST(二)_控件_02

您可以在顶部看到页面大小设置,每行上的删除按钮以及底部的导航按钮。导航按钮演示了超媒体控件的强大功能。

在下图中,您可以看到将元数据插入到 HTML 输入占位符中:​​CreateDialog​

React.js 和 Spring Data REST(二)_REST_03

这确实显示了使用超媒体与域驱动的元数据(JSON 模式)相结合的强大功能。网页不必知道哪个字段是哪个字段。相反,用户可以看到它并知道如何使用它。如果将其他字段添加到域对象,此弹出窗口将自动显示该字段。​​Employee​

回顾

在本节中:

  • 您打开了 Spring Data REST 的分页功能。
  • 你抛弃了硬编码的 URI 路径,并开始使用根 URI 和关系名称或“rels”。
  • 您更新了 UI 以动态使用基于页面的超媒体控件。
  • 你添加了创建和删除员工以及根据需要更新 UI 的功能。
  • 您可以更改页面大小并让 UI 灵活响应。

问题?

您使网页动态化。但是打开另一个浏览器选项卡并将其指向同一应用程序。一个选项卡中的更改不会更新另一个选项卡中的任何内容。

我们将在下一节中讨论该问题。

第 3 部分 - 条件操作

在上一节,您了解了如何打开 Spring Data REST 的超媒体控件,如何让 UI 通过分页进行导航,以及如何根据更改页面大小动态调整大小。您添加了创建和删除员工以及调整页面的功能。但是,如果不考虑其他用户对您当前正在编辑的相同数据所做的更新,任何解决方案都是不完整的。

随意获取代码从此存储库并继续操作。本节基于上一节,但添加了额外的功能。

放还是不放?这就是问题所在。

当您获取资源时,风险是如果其他人更新它,它可能会过时。为了解决这个问题,Spring Data REST集成了两种技术:资源版本控制和ETag。

通过在后端对资源进行版本控制并在前端使用 ETag,可以有条件地进行更改。换句话说,您可以检测资源是否已更改,并防止 (或 ) 踩踏其他人的更新。​​PUT​​​​PUT​​​​PATCH​

对 REST 资源进行版本控制

若要支持资源的版本控制,请为需要此类型保护的域对象定义版本属性。下面的清单显示了如何对对象执行此操作:​​Employee​

例 17.src/main/java/com/greglturnquist/payroll/Employee.java

@Entity
public class Employee {

private @Id @GeneratedValue Long id;
private String firstName;
private String lastName;
private String description;

private @Version @JsonIgnore Long version;

private Employee() {}

public Employee(String firstName, String lastName, String description) {
this.firstName = firstName;
this.lastName = lastName;
this.description = description;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return Objects.equals(id, employee.id) &&
Objects.equals(firstName, employee.firstName) &&
Objects.equals(lastName, employee.lastName) &&
Objects.equals(description, employee.description) &&
Objects.equals(version, employee.version);
}

@Override
public int hashCode() {

return Objects.hash(id, firstName, lastName, description, version);
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

public Long getVersion() {
return version;
}

public void setVersion(Long version) {
this.version = version;
}

@Override
public String toString() {
return "Employee{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", description='" + description + '\'' +
", version=" + version +
'}';
}
}
  • 该字段用 注释。它会导致每次插入和更新行时自动存储和更新值。versionjavax.persistence.Version

当获取单个资源(不是集合资源)时,Spring Data REST 会自动添加一个ETag 响应标头与此字段的值。

获取单个资源及其标头

在上一节,您使用集合资源来收集数据并填充 UI 的 HTML 表。使用Spring Data REST,数据集被视为数据的预览。虽然对于浏览数据很有用,但要获取像 ETag 这样的标头,您需要单独获取每个资源。​​_embedded​

在此版本中,更新为提取集合。然后,可以使用 URI 检索每个单独的资源:​​loadFromServer​

例 18。src/main/js/app.js - 获取每个资源

loadFromServer(pageSize) {
follow(client, root, [ (1)
{rel: 'employees', params: {size: pageSize}}]
).then(employeeCollection => { (2)
return client({
method: 'GET',
path: employeeCollection.entity._links.profile.href,
headers: {'Accept': 'application/schema+json'}
}).then(schema => {
this.schema = schema.entity;
this.links = employeeCollection.entity._links;
return employeeCollection;
});
}).then(employeeCollection => { (3)
return employeeCollection.entity._embedded.employees.map(employee =>
client({
method: 'GET',
path: employee._links.self.href
})
);
}).then(employeePromises => { (4)
return when.all(employeePromises);
}).done(employees => { (5)
this.setState({
employees: employees,
attributes: Object.keys(this.schema.properties),
pageSize: pageSize,
links: this.links
});
});
}

1

该函数转到集合资源。​​follow()​​​​employees​

2

第一个子句创建一个调用来提取 JSON 架构数据。它有一个内部 then 子句,用于在组件中存储元数据和导航链接。​​then(employeeCollection ⇒ …)​​​​<App/>​


请注意,此嵌入式承诺返回 .这样,集合可以传递到下一个调用,让您在此过程中获取元数据。​​employeeCollection​


3

第二个子句将员工的集合转换为一系列承诺来获取每个单独的资源。这是为每个员工获取 ETag 标头所需的内容。​then(employeeCollection ⇒ …)​​​​GET​

4

该子句采用 promise 数组,并将它们合并为一个 promise 中,当解析所有 GET 承诺时,将解析该承诺。​​then(employeePromises ⇒ …)​​​​GET​​​​when.all()​

5

​loadFromServer​​​总结使用此数据合并更新 UI 状态的位置。​​done(employees ⇒ …)​

该链也在其他地方实现。例如,(用于跳转到不同页面)已更新为获取单个资源。由于它与此处显示的内容基本相同,因此已将其排除在本节之外。​​onNavigate()​

更新现有资源

在本节中,您将添加一个 React 组件来编辑现有的员工记录:​​UpdateDialog​

例 19。src/main/js/app.js - UpdateDialog 组件

class UpdateDialog extends React.Component {

constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}

handleSubmit(e) {
e.preventDefault();
const updatedEmployee = {};
this.props.attributes.forEach(attribute => {
updatedEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim();
});
this.props.onUpdate(this.props.employee, updatedEmployee);
window.location = "#";
}

render() {
const inputs = this.props.attributes.map(attribute =>
<p key={this.props.employee.entity[attribute]}>
<input type="text" placeholder={attribute}
defaultValue={this.props.employee.entity[attribute]}
ref={attribute} className="field"/>
</p>
);

const dialogId = "updateEmployee-" + this.props.employee.entity._links.self.href;

return (
<div key={this.props.employee.entity._links.self.href}>
<a href={"#" + dialogId}>Update</a>
<div id={dialogId} className="modalDialog">
<div>
<a href="#" title="Close" className="close">X</a>

<h2>Update an employee</h2>

<form>
{inputs}
<button onClick={this.handleSubmit}>Update</button>
</form>
</div>
</div>
</div>
)
}

};

这个新组件同时具有函数和预期函数,类似于组件。​​handleSubmit()​​​​render()​​​​<CreateDialog />​

我们以相反的顺序深入研究这些函数,首先看一下函数。​​render()​

渲染

此组件使用与上一节相同的 CSS/HTML 策略来显示和隐藏对话框。​​<CreateDialog />​

它将 JSON 架构属性数组转换为 HTML 输入数组,这些输入包含在段落元素中以进行样式设置。这也与 相同,但有一个区别:字段加载了 .在组件中,字段为空。​​<CreateDialog />​​​​this.props.employee​​​​CreateDialog​

该字段的构建方式不同。整个 UI 上只有一个链接,但显示的每一行都有一个单独的链接。因此,该字段基于链接的 URI。这在元素的 React 、HTML 锚标记和隐藏弹出窗口中使用。​​id​​​​CreateDialog​​​​UpdateDialog​​​​id​​​​self​​​​<div>​​​​key​

处理用户输入

提交按钮链接到组件的功能。这很容易用于通过使用提取弹出窗口的详细信息​​handleSubmit()​​​​React.findDOMNode()​​反应引用.

提取输入值并将其加载到对象中后,将调用顶级方法。这延续了 React 的单向绑定风格,其中要调用的函数从上层组件推送到下层组件。这样,状态仍然在顶部进行管理。​​updatedEmployee​​​​onUpdate()​

有条件的看跌期权

因此,您已经竭尽全力在数据模型中嵌入版本控制。Spring Data REST已将该值作为ETag响应标头提供。这是您可以充分利用它的地方:

例 20。src/main/js/app.js - onUpdate function

onUpdate(employee, updatedEmployee) {
client({
method: 'PUT',
path: employee.entity._links.self.href,
entity: updatedEmployee,
headers: {
'Content-Type': 'application/json',
'If-Match': employee.headers.Etag
}
}).done(response => {
this.loadFromServer(this.state.pageSize);
}, response => {
if (response.status.code === 412) {
alert('DENIED: Unable to update ' +
employee.entity._links.self.href + '. Your copy is stale.');
}
});
}

A 与​​PUT​​If-Match请求标头导致 Spring Data REST 根据当前版本检查该值。如果传入的值与数据存储的版本值不匹配,Spring Data REST 将失败,并显示 .​​If-Match​​​​HTTP 412 Precondition Failed​

规格为承诺/A+​实际上将其 API 定义为 .到目前为止,您已经看到它仅与成功函数一起使用。在前面的代码片段中,有两个函数。成功函数调用 ,而错误函数显示有关过时数据的浏览器警报。​​then(successFunction, errorFunction)​​​​loadFromServer​

将一切整合在一起

定义 React 组件并将其很好地链接到顶级函数后,最后一步是将其连接到组件的现有布局中。​​UpdateDialog​​​​onUpdate​

在上一节中创建的被放在顶部,因为只有一个实例。但是,直接与特定员工相关联。所以你可以在下面的 React 组件中看到它插入:​​CreateDialog​​​​EmployeeList​​​​UpdateDialog​​​​Employee​

例 21。src/main/js/app.js - Employee with UpdateDialog

class Employee extends React.Component {

constructor(props) {
super(props);
this.handleDelete = this.handleDelete.bind(this);
}

handleDelete() {
this.props.onDelete(this.props.employee);
}

render() {
return (
<tr>
<td>{this.props.employee.entity.firstName}</td>
<td>{this.props.employee.entity.lastName}</td>
<td>{this.props.employee.entity.description}</td>
<td>
<UpdateDialog employee={this.props.employee}
attributes={this.props.attributes}
onUpdate={this.props.onUpdate}/>
</td>
<td>
<button onClick={this.handleDelete}>Delete</button>
</td>
</tr>
)
}
}

在本节中,将从使用集合资源切换到使用单个资源。员工记录的字段现在位于 。它使我们能够访问 ,在那里我们可以找到 ETag。​​this.props.employee.entity​​​​this.props.employee.headers​

Spring Data REST还支持其他标头(例如上次修改时间),不属于本系列。因此,以这种方式构建数据很方便。

和 的结构仅在使用时相关​​.entity​​​​.headers​​休息.js作为首选的 REST 库。如果您使用其他库,则必须根据需要进行调整。

看到事物的实际效果

要查看修改后的应用程序工作,请执行以下操作:

  1. 通过运行 启动应用程序。./mvnw spring-boot:run
  2. 打开浏览器选项卡并导航到http://localhost:8080.

    您应该会看到类似于下图的页面:

React.js 和 Spring Data REST(二)_API_04

  1. 拉出佛罗多的编辑对话框。
  2. 在浏览器中打开另一个选项卡并调出相同的记录。
  3. 在第一个选项卡中更改记录。
  4. 尝试在第二个选项卡中进行更改。

    您应该看到浏览器选项卡发生了变化,如下图所示

React.js 和 Spring Data REST(二)_REST_05

通过这些修改,您可以通过避免冲突来提高数据完整性。

回顾

在本节中:

  • 您使用基于 JPA 的乐观锁定字段配置了域模型。@Version
  • 您调整了前端以提取单个资源。
  • 您将单个资源的 ETag 标头插入到请求标头中,以使 PAT 具有条件。If-Match
  • 您为列表中显示的每个员工编写了新代码。UpdateDialog

插入此插件后,很容易避免与其他用户发生冲突或覆盖他们的编辑。

问题?

知道您何时编辑不良记录当然很高兴。但是最好等到您单击“提交”后再找出答案吗?

获取资源的逻辑在 和 中非常相似。您是否看到避免重复代码的方法?​​loadFromServer​​​​onNavigate​

您可以充分利用 JSON 架构元数据来构建 和 输入。您是否看到其他地方可以使用元数据来使事情更加通用?假设您想向 添加另外五个字段。更新 UI 需要什么?​​CreateDialog​​​​UpdateDialog​​​​Employee.java​


标签:pageSize,links,Spring,employees,REST,React,props,employee,Data
From: https://blog.51cto.com/u_15326439/5976759

相关文章

  • SpringBoot+MongoDB实现一个物流订单系统
    码字不易,点赞收藏,养成习惯!原创作者公众号:​​bigsai​​​,回复bigsai领取5G的PDF学习资源!共同进步。更多精彩期待与您分享!项目收录在​​​github​​​的​​MongoDB案例......
  • React.js 和 Spring Data REST(三)
    第4部分-事件在上一节,您引入了条件更新以避免在编辑相同数据时与其他用户发生冲突。您还学习了如何使用乐观锁定对后端的数据进行版本控制。如果有人编辑了同一记录,您会......
  • Spring Boot 启动和 OAuth2
    本指南介绍如何使用“社交登录”构建示例应用,以执行各种操作OAuth2.0和弹簧启动.它从简单的单提供程序单一登录开始,一直到具有身份验证提供程序选择的客户端:GitHub​或谷歌......
  • 使用 Spring Boot 和 Kotlin 构建 Web 应用程序
    本教程向您展示如何通过结合弹簧启动和科特林.如果您从Kotlin开始,您可以通过阅读参考文档,跟随在线KotlinKoans教程或只是使用Spring框架参考文档现在在Kotlin中提供......
  • React.js 和 Spring Data REST
    本教程展示了一系列使用SpringDataREST及其强大的后端功能的应用程序,结合React的复杂功能来构建易于理解的UI。弹簧数据休息提供了一种构建超媒体驱动的存储库的快速......
  • 我的 React Native 技能树点亮计划 の React Native 开发 IDE 选型和配置
    公众号:移动开发前线ReactNative发布一年多了,有不少公司已经在线上产品中或小范围试水,或大范围应用,很多公司或开发者都在为ReactNative的生态系统作出自己的贡献。Reac......
  • #yyds干货盘点# react笔记之学习之空列表提示
    前言我是歌谣我有个兄弟巅峰的时候排名c站总榜19叫前端小歌谣曾经我花了三年的时间创作了他现在我要用五年的时间超越他今天又是接近兄弟的一天人生难免坎坷大不了从......
  • springboot mail 发送163邮件基础配置操作
    #发送邮件配置spring.mail.protocol=smtps#配置smtp服务器地址spring.mail.host=smtp.163.com#服务器的端口spring.mail.port=465#配置邮箱用户名spring.mail......
  • spring-cloud技术栈使用eureka注册中心微服务优雅停机实践
    问题使用SpringCloud搭建微服务体系,如果注册中心选用Eureka,使用spring-cloud-starter-netflix-eureka-client包,能在项目中方便的整合Eureka。在日常开发中经常会遇到一......
  • react 脚手架搭建项目 报错C:\Program Files\nodejs\node_cache\_logs\2022-12-2
    报错内容: 解决方法:第一步:删除C:\ProgramFiles\nodejs\node_cache\_logs目录下所有文件  第二步:切换镜像 npmconfigsetregistryhttps://registry.npm.tao......