React.js 和 Spring Data REST:第一部分 - 基本特性

工程 | Greg L. Turnquist | 2015 年 9 月 1 日 | ...
要查看此代码的更新,请访问我们的React.js 和 Spring Data REST 教程

欢迎来到 Spring 社区,

这是几篇博客文章中的第一篇。在本部分中,您将看到如何快速启动并运行一个最基础的 Spring Data REST 应用程序。然后,您将使用 Facebook 的 React.js 工具集在其上构建一个简单的 UI。

步骤 0 - 设置您的环境

您可以随意从这个仓库获取代码并跟着操作。

如果您想自己动手,请访问 http://start.spring.io 并选择以下项

  • Rest Repositories
  • Thymeleaf
  • JPA
  • H2

这个演示使用 Java 8、Maven 项目和 Spring Boot 的最新稳定版本。这将为您提供一个干净、空的项目。然后,您可以添加本节中明确显示的不同文件,和/或从上面列出的仓库中借用。

最初…​

最初,有数据。而且数据是好的。但后来人们想通过各种方式访问数据。多年来,人们拼凑了许多 MVC 控制器,其中许多使用了 Spring 强大的 REST 支持。但一遍又一遍地这样做花费了大量时间。

如果做出一些假设,Spring Data REST 可以解决这个问题有多简单

  • 开发者使用支持仓库模型的 Spring Data 项目。
  • 系统使用被广泛接受的行业标准协议,如 HTTP 动词、标准化媒体类型和 IANA 批准的链接名称。

声明您的域

任何基于 Spring Data REST 的应用程序的基石都是域对象。在本节中,您将构建一个应用程序来跟踪公司的员工。通过创建这样一个数据类型来开始

src/main/java/com/greglturnquist/payroll/Employee.java
@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@RequiredArgsConstructor 是 Project Lombok 注解,用于自动生成 getter、setter、构造函数、toString、hash、equals 等。这减少了样板代码。

此实体用于跟踪员工信息。在本例中,是他们的姓名和职位描述。

注意
Spring Data REST 不仅限于 JPA。它支持许多 NoSQL 数据存储,但这里不会涵盖这些内容。

定义仓库

Spring Data REST 应用程序的另一个关键部分是创建相应的仓库定义。

src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
public interface EmployeeRepository extends CrudRepository<Employee, Long> {

}

  • 该仓库扩展了 Spring Data Commons 的 CrudRepository,并插入了域对象的类型及其主键

这就是所需的全部内容!事实上,如果它是顶层且可见的,您甚至不需要注解这个看不见的东西。如果您使用 IDE 打开 CrudRepository,您会发现一堆已经定义的预构建方法。

注意
如果您愿意,可以定义您自己的仓库。Spring Data REST 也支持这一点。

预加载演示

要使用此应用程序,您需要像这样预加载一些数据

src/main/java/com/greglturnquist/payroll/DatabaseLoader.java
@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 创建和注册后运行。
  • 它使用构造函数注入和自动装配来获取 Spring Data 自动创建的 EmployeeRepository
  • run() 方法通过命令行参数调用,加载您的数据。

Spring Data 最大、最强大的特性之一是它能够为您编写 JPA 查询。这不仅减少了您的开发时间,还降低了 Bug 和错误的风险。Spring Data 查看仓库类中方法的名称,并找出您所需的操作,包括保存、删除和查找。

这就是我们如何编写一个空接口并继承已经构建的保存、查找和删除操作的原因。

调整根 URI

默认情况下,Spring Data REST 在 / 上托管链接的根集合。由于您将在同一路径上托管 Web UI,因此您需要更改根 URI。

src/main/resources/application.properties
spring.data.rest.base-path=/api

启动后端

启动一个完全可操作的 REST API 所需的最后一步是使用 Spring Boot 编写一个 public static void main

src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java
@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 来启动它。(Windows 用户使用 mvnw.bat)。

注意
如果您对 Spring Boot 及其工作原理不是最新的,您应该考虑观看 Josh Long 的介绍性演示。看过了?继续前进!

浏览您的 REST 服务

应用程序运行后,您可以使用 cURL(或您喜欢的任何其他工具)在命令行上查看情况。

$ curl localhost:8080/api
{
  "_links" : {
    "employees" : {
      "href" : "http://localhost:8080/api/employees"
    },
    "profile" : {
      "href" : "http://localhost:8080/api/profile"
    }
  }
}

当您 ping 根节点时,会返回一个包装在 HAL 格式的 JSON 文档中的链接集合。

  • _links 是可用链接的集合。
  • employees 指向由 EmployeeRepository 接口定义的员工对象的聚合根。
  • profile 是 IANA 标准关系,指向关于整个服务的可发现元数据。我们将在稍后的部分中探讨这一点。

您可以通过导航 employees 链接来进一步深入此服务。

$ curl localhost:8080/api/employees
{
  "_embedded" : {
    "employees" : [ {
      "firstName" : "Frodo",
      "lastName" : "Baggins",
      "description" : "ring bearer",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/employees/1"
        }
      }
    } ]
  }
}

在此阶段,您正在查看整个员工集合。

