React.js 与 Spring Data REST(二)(官方文档翻译)
第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.javapublic 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}><<</button>); } if ("prev" in this.props.links) { navLinks.push(<button key="prev" onClick={this.handleNavPrev}><</button>); } if ("next" in this.props.links) { navLinks.push(<button key="next" onClick={this.handleNavNext}>></button>); } if ("last" in this.props.links) { navLinks.push(<button key="last" onClick={this.handleNavLast}>>></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灵活响应。、
问题?你让网页变得动态。但是打开另一个浏览器标签并将其指向同一个应用程序,一个选项卡中的更改不会更新另一个选项卡。这是我们在下一节中可以解决的问题。
阅读更多
- spring-boot-starter-data-redis 翻译官方文档 8.1 - 8.3
- spring-boot-starter-data-redis 翻译官方文档 5.7 - 5.9
- spring-boot-starter-data-redis 翻译官方文档 6.1 - 6.4
- React.js 官方文档翻译3 代码集合
- spring-boot-starter-data-redis 翻译官方文档 8.4 - 8.6
- React.js 官方文档翻译2
- spring-boot-starter-data-redis 翻译官方文档 5.3 - 5.6
- spring-boot-starter-data-redis 翻译官方文档 5.10 - 5.13
- React.js 官方文档翻译
- spring-boot官方文档翻译——第三部分
- Data Binding Guide——google官方文档翻译(下)
- part2 react官方文档笔记09--JSX In Depth
- Spring官方文档翻译
- spring3.2.1api文档(翻译官方英文版的方法),spring-framework-reference,spring-framework-3.2.1.RELEASE
- Spring官方文档翻译——5.资源
- Spring Data REST + GemFire + AngularJS Integration
- React.js 官方文档摘记:非受控组件
- Reactjs入门官方文档(一)【jsx】
- Spring Boot中文文档(官方文档翻译 基于1.5.2.RELEASE)
- Data Binding Guide——google官方文档翻译(下)