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.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.handleDeletehandleDelete()函数可以调用传递下来的回调,同时提供上下文重要的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灵活响应。、


问题?

你让网页变得动态。但是打开另一个浏览器标签并将其指向同一个应用程序,一个选项卡中的更改不会更新另一个选项卡。

这是我们在下一节中可以解决的问题。


猜你喜欢

转载自blog.csdn.net/zmzmzm123321/article/details/80568073