与您之前预加载的数据一起包含的是一个带有 self 链接的 _links 属性。这是该特定员工的规范链接。什么是规范链接?它意味着没有上下文。例如,可以通过诸如 /api/orders/1/processor 之类的链接获取同一用户,其中员工与处理特定订单相关联。在此处,与任何其他实体都没有关系。

重要提示
链接是 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"
    }
  }
}

您还可以进行 PUT、PATCH 和 DELETE 操作,如本相关指南所示。但我们不必深入探讨这些。您已经花了太多时间手动与此 REST 服务交互了。您难道不想构建一个时尚的 UI 吗?

设置自定义 UI 控制器

Spring Boot 使构建自定义网页变得异常简单。首先,您需要一个 Spring MVC 控制器。

src/main/java/com/greglturnquist/payroll/HomeController.java
@Controller
public class HomeController {
@RequestMapping(value = "/")
public String index() {
	return "index";
}

}

  • @Controller 标记此类为 Spring MVC 控制器。
  • @RequestMapping 标记 index() 方法以支持 / 路径。
  • 它返回 index 作为模板名称,Spring Boot 的自动配置视图解析器将映射到 src/main/resources/templates/index.html

定义 HTML 模板

您正在使用 Thymeleaf,尽管您不会真正使用它的许多特性。

src/main/resources/templates/index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
    <meta charset="UTF-8"/>
    <title>ReactJS + Spring Data REST</title>
    <script async="" data-main="/run.js" src="/bower_components/requirejs/require.js"></script>
    <link rel="stylesheet" href="/main.css" />
</head>
<body>
&lt;div id="react"&gt;&lt;/div&gt;

</body> </html>

此模板中的关键部分是中间的 <div id="react"></div> 组件。您将在此处指示 React 插入渲染的输出。

加载 JavaScript 模块

本教程不会详细介绍如何使用 require.js 加载 JavaScript 模块。但借助 frontend-maven-plugin,您无需安装任何 node.js 工具即可构建和运行代码。

将使用以下 JavaScript 模块

  • require.js
  • react.js
  • rest.js
  • requirejs-react-js

如果您感兴趣,JavaScript 模块的路径在 run.js 中定义。主 JavaScript 应用程序通过 main.js 由网页加载。

完成所有这些设置后,您可以专注于 React 部分,这些部分在 DOM 加载后获取。它分解如下

由于您正在使用 require.js 加载内容,请继续获取您需要的模块

src/main/resources/static/app.jsx
var React = require('react');
var client = require('./client');
  • React 是 Facebook 的主库,用于构建此应用程序。
  • client 是自定义代码,用于配置 rest.js 以支持 HAL、URI 模板等。它还设置默认的 Accept 请求头为 application/hal+json。您可以在此处阅读代码

深入 React

React 基于定义组件。通常,一个组件可以在父子关系中包含另一个组件的多个实例。这个概念很容易扩展到多个层级。

首先,为所有组件设置一个顶层容器非常方便。(当您在本系列中扩展代码时,这一点会变得更加明显。)目前,您只有员工列表。但以后您可能需要一些其他相关的组件,所以让我们从这里开始

src/main/resources/static/app.jsx - App 组件
var App = React.createClass({
    getInitialState: function () {
        return ({employees: []});
    },
    componentDidMount: function () {
        client({method: 'GET', path: '/api/employees'}).done(response => {
            this.setState({employees: response.entity._embedded.employees});
        });
    },
    render: function () {
        return (
            <EmployeeList employees={this.state.employees}/>
        )
    }
})
  • React.createClass({…​}) 是创建 React 组件的方法。
  • getInitialState 是初始化状态数据(预计会变化的数据)的 API。
  • componentDidMount 是 React 在 DOM 中渲染组件后调用的 API。
  • render 是在屏幕上“绘制”组件的 API。
注意
在 React 中,大写是组件命名约定。

App 组件中,从 Spring Data REST 后端获取员工数组并存储在此组件的 state 数据中。

React 组件有两种数据:stateproperties

State 是组件本身应处理的数据。它也是可能波动和变化的数据。要读取状态,您使用 this.state。要更新它,您使用 this.setState()。每次调用 this.setState() 时,React 都会更新状态,计算先前状态和新状态之间的差异,并将一组更改注入到页面上的 DOM 中。这会使您的 UI 更新快速高效。

常见的约定是在 getInitialState 中初始化所有属性为空的状态。然后您使用 componentDidMount 从服务器查找数据并填充属性。从那时起,更新可以由用户操作或其他事件驱动。

Properties 包含传递到组件中的数据。Properties 不会改变,而是固定值。要设置它们,您在创建新组件时将它们赋值给属性,您很快就会看到。

警告
与其他语言不同,JavaScript 不会锁定数据结构。您可以尝试通过赋值来绕过 properties,但这不适用于 React 的差分引擎,应避免这样做。

在这段代码中,函数通过 client(一个Promise 规范的 rest.js 实例)加载数据。从 /api/employees 获取数据完成后,它会调用 done() 中的函数,并根据其 HAL 文档(response.entity._embedded.employees)设置状态。您可能还记得之前 curl /api/employees 的结构,并看到它如何映射到这个结构。

