本教程展示了一组使用Spring Data REST的应用程序,以及它强大的后端功能,结合了React的复杂特性,构建了一个易于grok(理解?)的UI。
Spring Data REST提供了一种快速的方法来构建基于多媒体的存储库。
React 是Facebook在JavaScript的领域里用来高效、快速和易于使用视图的解决方案。
第1部分-基本特征
欢迎Spring社区,在本节中,您将看到如何快速地获得一个简单的Spring Data REST应用程序。然后,您将使用Facebook的React构建一个简单的UI。js工具集。
步骤0——设置您的环境
您可以从这个存储库中获取代码并继续执行。
如果您想自己动手,请访问http://start.spring。输入这些项目:Rest Repositories、Thymeleaf、JPA、H2、Lombok(可能需要确保您的IDE也支持此功能)。
这个Demo使用了Java 8、Maven项目和SpringBoot的最新稳定版本。它还使用React.js的ES6编码。它将为您提供一个干净、空的项目。在这里,您可以添加本节中显式显示的各种文件,或从上面列出的存储库中借用。
一开始……
一开始就有数据。很好。但是接下来人们想要通过各种方式访问数据。多年来,人们拼凑了大量的MVC控制器,许多人使用Spring强大的REST支持。但是这样做一遍又一遍的花费了很多时间。
如果有一些假设的话,SpringDataREST使这个问题变得简单:
1、开发人员使用一个支持存储库模型的Spring Data项目。
2、该系统使用公认的行业标准协议,如HTTP枚举、标准化媒体类型和iana-provides的链接名称。
声明你的domain(域名?)
任何基于Spring Data REST的应用程序的基础都是域名对象。对于本节,您将构建一个应用程序来跟踪公司的员工。通过创建这样的数据类型来启动它:
@Data
@Entity
public class Employee {
private @Id @GeneratedValue Long id;
private String firstName;
private String lastName;
private String description;
private Employee() {}
public Employee(String firstName, String lastName, String description) {
this.firstName = firstName;
this.lastName = lastName;
this.description = description;
}
}
@entity是一个JPA注解,它表示整个类在关系表中存储。
@id和@generatedvalue是JPA注解,用来记录主键,并且在需要时自动生成。
@data是一个项目Lombok注释,用于自动生成getter、setter、构造函数、toString、hash、equals和其他东西。它减少了样板文件。
这个实体用于跟踪员工信息。在这个例子中包含员工的名字和职位描述。
SpringDataREST并不局限于JPA。它支持许多NoSQL数据存储,但是您不会在这里涉及这些数据存储。
定义存储库
Spring Data REST应用程序的另一个关键部分是创建相应的资源库定义。
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
}
存储库继承了Spring Data Commons的CrudRepository,并插入了域对象及其主键的类型。
这就是我们所需要的!事实上,如果它是顶级的和可见的,你甚至不需要注释它。如果您使用IDE并打开CrudRepository,您将会发现一个已经定义好的全的预构建方法集。
如果您愿意,您可以定义自己的存储库。SpringDataREST也支持这一点。
预加载这个demo
要使用这个应用程序,您需要使用如下的数据预加载它:
@Component
public class DatabaseLoader implements CommandLineRunner {
private final EmployeeRepository repository;
@Autowired
public DatabaseLoader(EmployeeRepository repository) {
this.repository = repository;
}
@Override
public void run(String... strings) throws Exception {
this.repository.save(new Employee("Frodo", "Baggins", "ring bearer"));
}
}
这个类用Spring的@Component注解来标记,这样它就会被@SpringBootApplication自动拾取。
它实现了Spring Boot的CommandLineRunner接口,这样它就可以在所有bean被创建和注册之后运行。
它使用构造器注入和自动装配来获得SpringData自动创建的EmployeeRepository。
run()方法是用命令行参数调用的,加载您的数据。
Spring Data最大的、最强大的特性之一是它能够为您编写JPA查询。这不仅减少了您的开发时间,而且还降低了bug和error的风险。SpringData查看存储库类中的方法名,并找出您需要的操作,包括保存、删除和查找。
这就是我们如何编写一个空接口,并继承已经构建的save、find和delete的操作。
调整根URI
默认情况下,Spring Data REST用 / 提供链接的根集合。因为您将在相同的路径上提供一个web UI,所以您需要更改根URI。
spring.data.rest.base-path=/api
启动后端
要获得完全运行的REST API所需的最后一步是使用SpringBoot来编写一个public static void main:
@SpringBootApplication
public class ReactAndSpringDataRestApplication {
public static void main(String[] args) {
SpringApplication.run(ReactAndSpringDataRestApplication.class, args);
}
假设前面的类以及您的Maven构建文件都是通过http://start,spring,io生成的,那么您现在可以通过在IDE中运行main()方法或者在命令行中输入 ./mvnw spring-boot:run引导来启动它。(mvnw.bat为Windows用户提供使用)。
如果你不了解SpringBoot的最新情况,以及它是如何工作的,你应该考虑一下Josh Long的介绍性演讲。(需翻墙)
Touring(参观?)你的REST服务
随着应用程序的运行,您可以使用cURL(或者您喜欢的任何其他工具)在命令行上检查事情。
$ curl localhost:8080/api { "_links" : { "employees" : { "href" : "http://localhost:8080/api/employees" }, "profile" : { "href" : "http://localhost:8080/api/profile" } } }
当你ping根节点时,你会得到一个包含在HAL-formated JSON document中的链接的集合。
_link是一个可用的链接集合。
employees指向由EmployeeRepository接口定义的员工对象的聚合根。
profile是一个IANA-standard关系,并指向可发现的关于整个服务的元数据。我们将在后面的部分中探讨这个问题。
您可以通过导航员工链接进一步挖掘这项服务。
$ curl localhost:8080/api/employees { "_embedded" : { "employees" : [ { "firstName" : "Frodo", "lastName" : "Baggins", "description" : "ring bearer", "_links" : { "self" : { "href" : "http://localhost:8080/api/employees/1" } } } ] } }
在这个阶段,您将查看所有员工的集合。
与您之前预加载的数据一起包含的是带有自链接的_links属性。这是该特定员工的典型链接。规范是什么?它意味着没有上下文。例如,同一用户可以通过像/api/orders/1/processor这样的链接获取,在这种链接中,员工被处理成一个特定的order(规则?)。在这里,与其他实体没有关系。
链接是REST的一个关键方面。它们提供了导航到相关项目的能力。它使其他各方能够在您的API中导航,而不需要在每次发生更改时重写这些东西。当客户端硬编码到资源的路径时,客户端的更新是一个常见的问题。重组资源可能导致代码的大动荡。如果使用链接,而导航路线是被维护的,那么进行这样的调整就变得容易和灵活了。
如果你愿意,你可以决定去查看一名员工的信息。
$ curl localhost:8080/api/employees/1 { "firstName" : "Frodo", "lastName" : "Baggins", "description" : "ring bearer", "_links" : { "self" : { "href" : "http://localhost:8080/api/employees/1" } } }
这里没有什么变化,除了没有必要使用_embedded包装器,因为只有域对象。
这很好,但是你可能想要创建一些新的实体。
$ curl -X POST localhost:8080/api/employees -d "{\"firstName\": \"Bilbo\", \"lastName\": \"Baggins\", \"description\": \"burglar\"}" -H "Content-Type:application/json" { "firstName" : "Bilbo", "lastName" : "Baggins", "description" : "burglar", "_links" : { "self" : { "href" : "http://localhost:8080/api/employees/2" } } }
您还可以像本指南中所示的那样POST、PATCH和DELETE。但我们还是不要深究了。您已经花费了太多的时间来手动与REST服务交互。难道你不想构建一个漂亮的UI吗?
设置一个自定义UI控制器
SpringBoot使它非常容易地定制一个web页面。首先,您需要一个Spring MVC控制器。
@Controller
public class HomeController {
@RequestMapping(value = "/")
public String index() {
return "index";
}
}
@Controller把这个类标记为Spring MVC控制器。
@RequestMapping标记index()方法来支持/路由。
它返回索引作为模板的名称,Spring Boot的自动配置视图解析器将映射到src/main/resources/templat/index.html。
定义一个HTML模板
你使用的是Thymeleaf,尽管你不会真正使用它的很多特征。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
<meta charset="UTF-8"/>
<title>ReactJS + Spring Data REST</title>
<link rel="stylesheet" href="/main.css" />
</head>
<body>
<div id="react"></div>
<script src="built/bundle.js"></script>
</body>
</html>
这个模板的关键部分是中间的<div id="react"></div>组件。在这里,您将直接作出反应,以插入渲染的输出。
您可能还想知道bundle.js文件从何而来。它的构建方式将在下一节中显示。
加载JavaScript模块
This section contains the barebones information to get off the JavaScript bits off the ground。(?)
虽然您可以安装所有的javascommand命令行工具,但您不必这样做。至少,现在还不是时候。相反,你需要添加的是你的pom.xml构建文件:
frontend-maven-plugin
used to build JavaScript bits
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.2</version>
<configuration>
<installDirectory>target</installDirectory>
</configuration>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>v4.4.5</nodeVersion>
<npmVersion>3.9.2</npmVersion>
</configuration>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>webpack build</id>
<goals>
<goal>webpack</goal>
</goals>
</execution>
</executions>
</plugin>
这个小插件执行多个步骤:
install-node-npm命令将安装node.js和它的包管理工具npm进入目标文件夹。(这确保了二进制文件没有被拉到源代码控制之下,并且可以用干净的方式进行清理)。
npm命令将使用提供的参数(安装)来执行npm二进制文件。这将安装在package.json中定义的模块。
webpack命令将执行webpack二进制文件,它根据webpack.config.js编译所有JavaScript代码。
这些步骤是按顺序运行的,本质上是安装node.js,下载JavaScript模块,构建JS bits.
什么模块被安装?JavaScript开发人员通常使用npm来构建一个package.json文件如下所示:
{
"name": "spring-data-rest-and-reactjs",
"version": "0.1.0",
"description": "Demo of ReactJS + Spring Data REST",
"repository": {
"type": "git",
"url": "[email protected]:spring-guides/tut-react-and-spring-data-rest.git"
},
"keywords": [
"rest",
"hateoas",
"spring",
"data",
"react"
],
"author": "Greg L. Turnquist",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/spring-guides/tut-react-and-spring-data-rest/issues"
},
"homepage": "https://github.com/spring-guides/tut-react-and-spring-data-rest",
"dependencies": {
"react": "^15.3.2",
"react-dom": "^15.3.2",
"rest": "^1.3.1",
"webpack": "^1.12.2"
},
"scripts": {
"watch": "webpack --watch -d"
},
"devDependencies": {
"babel-core": "^6.18.2",
"babel-loader": "^6.2.7",
"babel-polyfill": "^6.16.0",
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0"
}
}
关键依赖关系包括:
react.js-本教程使用的工具箱
rest.js-CujoJS工具包用于进行REST调用
webpack-用来将JavaScript组件编译成一个可加载的捆绑包的工具包
babel-使用ES6编写JavaScript代码并将其编译为ES5,以便在浏览器中运行
要构建JavaScript代码,您需要进一步深入了解,您需要为webpack定义一个构建文件
var path = require('path');
module.exports = {
entry: './src/main/js/app.js',
devtool: 'sourcemaps',
cache: true,
debug: true,
output: {
path: __dirname,
filename: './src/main/resources/static/built/bundle.js'
},
module: {
loaders: [
{
test: path.join(__dirname, '.'),
exclude: /(node_modules)/,
loader: 'babel',
query: {
cacheDirectory: true,
presets: ['es2015', 'react']
}
}
]
}
};
这个webpack配置文件执行以下操作:
将入口点定义为 ./src/main/js/app.js。从本质上说,app.js(我们将很快编写的一个模块)是我们的JavaScript应用程序的public statoc void main()。webpack必须知道这一点,以便知道当最终捆绑包被浏览器加载时要启动什么。
在浏览器中调试JS代码时,创建sourcemaps,能够链接回原始源代码。
将所有的JavaScript位编译成./src/main/resources/static/buil/bundle.js,它是一款类似于SpringBoot的uber JAR的JavaScript脚本。所有您的自定义代码和in a la require()调用的模块都被塞进这个文件中。
它使用es2015和响应预置来连接到babel引擎,以便将ES6的反应代码编译成能够在任何标准浏览器中运行的格式。
想要看到你的JavaScript自动改变吗?运行npm run-script watch 将webpack放入观察模式。它将在编辑源代码时重新生成bundle.js。
有了这些之后,您就可以将注意力集中在DOM加载后获取的反应位上。它被分解成如下部分:
因为您正在使用webpack来组装东西,所以继续获取您需要的模块:
const React = require('react');
const ReactDOM = require('react-dom');
const client = require('./client');
React和ReactDom是Facebook用来构建这款应用的主要库。client是配置rest的自定义代码。js包括对HAL、URI模板和其他东西的支持。它还将默认的Accept请求头设置为application/hal+json。你可以在这里阅读代码。
深入React
React是基于组件定义的。通常,一个组件可以在父子关系中容纳另一个组件的多个实例。这个概念很容易扩展好几层。
首先,对于所有组件都有一个顶级的容器是非常方便的。(当您在本系列文章中扩展代码时,这将变得更加明显。)现在,你只有员工名单。但是以后可能还需要一些其他相关的组件,让我们从这个开始:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {employees: []};
}
componentDidMount() {
client({method: 'GET', path: '/api/employees'}).done(response => {
this.setState({employees: response.entity._embedded.employees});
});
}
render() {
return (
<EmployeeList employees={this.state.employees}/>
)
}
}
class Foo extends React.Component{…} 是创建React组件的方法。
componentDidMount t是在React中调用DOM中的组件后调用的API。
render 是在屏幕上“绘制”组件的API。
在React中,大写是命名组件的约定。
在App组件中,从Spring Data REST后端取出一批员工,并存储在该组件的状态数据中。
React组件有两种类型的数据:状态和属性。
状态是组件被期望处理自身的数据。它也是可以波动和变化的数据。要读取状态,您可以使用this.state。要更新它,您可以使用this.setstate()。每次调用setstate()时,都会对状态进行更新,计算前一个状态和新状态之间的差异,并向页面上的DOM注入一组更改。这将导致对UI的快速和高效的更新。
常见的约定是在构造函数中清空所有属性的状态。然后使用componentDidMount从服务器查找数据并填充属性。从这里开始,更新可以由用户操作或其他事件驱动。
属性包含传递到组件的数据。属性不会改变,而是固定的值。为了设置它们,您在创建新组件时将它们分配给属性,您很快就会看到。
JavaScript不会像其他语言一样锁定数据结构。你可以通过分配值来破坏属性,但是这与React的微分引擎不兼容,应该避免。
在这段代码中,函数通过客户端加载数据,这是一个承诺兼容的rest.js实例。当它从/api/employees中检索时,它会调用done()中的函数,并根据它的HAL document(response.entity._embedded.employees)来设置状态。您可能还记得curl/api/employees的结构,并了解它是如何映射到这个结构的。
当状态更新时,render()函数将被框架调用。员工状态数据包含在创建employeelist/>React组件作为输入参数的过程中。
下面是一个雇员列表的定义。
class EmployeeList extends React.Component{
render() {
var employees = this.props.employees.map(employee =>
<Employee key={employee._links.self.href} employee={employee}/>
);
return (
<table>
<tbody>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Description</th>
</tr>
{employees}
</tbody>
</table>
)
}
}
使用JavaScript的map函数,this.props.employees从一组员工记录转变为<Element />React组件(您将会看到更深入的内容)。
<Employee key={employee._links.self.href} data={employee} />
这显示了一个新的React组件(注意大写的格式)和两个属性:键和数据。这些都是来自employee.self.href和employee。
当您使用SpringDataREST时,self link是给定资源的键。React需要一个唯一的子节点标识,以及_links.self.href是完美的。
最后,您返回一个HTML表格,它围绕着用映射构建的雇员数组。
<table>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Description</th>
</tr>
{employees}
</table>
这个简单布局的状态、属性和HTML显示了React如姐让您可以声明地创建一个简单且易于理解的组件。
这段代码包含HTML和JavaScript吗?是的,这是JSX。没有必要使用它。可以使用纯JavaScript编写响应,但是JSX语法非常简洁。多亏了babel.js的快速工作。它同时提供了JSX和ES6支持。
JSX还包括ES6的部分。在代码中使用的是箭头函数。它避免创建一个嵌套函数(),并避免需要一个self变量。
担心把逻辑和你的结构混合在一起?React的api鼓励良好的、声明性的结构与状态和属性相结合。与其混合一堆不相关的JavaScript和HTML,还不如鼓励构建简单的组件,这些组件的相关状态和属性可以很好地协同工作。它让您查看单个组件并了解设计。然后它们很容易组合在一起,形成更大的结构。
接下来,您需要定义什么是<Employee />。
class Employee extends React.Component{
render() {
return (
<tr>
<td>{this.props.employee.firstName}</td>
<td>{this.props.employee.lastName}</td>
<td>{this.props.employee.description}</td>
</tr>
)
}
}
这个组件非常简单。它有一个单独的HTML表格行,围绕着员工的三个属性。这个属性本身就是this.prop.employee。请注意,如何通过JavaScript对象使传递从服务器获取的数据变得容易?
因为这个组件不管理任何状态,也不处理用户输入,所以没有其他事情可做。这可能会诱使你把它塞进上面的<EmployeeList />。不要这样做!相反,将你的应用分割成小的组件,每个人都做一份工作,这将使你在未来更容易建立起功能。
最后一步是渲染整个过程。
ReactDOM.render(
<App />,
document.getElementById('react')
)
有了这些,重新运行应用程序(./mvnw spring-boot:run)并访问http://localhost:8080。
您可以看到系统中加载的初始员工。
还记得使用cURL来创建新条目吗?再做一次。
curl -X POST localhost:8080/api/employees -d "{\"firstName\": \"Bilbo\", \"lastName\": \"Baggins\", \"description\": \"burglar\"}" -H "Content-Type:application/json"
刷新浏览器,您应该看到新的条目:
现在你可以看到它们都在网站上列出了。
回顾
在本节中:
你定义了一个域对象和一个相应的存储库。
您可以让SpringDataREST导出它,并使用完全的超媒体控件。
您在父子关系中创建了两个简单的React组件。
您获取了服务器数据并将它们呈现为一个简单的静态HTML结构。
问题?
web页面并不是动态的。您必须刷新浏览器以获取新的记录。
web页面没有使用任何超媒体控件或元数据。相反,它是硬编码从/api/雇员获取数据的。
它是只读的。虽然您可以使用cURL来修改记录,但是web页面并没有提供这些内容。
这些是我们在下一节中可以解决的问题。