您的位置:首页 > Web前端 > React

React.js 与 Spring Data REST(二)(官方文档翻译)

2018-06-04 16:49 281 查看

第2部分-超媒体控制

在上一节中,您了解了如何使用Spring Data REST来建立后端工资服务来存储员工数据。它缺少了一个使用超媒体控件和链接导航的关键特性。相反,它硬编码了查找数据的路径。

您可以从这个存储库中获取代码并继续执行。本节基于前一节的应用程序,添加了额外的内容。

一开始有数据,然后是REST

我对很多人将任何基于http的接口称为REST API而感到沮丧。今天的例子是SocialSite REST API。这是RPC。需要做些什么才能让REST架构风格清楚地认识到超文本是一种约束?换句话说,如果应用程序状态(以及API)的引擎不是由超文本驱动的,那么它就不能是RESTful的,也不能是REST API。

http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven

那么,超媒体控制到底是什么,也就是超文本,你怎么能使用它们呢?为了找到答案,让我们退一步,看看REST的核心使命。

REST的概念是借用使web如此成功并将其应用于api的思想。尽管web的大小、动态特性和低速率,客户端,也就是浏览器,都被更新了,但是web是一个惊人的成功。罗伊菲尔丁试图利用它的一些约束和特性,看看是否能提供类似的API生产和消费的扩展。

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

GET -在不改变系统的情况下获取资源的状态

POST-创建一个新资源,而不需要说明

PUT-替换现有的资源,重写已经存在的其他资源(如果有的话)

DELETE-删除现有资源

PATCH-改变现有资源的一部分

这些都是标准的HTTP动词与书写规范。通过挑选和使用已经创造了的HTTP操作,我们不需要发明一种新的语言。

REST的另一个约束是使用媒体类型来定义数据的格式。与其让每个人都用自己的方言来交换信息,不如开发一些媒体类型。最受欢迎的一个是HAL,媒体类型应用程序/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),看看它是如何工作的。$ 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,所以要在实际操作中看到它,大小=2。正如预期的那样,只有两名员工被列出。此外,还有第一个、下一个和最后一个链接。还有自链接,没有上下文,包括页面参数。
如果你导航到下一个链接,你也会看到一个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数据REST的一部分。没有混乱的控制器更新!)

需要指出的是,这个应用程序并不是“Spring Data REST特有的”。相反,它使用HAL、URI模板和其他标准。使用rest.js很简单:这个包里有HAL的支持。

在上一节中,您硬编码了通往/api/employee的路径。相反,您应该硬编码的唯一路径是根。

...
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():

follow()函数的第一个参数是用来进行REST调用的客户端对象。

第二个参数是开始的根URI。

第三个参数是一系列的关系来进行导航。每一个都可以是一个字符串或一个对象。

关系的数组可以像“雇员”一样简单,也就是说,当第一次调用时,查看关系(或rel)的链接的链接。找到它的href,并导航到它。如果阵列中有另一种关系,则冲洗并重复。

有时候,一个rel本身是不够的。在这段代码中,它还插入了一个查询参数:size=<pagesize>。还有其他的选项可以提供,您将会看到更多。

抓住JSON元数据模式

在使用基于size的查询导航到员工之后,员工的工作就在你的指尖。在上一节中,我们将其命名为day,并在<employeelist/>中显示该数据。今天,您正在执行另一个调用,以获取在/api/profile/employees/处发现的JSON模式元数据。

你可以自己看到数据:$ 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" : "http://json-schema.org/draft-04/schema#" }/profile/employee的元数据的默认形式是ALPS。但是,在这种情况下,您使用内容协商来获取JSON模式。

通过在“<App/>”组件的状态中捕获这些信息,您可以在构建输入表单时更好地利用它。

创造新的记录

有了这些元数据,您现在可以向UI添加一些额外的控件。创建一个新的React组件,<CreateDialog/>。

class CreateDialog extends React.Component {

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

handleSubmit(e) {
e.preventDefault();
var 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() {
var 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 onClick={this.handleSubmit}>Create</button>
</form>
</div>
</div>
</div>
)
}

}
这个新组件既有handleSubmit()函数,也有预期的render()函数。

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

呈现

您的代码映射在属性中找到的JSON模式数据,并将其转换为一个数组的“<p><input></p>”元素。

key需要再次对多个子节点进行区分。它是一个简单的基于文本的输入字段。

placeholder是我们可以向用户显示字段的地方。

您可能曾经有一个name属性,但这不是必需的。有了响应,ref是获取特定DOM节点的机制(您将很快看到)。

这代表了组件的动态特性,这是通过从服务器加载数据驱动的。

在这个组件的顶层是一个锚标记<div>和另一个<div>。锚标签是打开对话框的按钮。嵌套的<div>是隐藏的对话框本身。在本例中,您使用的是纯HTML5和CSS3。没有JavaScript !您可以看到用于显示/隐藏对话框的CSS代码。我们不会深入讨论这个问题。

在一个<div id="createEmployee">表单中,您的输入字段的动态列表被注入,然后是Create按钮。这个按钮有一个onClick={this.handleSubmit}事件处理程序。这是React处理注册一个事件处理程序的响应方式。