状态更新后,框架会调用 render() 函数。员工状态数据作为输入参数包含在 <EmployeeList /> React 组件的创建中。

下面是 EmployeeList 的定义。

src/main/resources/static/app.jsx - EmployeeList 组件
var EmployeeList = React.createClass({
    render: function () {
        var employees = this.props.employees.map(employee =>
            <Employee key={employee._links.self.href} employee={employee}/>
        );
        return (
            <table>
                <tr>
                    <th>First Name</th>
                    <th>Last Name</th>
                    <th>Description</th>
                </tr>
                {employees}
            </table>
        )
    }
})

使用 JavaScript 的 map 函数,this.props.employees 从一个员工记录数组转换成一个 <Element /> React 组件数组(您稍后会看到)。

<Employee key={employee._links.self.href} data={employee} />

这显示了一个新的 React 组件(注意大写格式)正在被创建,并带有两个属性:keydata。这些属性的值来自 employee._links.self.hrefemployee

重要提示
每当您使用 Spring Data REST 时,self 链接就是给定资源的键。React 需要子节点的唯一标识符,而 _links.self.href 是完美的。

最后,您返回一个包裹着通过映射构建的 employees 数组的 HTML 表格。

<table>
    <tr>
        <th>First Name</th>
        <th>Last Name</th>
        <th>Description</th>
    </tr>
    {employees}
</table>

这种简单的状态、属性和 HTML 布局展示了 React 如何让您声明式地创建一个简单易懂的组件。

这段代码既包含 HTML 又包含 JavaScript 吗?是的。如果您注意到了文件扩展名,这是 JSX。没有强制要求使用它。React 可以用纯 JavaScript 编写,但 JSX 语法非常简洁。

JSX 还包含一些 ES6 的片段。代码中使用的是 箭头函数。它避免创建带有自己作用域 this 的嵌套 function(),也避免需要一个 self 变量

担心将逻辑与结构混合?React 的 API 鼓励使用状态和属性构建漂亮的、声明式的结构。React 鼓励构建简单的组件,这些组件包含少量相关的状态和属性,并且能够协同工作,而不是将一堆不相关的 JavaScript 和 HTML 混合在一起。这让您可以查看单个组件并理解其设计。然后,您可以轻松地将它们组合起来构建更大的结构。

接下来,您需要实际定义 <Employee /> 是什么。

src/main/resources/static/app.jsx - Employee 组件
var Employee = React.createClass({
    render: function () {
        return (
            <tr>
                <td>{this.props.employee.firstName}</td>
                <td>{this.props.employee.lastName}</td>
                <td>{this.props.employee.description}</td>
            </tr>
        )
    }
})

这个组件非常简单。它是一个包裹着员工三个属性的单行 HTML 表格行。属性本身是 this.props.employee。注意传递 JavaScript 对象后,传递从服务器获取的数据是多么容易?

因为这个组件既不管理任何状态也不处理用户输入,所以没有其他要做的事情。这可能会诱使您将其塞入上面的 <EmployeeList /> 中。不要这样做!相反,将您的应用程序分解成每个只完成一项工作的更小的组件,这将使您将来更容易构建功能。

最后一步是渲染整个内容。

src/main/resources/static/app.jsx - 渲染代码
React.render(
    <App />,
    document.getElementById('react')
)

React.render() 接受两个参数:您定义的 React 组件以及要将它插入的 DOM 节点。还记得您之前从 HTML 页面看到的 <div id="react"></div> 元素吗?这就是它被选取并插入的地方。

完成所有这些设置后,重新运行应用程序 (./mvnw spring-boot:run) 并访问 http://localhost:8080

basic 1

您可以看到系统最初加载的员工。

还记得使用 cURL 创建新条目吗?再做一次。

curl -X POST localhost:8080/api/employees -d '{"firstName": "Bilbo", "lastName": "Baggins", "description": "burglar"}' -H 'Content-Type:application/json'

刷新浏览器,您应该看到新的条目

basic 2

现在您可以看到他们两个都列在网站上了。

回顾

在本节中

  • 您定义了一个域对象和一个对应的仓库。
  • 您让 Spring Data REST 以完整的超媒体控件将其导出。
  • 您创建了两个简单的 React 组件,它们具有父子关系。
  • 您获取了服务器数据并将其渲染为简单的静态 HTML 结构。

问题?

  • 网页不是动态的。您必须刷新浏览器才能获取新记录。
  • 网页没有使用任何超媒体控件或元数据。相反,它硬编码了从 /api/employees 获取数据。
  • 它是只读的。虽然您可以使用 cURL 修改记录,但网页不提供任何此类功能。

这些问题我们将在下一节中解决。在此之前,编码愉快!

获取 Spring 新闻通讯

订阅 Spring 新闻通讯,保持联系

订阅

领先一步

VMware 提供培训和认证,为您的进步注入强大动力。

了解更多

获取支持

Tanzu Spring 通过一项简单的订阅,提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

近期活动

查看 Spring 社区的所有近期活动。

查看全部