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 仓库
  • 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" : "https://127.0.0.1:8080/api/employees"
    },
    "profile" : {
      "href" : "https://127.0.0.1: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" : "https://127.0.0.1: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" : "https://127.0.0.1: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" : "https://127.0.0.1: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 控制器。
  • @RequestMappingindex() 方法标记为支持 / 路由。
  • 它返回 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 由网页加载。

有了所有这些,您可以专注于在 DOM 加载后获取的 React 部分。它被分解成如下所示的部分:

由于您使用 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 组件拥有两种类型的数据:状态属性

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

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

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

警告
JavaScript 不会像其他语言那样锁定数据结构。您可以尝试通过赋值来破坏属性,但这不适用于 React 的差异引擎,应避免。

在此代码中,该函数通过client加载数据,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的嵌套函数(),并且避免需要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)并访问https://127.0.0.1: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 社区中所有即将举行的活动。

查看全部