React不会在每个DOM元素上创建一个事件处理程序。相反,它有一个更高效、更复杂的解决方案。关键是,您不需要管理基础设施,而是可以专注于编写功能代码。

处理用户输入

handleSubmit()函数首先阻止事件在层次结构中冒泡。然后它使用相同的JSON模式属性来找到每个<input>,使用React.findDOMNode(this.refs[attribute])。

this.refs是一种通过名称获取特定反应组件的方法。从这个意义上说,您只获得了虚拟DOM组件。要获取实际的DOM元素,您需要使用反应器

React.findDOMNode()
.

在遍历每一个输入并构建newEmployee对象之后,我们调用一个回调到onCreate()新来的员工。这个函数在app.oncreate中向上,并作为另一个属性提供给这个React组件。看看这个顶级函数是如何运作的:

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);
}
});
}

再一次,使用follow()函数导航到执行POST操作的雇员资源。在这种情况下,不需要应用任何参数,因此基于字符串的串列是可以的。在这种情况下,POST调用返回。这允许下一个then()子句处理处理POST的结果。

新的记录通常被添加到数据集的末尾。因为您正在查看某个页面,所以期望新的员工记录不会出现在当前页面上是合乎逻辑的。要处理这个问题,您需要获取具有相同页面大小的新一批数据。这个承诺会返回到done()的最终子句中。

因为用户可能想要看到新创建的员工,所以您可以使用超媒体控件并导航到最后一个条目。

这在UI中引入了分页的概念。让我们解决下!第一次使用基于承诺的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 */
});

要了解更多细节,请查看本教程的承诺。用承诺记住的秘密是,then()函数需要返回一些东西,不管它是一个值还是另一个承诺。done()函数不会返回任何东西,之后也不会对任何东西进行链锁。如果您还没有注意到,客户端(这是rest.js的rest实例),以及follow函数返回的承诺。

翻阅资料

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

在上一节中,您使用页面控件跳到最后一页。将它动态地应用到UI并让用户按照需要进行导航将非常方便。根据可用的导航链接动态调整控件是非常棒的。

首先,让我们看看您使用的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
});
});
}
这是在顶部,在App.onNavigate中定义的。同样,这是为了管理顶层组件中UI的状态。将onNavigate()传递到<EmployeeList />React组件之后,下面的处理程序将被编码以处理单击某些按钮:
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() {
var employees = this.props.employees.map(employee =>
<Employee key={employee._links.self.href} employee={employee} onDelete={this.props.onDelete}/>
);

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

return (
<div>
<input ref="pageSize" defaultValue={this.props.pageSize} onInput={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>
)
}
和前一节一样,它仍然转换了this.props.employees分成一系列的<Element />组件。然后,它构建了一系列的navlink,一组HTML按钮。因为React基于XML的,所以不能在元素中放入“<”。您必须使用编码的版本。

然后你可以看到在返回的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>
)
}
}
这个更新的员工组件显示了一行末尾的一个额外条目,一个delete按钮。当点击它时,用来注册调用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删除记录之后应用的行为有点棘手。在这种情况下,它会从服务器重新加载整个数据,应用相同的页面大小。然后它显示了第一个页面。如果您正在删除最后一页上的最后一个记录,那么它将跳转到第一个页面。
调整页面大小

要了解超媒体的真正亮点,一种方法是更新页面大小。SpringDataREST流畅地更新基于页面大小的导航链接。

这是顶部的一个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();
var 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);
}
}
它阻止了事件的发生。然后,它使用<input>的ref属性来查找DOM节点并提取其值,所有这些都通过响应的findDOMNode()助手函数。它通过检查输入是否为一串数字来测试输入是否真的是一个数字。如果是这样,它会调用回调,将新的页面大小发送给应用程序反应组件。如果没有,输入的字符就会被删除。

当应用程序获得updatePageSize()时,App会做什么?检查一下:

updatePageSize(pageSize) {
if (pageSize !== this.state.pageSize) {
this.loadFromServer(pageSize);
}
}
因为新的页面大小会导致所有导航链接的更改,所以最好重新获取数据并从一开始就开始。
把它放在一起

有了这些漂亮的附加功能,你就有了一个真正的用户界面。


您可以看到顶部的页面大小设置、每行的delete按钮以及底部的导航按钮。导航按钮显示了超媒体控件的强大功能。在下面,您可以看到CreateDialog,将元数据插入到HTML输入占位符中。


这实际上显示了使用超媒体耦合与域驱动元数据(JSON模式)的强大功能。web页面不需要知道哪个字段是哪个字段。相反,用户可以看到它并知道如何使用它。如果您向Employee域对象添加了另一个字段,这个弹出框会自动显示它。

审查在本节中:您打开了Spring Data REST的分页功能。您抛弃了硬编码的URI路径,并开始使用与关系名或“黑”相结合的根URI。您更新了UI,以动态地使用基于页面的超媒体控件。您增加了创建和删除员工的能力,并根据需要更新UI。您使更改页面大小成为可能,并使UI灵活响应。、
问题?你让网页变得动态。但是打开另一个浏览器标签并将其指向同一个应用程序,一个选项卡中的更改不会更新另一个选项卡。这是我们在下一节中可以解决的问题。


阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: