SpringBoot2-和-React-全栈开发实用指南-全-

SpringBoot2 和 React 全栈开发实用指南(全)

原文:zh.annas-archive.org/md5/B5164CAFF262E48113020BA46AD77AF2

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在本书中,我们将使用 Spring Boot 2.0 和 React 创建一个现代 Web 应用程序。我们将从后端开始,使用 Spring Boot 和 MariaDB 开发 RESTful Web 服务。我们还将保护后端并为其创建单元测试。前端将使用 React JavaScript 库开发。将使用不同的第三方 React 组件使前端更加用户友好。最后,应用程序将部署到 Heroku。该书还演示了如何将后端 Docker 化。

本书适合谁

这本书是为:

  • 想要学习全栈开发的前端开发人员

  • 想要学习全栈开发的后端开发人员

  • 使用其他技术的全栈开发人员

  • 熟悉 Spring 但从未构建过全栈应用程序的 Java 开发人员。

充分利用本书

读者应具备以下知识:

  • 基本的使用一些终端,如 PowerShell 的知识

  • 基本的 Java 和 JavaScript 编程知识

  • 基本的 SQL 数据库知识

  • 基本的 HTML 和 CSS 知识

下载示例代码文件

您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

您可以按照以下步骤下载代码文件:

  1. www.packtpub.com登录或注册。

  2. 选择“支持”选项卡。

  3. 点击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明进行操作。

文件下载后,请确保使用最新版本的解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Full-Stack-Development-with-Spring-Boot-2.0-and-React。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有其他代码包,来自我们丰富的书籍和视频目录,可在github.com/PacktPublishing/上找到。去看看吧!

使用的约定

本书中使用了许多文本约定。

CodeInText:指示文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄。这是一个例子:“在domain包中创建一个名为CarRepository的新类。”

代码块设置如下:

@Entity
public class Car {

}

任何命令行输入或输出都是这样写的:

mvn clean install

粗体:指示一个新术语,一个重要的词,或者您在屏幕上看到的词。例如,菜单或对话框中的单词会以这种形式出现在文本中。这是一个例子:“在 Eclipse 的 Project Explorer 中激活根包,右键单击。从菜单中选择 New | Package。”

警告或重要说明会显示在这样的形式下。

提示和技巧会显示在这样的形式下。

第一章:设置环境和工具 - 后端

在本章中,我们将为使用 Spring Boot 进行后端编程设置环境和工具。Spring Boot 是一个现代的基于 Java 的后端框架,使开发速度比传统的基于 Java 的框架更快。使用 Spring Boot,您可以创建一个具有嵌入式应用服务器的独立 Web 应用程序。

在本章中,我们将研究以下内容:

  • 为 Spring Boot 开发构建环境

  • Eclipse IDE 和 Maven 的基础知识

  • 创建和运行 Spring Boot 项目

  • 解决运行 Spring Boot 应用程序的常见问题

技术要求

使用 Eclipse IDE 需要 Java SDK 版本 8 或更高版本。

在本书中,我们使用的是 Windows 操作系统,但所有工具也适用于 Linux 和 macOS。

设置环境和工具

有许多不同的 IDE 工具可用于开发 Spring Boot 应用程序。在本书中,我们使用 Eclipse,这是一个用于多种编程语言的开源 IDE。我们将通过使用 Spring Initializr 项目启动页面来创建我们的第一个 Spring Boot 项目。然后将项目导入 Eclipse 并执行。阅读控制台日志是开发 Spring Boot 应用程序时的关键技能。

安装 Eclipse

Eclipse 是由 Eclipse 基金会开发的开源编程 IDE。安装包可以从www.eclipse.org/downloads下载。Eclipse 适用于 Windows、Linux 和 macOS。您应该下载最新版本的 Eclipse IDE for Java EE developers。

您可以下载 Eclipse 的 ZIP 包或执行安装向导的安装程序包。如果使用 ZIP 包,您只需将包解压到本地磁盘上,它将包含一个可执行的Eclipse.exe文件,您可以通过双击该文件来运行它。

Eclipse 和 Maven 的基础知识

Eclipse 是用于多种编程语言的 IDE,如 Java、C++和 Python。Eclipse 包含不同的透视图以满足您的需求。透视图是 Eclipse 工作台中的一组视图和编辑器。以下屏幕截图显示了 Java 开发的常见透视图:

在左侧,我们有项目资源管理器,可以在其中查看项目结构和资源。项目资源管理器也用于双击打开文件。文件将在工作台中间的编辑器中打开。控制台视图位于工作台的下部。控制台视图非常重要,因为它显示应用程序的日志消息。

如果您愿意,可以为 Eclipse 获取Spring Tool SuiteSTS),但在本书中我们不会使用它,因为纯净的 Eclipse 安装已经足够满足我们的需求。STS 是一组插件,使 Spring 应用程序开发更加简单(spring.io/tools)。

Apache Maven 是一个软件项目管理工具。Maven 的基础是项目对象模型pom)。Maven 使软件开发过程更加简单,也统一了开发过程。您也可以在 Spring Boot 中使用另一个名为 Gradle 的项目管理工具,但在本书中,我们将专注于使用 Maven。

pom 是一个包含有关项目的基本信息的pom.xml文件。还有 Maven 应该下载的所有依赖项以能够构建项目。

可以在pom.xml文件的开头找到有关项目的基本信息,例如应用程序的版本、打包格式等。

pom.xml文件的最低版本应包含项目根目录、modelVersiongroupIdartifactIdversion

依赖项在依赖项部分中定义,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<project  
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.packt</groupId>
  <artifactId>cardatabase</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>cardatabase</name>
  <description>Demo project for Spring Boot</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Maven 通常是从命令行中使用的。Eclipse 包含嵌入的 Maven,并处理我们需要的所有 Maven 操作。因此,我们在这里不专注于 Maven 命令行的使用。最重要的是要了解pom.xml文件的结构以及如何向其中添加新的依赖项。

使用 Spring Initializr 创建项目

我们将使用 Spring Intializr 创建我们的后端项目,这是一个用于创建 Spring Boot 项目的基于 Web 的工具。Spring Intializr 可以在start.spring.io找到:

我们将使用 Java 和最新的 Spring Boot 版本生成一个 Maven 项目。在“Group”字段中,我们将定义我们的 group ID,这也将成为我们 Java 项目中的基本包。在“Artifact”字段中,我们将定义 artifact ID,这也将是我们在 Eclipse 中项目的名称。

在“Dependencies”部分,我们将选择我们项目中需要的启动器和依赖项。Spring Boot 提供了简化 Maven 配置的启动器包。Spring Boot 启动器实际上是一组您可以包含在项目中的依赖项。您可以在搜索字段中键入依赖项的关键字,也可以点击“切换到完整版本”链接查看所有可用的依赖项。我们将通过选择两个依赖项——Web 和 DevTools 来启动我们的项目。您可以在搜索字段中键入依赖项,也可以切换到完整版本并查看所有可用的启动器包和依赖项:

DevTools 依赖项为我们提供了 Spring Boot 开发工具,提供了自动重启功能。这样做可以加快开发速度,因为应用程序在保存更改后会自动重新启动。Web 启动器包是全栈开发的基础,并提供了嵌入式 Tomcat。

最后,您必须按“Generate Project”按钮,这将为我们生成项目启动器 ZIP 包。

如何运行项目

  1. 在上一个主题中创建的项目 ZIP 包中提取并打开 Eclipse。

  2. 我们将把项目导入到 Eclipse IDE 中。要开始导入过程,请选择“文件|导入”菜单,导入向导将打开。以下屏幕截图显示了向导的第一页:

  1. 在第一阶段,您应该从Maven文件夹下的列表中选择“Existing Maven Projects”,然后按“Next”按钮进入下一阶段。以下屏幕截图显示了导入向导的第二步:

  1. 在此阶段,通过按“Browse...”按钮选择提取的项目文件夹。然后,Eclipse 会找到项目文件夹根目录下的pom.xml文件,并在窗口的“Projects”部分中显示它。

  2. 按“Finish”按钮完成导入。如果一切顺利,您应该在 Eclipse 项目资源管理器中看到cardatabase项目。项目准备就绪需要一段时间,因为所有依赖项将在导入后由 Maven 加载。您可以在 Eclipse 右下角看到依赖项下载的进度。以下屏幕截图显示了成功导入后的 Eclipse 项目资源管理器:

项目资源管理器还显示了我们项目的包结构,现在一开始只有一个名为com.packt.cardatabase的包。在该包下是我们的主应用程序类,名为CardatabaseApplication.java

  1. 现在,我们的应用程序中没有任何功能,但我们可以运行它并查看是否一切顺利启动。要运行项目,请双击打开主类,然后在 Eclipse 工具栏中按“Run”按钮,或者选择运行菜单并按“Run as | Java Application”:

您可以在 Eclipse 中看到控制台视图打开,其中包含有关项目执行的重要信息。这是所有日志文本和错误消息出现的视图,因此在出现问题时检查视图的内容非常重要。

现在,如果项目被正确执行,您应该在控制台的末尾看到文本Started CardatabaseApplication in...。以下屏幕截图显示了我们的 Spring Boot 项目启动后 Eclipse 控制台的内容:

在我们项目的根目录中有pom.xml文件,这是我们项目的 Maven 配置文件。如果您查看文件中的依赖项,您会发现现在有我们在 Spring Initializr 页面上选择的依赖项。还有一个测试依赖项自动包含,无需任何选择。在接下来的章节中,我们将为我们的应用程序添加更多功能,然后我们将手动向pom.xml文件添加更多依赖项:

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

让我们仔细看一下 Spring Boot 主类。在类的开头,有@SpringBootApplication注释。实际上,它是多个注释的组合,例如以下内容:

注释 描述
@EnableAutoConfiguration 启用 Spring Boot 自动配置。Spring Boot 将根据依赖项自动配置您的项目。例如,如果您有spring-boot-starter-web依赖项,Spring Boot 会假定您正在开发 Web 应用程序,并相应地配置您的应用程序。
@ComponentScan 启用 Spring Boot 组件扫描,以查找应用程序中的所有组件。
@Configure 定义可用作 bean 定义来源的类。

以下代码显示了 Spring Boot 应用程序的main类:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CardatabaseApplication {

  public static void main(String[] args) {
    SpringApplication.run(CardatabaseApplication.class, args);
  }
}

应用程序的执行从main方法开始,就像标准的 Java 应用程序一样。

建议将main应用程序类放在其他类上方的根包中。应用程序无法正确工作的一个常见原因是 Spring Boot 无法找到一些关键类。

Spring Boot 开发工具

Spring Boot 开发工具使应用程序开发过程更加轻松。如果将以下依赖项添加到 Maven 的pom.xml文件中,项目将包括开发人员工具:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <scope>runtime</scope>
    </dependency>

创建应用程序的完全打包生产版本时,开发工具将被禁用。

当您对项目类路径文件进行更改时,应用程序会自动重新启动。您可以通过向main类添加一行注释来测试。保存文件后,您可以在控制台中看到应用程序已重新启动:

package com.packt.cardatabase;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CardatabaseApplication {

  public static void main(String[] args) {
    // After adding this comment the application is restarted
    SpringApplication.run(CardatabaseApplication.class, args);
  }
}

日志和问题解决

Spring Boot starter 包提供了一个 logback,我们可以在没有任何配置的情况下用于日志记录。以下示例代码显示了如何使用日志记录:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CardatabaseApplication {
  private static final Logger logger = LoggerFactory.getLogger(CardatabaseApplication.class);
  public static void main(String[] args) {
    SpringApplication.run(CardatabaseApplication.class, args);
    logger.info("Hello Spring Boot");
  }
}

运行项目后,可以在控制台中看到日志消息:

日志记录有七个不同的级别——TRACEDEBUGINFOWARNERRORFATALOFF。您可以在 Spring Boot 的application.properties文件中配置日志记录级别。该文件可以在项目内的resources文件夹中找到:

如果我们将日志级别设置为INFO,我们可以看到低于INFO级别(INFOWARNERRORFATAL)的日志消息。在下面的示例中,我们设置了根日志级别,但您也可以在包级别设置它:

logging.level.root=INFO

现在,当您运行项目时,您将不再看到TRACEDEBUG消息。这可能是应用程序生产版本的良好设置:

Spring Boot 默认使用 Apache Tomcat (tomcat.apache.org/)作为应用服务器。默认情况下,Tomcat 在端口8080上运行。您可以在application.properties文件中更改端口。以下设置将在端口8081上启动 Tomcat:

server.port=8081

如果端口被占用,应用程序将无法启动,并且您将在控制台中看到以下消息:

您必须停止监听端口8080的进程,或在 Spring Boot 应用程序中使用另一个端口。

安装 MariaDB

在下一章中,我们将使用 MariaDB,因此我们将在本地计算机上安装它。MariaDB 是一个广泛使用的开源关系数据库。MariaDB 适用于 Windows 和 Linux,您可以从downloads.mariadb.org/下载最新稳定版本。MariaDB 是在 GNU GPL 2 许可下开发的。

对于 Windows,有 MSI 安装程序,我们将在这里使用。下载安装程序并执行它。从安装向导中安装所有功能:

在下一步中,您应该为 root 用户提供密码。在下一章中,我们连接到数据库时需要此密码:

在下一阶段,我们可以使用默认设置:

现在安装开始了,MariaDB 将安装到您的本地计算机上。安装向导将为我们安装HeidiSQL。这是一个图形化易于使用的数据库客户端。我们将使用它来添加新数据库并对我们的数据库进行查询。您还可以使用安装包中包含的命令提示符:

总结

在本章中,我们安装了使用 Spring Boot 进行后端开发所需的工具。对于 Java 开发,我们使用了 Eclipse IDE,这是一个广泛使用的编程 IDE。我们通过使用 Spring Initializr 页面创建了一个新的 Spring Boot 项目。创建项目后,它被导入到 Eclipse 中,并最终执行。我们还介绍了如何解决 Spring Boot 的常见问题以及如何查找重要的错误和日志消息。最后,我们安装了一个 MariaDB 数据库,我们将在下一章中使用。

问题

  1. Spring Boot 是什么?

  2. Eclipse IDE 是什么?

  3. Maven 是什么?

  4. 我们如何创建一个 Spring Boot 项目?

  5. 我们如何运行 Spring Boot 项目?

  6. 我们如何在 Spring Boot 中使用日志记录?

  7. 我们如何在 Eclipse 中查找错误和日志消息?

进一步阅读

Packt 还有其他很好的资源可供学习 Spring Boot:

第二章:使用 JPA 创建和访问数据库

本章介绍了如何在 Spring Boot 中使用 JPA。我们将使用实体类创建数据库。在第一阶段,我们将使用 H2 内存数据库进行开发和演示。H2 是一个内存中的 SQL 数据库,非常适合快速开发或演示目的。在第二阶段,我们将从 H2 转移到使用 MariaDB。本章还描述了 CRUD 存储库的创建以及数据库表之间的一对多连接。

在本章中,我们将研究以下内容:

  • 使用 JPA 的基础知识和好处

  • 如何使用实体类定义数据库

  • 如何使用 Spring Boot 后端创建数据库

技术要求

使用 Spring Boot 需要 Java SDK 版本 8 或更高版本(www.oracle.com/technetwork/java/javase/downloads/index.html)。

为了创建数据库应用程序,需要安装 MariaDB(downloads.mariadb.org/)。

ORM、JPA 和 Hibernate 的基础知识和好处

对象关系映射ORM)是一种技术,允许您使用面向对象的编程范式从数据库中提取和操作数据。ORM 对程序员来说非常好,因为它依赖于面向对象的概念,而不是数据库结构。它还可以加快开发速度,减少源代码量。ORM 大多数独立于数据库,开发人员不必担心特定于供应商的 SQL 语句。

Java 持久 APIJPA)为 Java 开发人员提供了对象关系映射。JPA 实体是一个 Java 类,表示数据库表的结构。实体类的字段表示数据库表的列。

Hibernate 是最流行的基于 Java 的 JPA 实现,它在 Spring Boot 中作为默认使用。Hibernate 是一个成熟的产品,在大型应用程序中被广泛使用。

创建实体类

实体类是一个简单的 Java 类,带有 JPA 的@Entity注解。实体类使用标准的 JavaBean 命名约定,并具有适当的 getter 和 setter 方法。类字段具有私有可见性。

当应用程序初始化时,JPA 会创建一个名为类名的数据库表。如果要为数据库表使用其他名称,可以使用@Table注解。

为了能够使用 JPA 和 H2 数据库,我们必须将以下依赖项添加到pom.xml文件中:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>

以下是创建实体类的步骤:

  1. 要在 Spring Boot 中创建实体类,我们首先将为实体创建自己的包。该包应该在根包下创建。

  2. 在 Eclipse 项目资源管理器中激活根包,右键单击显示菜单。

  3. 从菜单中选择 New | Package。以下截图显示了为实体类创建包:

  1. 我们将包命名为com.packt.cardatabase.domain

  1. 接下来,我们创建实体类。激活一个新的实体包,右键单击,选择菜单中的 New | Class。因为我们要创建一个汽车数据库,实体类的名称是Car。在Name字段中输入Car,然后按下Finish按钮:

  1. 在项目资源管理器中双击打开编辑器中的Car类文件。首先,我们必须使用@Entity注解对类进行注释。Entity注解从javax.persistence包中导入:
      package com.packt.cardatabase.domain;

      import javax.persistence.Entity;

      @Entity
      public class Car {

      }

您可以使用 Eclipse IDE 中的Ctrl + Shift + O快捷键自动导入缺少的包。

  1. 接下来,我们为我们的类添加一些字段。实体类字段映射到数据库表列。实体类还必须包含一个唯一的 ID,该 ID 用作数据库中的主键:
      package com.packt.cardatabase.domain;

      import javax.persistence.Entity;
      import javax.persistence.GeneratedValue;
      import javax.persistence.GenerationType;
      import javax.persistence.Id;

      @Entity
      public class Car {
        @Id
        @GeneratedValue(strategy=GenerationType.AUTO)
        private long id;
        private String brand, model, color, registerNumber;
        private int year, price;
      }

使用@Id注释定义主键。@GeneratedValue注释定义 ID 由数据库自动生成。我们还可以定义我们的键生成策略。类型AUTO表示 JPA 提供程序为特定数据库选择最佳策略。您还可以通过使用@Id注释对多个属性进行注释来创建复合主键。

默认情况下,数据库列的命名按类字段命名。如果要使用其他命名约定,可以使用@Column注释。使用@Column注释,还可以定义列的长度以及列是否可为空。以下代码显示了使用@Column注释的示例。通过这个定义,在数据库中列的名称是desc,列的长度是512,并且它是不可为空的:

@Column(name="desc", nullable=false, length=512)
private String description
  1. 最后,我们为实体类添加 getter、setter 和带属性的构造函数。由于自动生成 ID,我们不需要在构造函数中添加 ID 字段。Car实体类构造函数的源代码如下:

Eclipse 提供了自动生成 getter、setter 和构造函数的功能。将光标放在类内并右键单击。从菜单中选择“Source | Generate Getters and Setters...”或“Source | Generate Constructor using fields...”

package com.packt.cardatabase.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Car {
  @Id
  @GeneratedValue(strategy=GenerationType.AUTO)
  private long id;
  private String brand, model, color, registerNumber;
  private int year, price;

  public Car() {}

  public Car(String brand, String model, String color, 
    String registerNumber, int year, int price) {
    super();
    this.brand = brand;
    this.model = model;
    this.color = color;
    this.registerNumber = registerNumber;
    this.year = year;
    this.price = price;
  }

以下是Car实体类的 getter 和 setter 的源代码:

  public String getBrand() {
    return brand;
  }
  public void setBrand(String brand) {
    this.brand = brand;
  }
  public String getModel() {
    return model;
  }
  public void setModel(String model) {
    this.model = model;
  }
  public String getColor() {
    return color;
  }
  public void setColor(String color) {
    this.color = color;
  }
  public String getRegisterNumber() {
    return registerNumber;
  }
  public void setRegisterNumber(String registerNumber) {
    this.registerNumber = registerNumber;
  }
  public int getYear() {
    return year;
  }
  public void setYear(int year) {
    this.year = year;
  }
  public int getPrice() {
    return price;
  }
  public void setPrice(int price) {
    this.price = price;
  } 
}

当我们运行应用程序时,数据库中必须创建名为car的表。为了确保这一点,我们将在application.properties文件中添加一个新属性。这将使 SQL 语句的日志记录到控制台:

spring.jpa.show-sql=true

当运行应用程序时,我们现在可以看到表创建语句:

H2 提供了一个基于 Web 的控制台,可用于探索数据库并执行 SQL 语句。要启用控制台,我们必须将以下行添加到application.properties文件中。第一个设置启用 H2 控制台,第二个设置定义了我们可以使用的端点来访问控制台:

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

您可以通过在 Web 浏览器中导航到localhost:8080/h2-console来访问 H2 控制台。在登录窗口中,使用jdbc:h2:mem:testdb作为 JDBC URL,并在密码字段中留空。按下“连接”按钮登录到控制台:

现在您可以在数据库中看到我们的car表。您可能会注意到注册号之间有一个下划线。这是由于属性(registerNumber)的驼峰命名法:

创建 CRUD 存储库

Spring Boot Data JPA 为 CRUD 操作提供了CrudRepository接口。它为我们的实体类提供了 CRUD 功能。

现在,我们将在domain包中创建我们的存储库,如下所示:

  1. domain包中创建一个名为CarRepository的新类,并根据以下代码片段修改文件:
      package com.packt.cardatabase.domain;

      import org.springframework.data.repository.CrudRepository;

      public interface CarRepository extends CrudRepository <Car, Long> {

      }

我们的CarRepository现在扩展了 Spring Boot JPA 的CrudRepository接口。<Car, Long>类型参数定义了这是Car实体类的存储库,ID 字段的类型是 long。

CrudRepository提供了多个 CRUD 方法,我们现在可以开始使用。以下表列出了最常用的方法:

方法 描述
long count() 返回实体的数量
Iterable<T> findAll() 返回给定类型的所有项目
Optional<T> findById(ID Id) 通过 id 返回一个项目
void delete(T entity) 删除实体
void deleteAll() 删除存储库的所有实体
<S extends T> save(S entity) 保存实体

如果方法只返回一个项目,则返回Optional<T>而不是TOptional类在 Java 8 SE 中引入。Optional是一种单值容器类型,可以有值,也可以没有。通过使用Optional,我们可以防止空指针异常。

  1. 现在我们准备向我们的 H2 数据库添加一些演示数据。为此,我们将使用 Spring Boot 的CommandLineRunnerCommandLineRunner接口允许我们在应用程序完全启动之前执行额外的代码。因此,这是向数据库添加演示数据的好时机。CommandLineRunner位于主类中:
      import org.springframework.boot.CommandLineRunner;
      import org.springframework.boot.SpringApplication;
      import org.springframework.boot.autoconfigure.SpringBootApplication;
      import org.springframework.context.annotation.Bean;

      @SpringBootApplication
      public class CardatabaseApplication {

        public static void main(String[] args) {
          SpringApplication.run(CardatabaseApplication.class, args);
        }

        @Bean
        CommandLineRunner runner(){
          return args -> {
            // Place your code here
          };
        } 
      }
  1. 接下来,我们必须将我们的 car repository 注入到主类中,以便能够将新的 car 对象保存到数据库中。使用@Autowired注解来启用依赖注入。依赖注入允许我们将依赖项传递给对象。在我们注入了存储库类之后,我们可以使用它提供的 CRUD 方法。以下示例代码显示了如何向数据库中插入一些汽车:
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.CommandLineRunner;
      import org.springframework.boot.SpringApplication;
      import org.springframework.boot.autoconfigure.SpringBootApplication;
      import org.springframework.context.annotation.Bean;

      import com.packt.cardatabase.domain.Car;
      import com.packt.cardatabase.domain.CarRepository;

      @SpringBootApplication
      public class CardatabaseApplication {
        @Autowired 
        private CarRepository repository;

        public static void main(String[] args) {
          SpringApplication.run(CardatabaseApplication.class, args);
        }

        @Bean
        CommandLineRunner runner(){
          return args -> {
            // Save demo data to database
            repository.save(new Car("Ford", "Mustang", "Red",
             "ADF-1121", 2017, 59000));
            repository.save(new Car("Nissan", "Leaf", "White",
             "SSJ-3002", 2014, 29000));
            repository.save(new Car("Toyota", "Prius", "Silver",
             "KKO-0212", 2018, 39000));
          };
        } 
      }

Insert语句可以在应用程序执行后在 Eclipse 控制台中看到:

您还可以使用 H2 控制台从数据库中获取汽车,如下截图所示:

您可以在 Spring Data 存储库中定义自己的查询。查询必须以前缀开头,例如findBy。在前缀之后,您定义在查询中使用的实体类字段。以下是三个简单查询的示例代码:

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CarRepository extends CrudRepository <Car, Long> {
  // Fetch cars by brand
  List<Car> findByBrand(String brand);

  // Fetch cars by color
  List<Car> findByColor(String color);

  // Fetch cars by year
  List<Car> findByYear(int year);

}

By关键字之后可以有多个字段,使用AndOr关键字连接:

package com.packt.cardatabase.domain;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CarRepository extends CrudRepository <Car, Long> {
  // Fetch cars by brand and model
  List<Car> findByBrandAndModel(String brand, String model);

  // Fetch cars by brand or color
  List<Car> findByBrandOrColor(String brand, String color); 
}

可以使用查询方法中的OrderBy关键字对查询进行排序:

package com.packt.cardatabase.domain;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CarRepository extends CrudRepository <Car, Long> {
  // Fetch cars by brand and sort by year
  List<Car> findByBrandOrderByYearAsc(String brand);
}

您还可以通过@Query注解使用 SQL 语句创建查询。以下示例展示了在CrudRepository中使用 SQL 查询的用法:

package com.packt.cardatabase.domain;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CarRepository extends CrudRepository <Car, Long> {
  // Fetch cars by brand using SQL
  @Query("select c from Car c where c.brand = ?1")
  List<Car> findByBrand(String brand);
}

您还可以使用@Query注解进行更高级的表达式,例如like。以下示例展示了在CrudRepository中使用like查询的用法:

package com.packt.cardatabase.domain;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CarRepository extends CrudRepository <Car, Long> {
  // Fetch cars by brand using SQL
  @Query("select c from Car c where c.brand like %?1")
  List<Car> findByBrandEndsWith(String brand);
}

Spring Data JPA 还提供了PagingAndSortingRepository,它扩展了CrudRepository。它提供了使用分页和排序获取实体的方法。如果您处理大量数据,这是一个很好的选择。PagingAndSortingRepository可以类似于我们使用CrudRepository创建:

package com.packt.cardatabase.domain;

import org.springframework.data.repository.PagingAndSortingRepository;

public interface CarRepository extends PagingAndSortingRepository<Car, Long> {

}

在这种情况下,您现在拥有了存储库提供的两个新的附加方法:

方法 描述
Iterable<T> findAll(Sort sort) 返回按给定选项排序的所有实体
Page<T> findAll(Pageable pageable) 根据给定的分页选项返回所有实体

表之间的关系

接下来,我们创建一个名为owner的新表,它与car表具有一对多的关系。所有者可以拥有多辆汽车,但一辆汽车只能有一个所有者。以下的 UML 图显示了表之间的关系:

以下是创建新表的步骤:

  1. 首先,在domain包中创建Owner实体和存储库。Owner实体和存储库的创建方式与Car类相似。以下是Owner实体类和OwnerRepository的源代码:
      // Owner.java

      package com.packt.cardatabase.domain;

      import javax.persistence.Entity;
      import javax.persistence.GeneratedValue;
      import javax.persistence.GenerationType;
      import javax.persistence.Id;

      @Entity
      public class Owner {
        @Id
        @GeneratedValue(strategy=GenerationType.AUTO)
        private long ownerid;
        private String firstname, lastname;

        public Owner() {}

        public Owner(String firstname, String lastname) {
          super();
          this.firstname = firstname;
          this.lastname = lastname;
        }

        public long getOwnerid() {
          return ownerid;
        }
        public void setOwnerid(long ownerid) {
          this.ownerid = ownerid;
        }
        public String getFirstname() {
          return firstname;
        }
        public void setFirstname(String firstname) {
          this.firstname = firstname;
        }
        public String getLastname() {
          return lastname;
        }
        public void setLastname(String lastname) {
          this.lastname = lastname;
        } 
      }
      // OwnerRepository.java

      package com.packt.cardatabase.domain;

      import org.springframework.data.repository.CrudRepository;

      public interface OwnerRepository extends CrudRepository<Owner, Long> 
      {

      }
  1. 在这个阶段,检查一切是否正确完成是很重要的。运行项目并检查数据库表是否都已创建,并且控制台中没有错误。下面的截图显示了在创建表时控制台的消息:

现在,我们的 domain 包含两个实体类和存储库:

  1. 一对多关系可以通过使用@ManyToOne@OneToMany注解来添加。在包含外键的汽车实体类中,您将使用@ManyToOne注解定义与所有者的关系。还要为所有者字段添加 getter 和 setter。建议对所有关联使用FetchType.LAZY。对于toMany关系,这是默认值,但对于toOne关系,您应该定义它。FetchType定义了从数据库中获取数据的策略。该值可以是EAGERLAZY。在我们的情况下,懒惰策略意味着当从数据库中获取所有者时,将在需要时获取与所有者关联的所有汽车。Eager意味着汽车将立即与所有者一起获取。以下源代码显示了如何在Car类中定义一对多关系:
      // Car.java

      @ManyToOne(fetch = FetchType.LAZY)
      @JoinColumn(name = "owner")
      private Owner owner;

      //Getter and setter
      public Owner getOwner() {
        return owner;
      }

      public void setOwner(Owner owner) {
        this.owner = owner;
      }

在所有者实体站点上,使用@OneToMany注解定义了关系。字段的类型是List<Car>,因为所有者可能拥有多辆汽车。还为此添加 getter 和 setter:

      // Owner.java  

      @OneToMany(cascade = CascadeType.ALL, mappedBy="owner")
      private List<Car> cars;

      //Getter and setter
      public List<Car> getCars() {
        return cars;
      }

      public void setCars(List<Car> cars) {
        this.cars = cars;
      }

@OneToMany注解有两个我们正在使用的属性。cascade属性定义了级联如何影响实体。属性设置ALL意味着如果所有者被删除,与该所有者关联的汽车也将被删除。mappedBy="owner"属性设置告诉我们Car类具有所有者字段,这是该关系的外键。

当您运行项目时,您可以从控制台看到关系现在已经创建:

  1. 现在,我们可以使用CommandLineRunner向数据库添加一些所有者。让我们还修改Car实体类的构造函数并在那里添加一个所有者:
      // Car.java constructor 

      public Car(String brand, String model, String color,
      String registerNumber, int year, int price, Owner owner) {
        super();
        this.brand = brand;
        this.model = model;
        this.color = color;
        this.registerNumber = registerNumber;
        this.year = year;
        this.price = price;
        this.owner = owner;
      }
  1. 我们首先创建两个所有者对象并将其保存到数据库中。为了保存所有者,我们还必须将OwnerRepository注入到主类中。然后我们通过Car构造函数将所有者连接到汽车。以下是应用程序主类CardatabaseApplication的源代码:
      @SpringBootApplication
      public class CardatabaseApplication {
        // Inject repositories
        @Autowired 
        private CarRepository repository;

        @Autowired 
        private OwnerRepository orepository;

        public static void main(String[] args) {
          SpringApplication.run(CardatabaseApplication.class, args);
        }

        @Bean
        CommandLineRunner runner() {
          return args -> {
            // Add owner objects and save these to db
            Owner owner1 = new Owner("John" , "Johnson");
            Owner owner2 = new Owner("Mary" , "Robinson");
            orepository.save(owner1);
            orepository.save(owner2);

            // Add car object with link to owners and save these to db.
            Car car = new Car("Ford", "Mustang", "Red", 
                "ADF-1121", 2017, 59000, owner1);
            repository.save(car);
            car = new Car("Nissan", "Leaf", "White",
                "SSJ-3002", 2014, 29000, owner2);
            repository.save(car);
            car = new Car("Toyota", "Prius", "Silver",
                "KKO-0212", 2018, 39000, owner2);
            repository.save(car);
          };
        } 
      }

如果现在运行应用程序并从数据库中获取汽车,您会发现所有者现在与汽车关联起来了。

如果要创建多对多关系,也就是说,在实践中,一个所有者可以拥有多辆汽车,一辆汽车可以有多个所有者,您应该使用@ManyToMany注解。在我们的示例应用程序中,我们将使用一对多关系,但以下是如何将关系更改为多对多的示例。在多对多关系中,建议使用Set而不是List与 hibernate 一起使用:

  1. Car实体类的多对多关系中,以以下方式定义 getter 和 setter:
      @ManyToMany(mappedBy = "cars") 
      private Set<Owner> owners; 

      public Set<Owner> getOwners() {
        return owners;
      }

      public void setOwners(Set<Owner> owners) {
        this.owners = owners;
      }

在所有者实体中,定义如下:

      @ManyToMany(cascade = CascadeType.MERGE)
      @JoinTable(name = "car_owner", joinColumns = { @JoinColumn(name =
       "ownerid") }, inverseJoinColumns = { @JoinColumn(name = "id") }) 
      private Set<Car> cars = new HashSet<Car>(0); 

      public Set<Car> getCars() {
        return cars;
      }

      public void setCars(Set<Car> cars) {
        this.cars = cars;
      }
  1. 现在,如果运行应用程序,将创建一个新的连接表,该表位于carowner表之间。使用@JoinTable注解定义连接表。通过该注解,我们可以设置连接表的名称和连接列。以下是在使用多对多关系时数据库结构的屏幕截图:

设置 MariaDB 数据库

现在,我们将数据库从 H2 切换到 MariaDB。数据库表仍然由 JPA 自动创建。但在运行应用程序之前,我们必须为其创建一个数据库。可以使用 HeidiSQL 创建数据库。打开 HeidiSQL,并按照以下步骤操作:

  1. 右键单击数据库列表中的鼠标。

  2. 然后,选择新建|数据库:

  1. 让我们将数据库命名为cardb。按下 OK 后,您应该在数据库列表中看到新的cardb

  1. 在应用程序中,我们向pom.xml文件添加了 MariaDB 依赖项,并删除了不再需要的 H2 依赖项:
      <dependency>
        <groupId>org.mariadb.jdbc</groupId>
        <artifactId>mariadb-java-client</artifactId>
      </dependency> 
  1. application.properties文件中,您定义了数据库连接。首先,您将定义数据库的urlusernamepassword和数据库驱动程序类。spring.jpa.generate-ddl设置定义了 JPA 是否应初始化数据库(true/false)。spring.jpa.hibernate.ddl-auto设置定义了数据库初始化的行为。可能的值有nonevalidateupdatecreatecreate-drop。Create-drop 意味着在应用程序启动时创建数据库,并在应用程序停止时删除数据库。如果您不定义任何值,create-drop 也是默认值。Create 值仅在应用程序启动时创建数据库。Update 值创建数据库并在架构更改时更新架构。
      spring.datasource.url=jdbc:mariadb://localhost:3306/cardb
      spring.datasource.username=root
      spring.datasource.password=YOUR_PASSWORD
      spring.datasource.driver-class-name=org.mariadb.jdbc.Driver

      spring.jpa.generate-ddl=true
      spring.jpa.hibernate.ddl-auto=create-drop
  1. 现在,在运行应用程序后,您应该在 MariaDB 中看到表。以下截图显示了数据库创建后的 HeidiSQL UI。您的应用程序现在已准备好与 MariaDB 一起使用:

总结

在本章中,我们使用 JPA 来创建 Spring Boot 应用程序数据库。首先,我们创建了实体类,这些类映射到数据库表。然后,我们为我们的实体类创建了CrudRepository,它为实体提供了 CRUD 操作。之后,我们通过使用CommandLineRunner成功向我们的数据库添加了一些演示数据。我们还在两个实体之间创建了一对多的关系。在本章的开头,我们使用了 H2 内存数据库,而在结尾,我们将数据库切换到了 MariaDB。在下一章中,我们将为我们的后端创建 RESTful web 服务。

问题

  1. ORM、JPA 和 Hibernate 是什么?

  2. 如何创建实体类?

  3. 如何创建CrudRepository

  4. CrudRepository如何为您的应用程序提供支持?

  5. 如何在表之间创建一对多的关系?

  6. 如何使用 Spring Boot 向数据库添加演示数据?

  7. 如何访问 H2 控制台?

  8. 如何将 Spring Boot 应用程序连接到 MariaDB?

进一步阅读

Packt 还有其他学习 Spring Boot 的资源:

第三章:使用 Spring Boot 创建 RESTful Web 服务

在本章中,我们将首先使用控制器类创建一个 RESTful Web 服务。之后,我们将演示如何使用 Spring Data REST 创建一个自动覆盖所有 CRUD 功能的 RESTful Web 服务。我们将使用前一章中创建的数据库应用作为起点。

在本章中,我们将研究以下内容:

  • RESTful Web 服务是什么

  • 如何使用 Spring Boot 创建 RESTful Web 服务

  • 如何测试 RESTful web 服务

技术要求

之前创建的 Spring Boot 应用程序是必需的。

需要使用 Postman、cURL 或其他适当的工具来使用各种 HTTP 方法传输数据。

使用 Spring Boot 创建 RESTful Web 服务

Web 服务是使用 HTTP 协议在互联网上进行通信的应用程序。有许多不同类型的 Web 服务架构,但所有设计的主要思想都是相同的。在本书中,我们正在从目前非常流行的设计中创建一个 RESTful Web 服务。

REST 的基础知识

REST表述状态转移)是一种用于创建 Web 服务的架构风格。REST 不是标准,但它定义了一组由 Roy Fielding 定义的约束。这六个约束如下:

  • 无状态:服务器不保存有关客户端状态的任何信息。

  • 客户端服务器:客户端和服务器独立运行。服务器不会在没有客户端请求的情况下发送任何信息。

  • 可缓存:许多客户端经常请求相同的资源,因此缓存响应以提高性能是有用的。

  • 统一接口:来自不同客户端的请求看起来是相同的。客户端可以是浏览器、Java 应用程序和移动应用程序等。

  • 分层系统:REST 允许我们使用分层系统架构。

  • 按需编码:这是一个可选的约束。

统一接口是一个重要的约束,它定义了每个 REST 架构应该具有以下元素:

  • 资源的识别:有资源及其唯一标识符,例如基于 Web 的 REST 服务中的 URI。REST 资源应该公开易于理解的目录结构 URI。因此,良好的资源命名策略非常重要。

  • 通过表示来操作资源:当向资源发出请求时,服务器会以资源的表示形式做出响应。通常,表示的格式是 JSON 或 XML。

  • 自描述消息:消息应该包含足够的信息,以便服务器知道如何处理它们。

  • 超媒体和应用状态引擎(HATEOAS):响应可以包含到服务的其他区域的链接。

在接下来的主题中,我们将开发一个遵循 REST 架构原则的 RESTful Web 服务。

创建 RESTful Web 服务

在 Spring Boot 中,所有的 HTTP 请求都由控制器类处理。为了能够创建一个 RESTful web 服务,首先我们必须创建一个控制器类。我们将为我们的控制器创建自己的 Java 包:

  1. 在 Eclipse 项目资源管理器中激活根包并右键单击。从菜单中选择 New | Package。我们将为我们的新包命名为com.packt.cardatabase.web

  1. 接下来,我们将在一个新的 Web 包中创建一个新的控制器类。在 Eclipse 项目资源管理器中激活com.packt.cardatabase.web包并右键单击。从菜单中选择 New | Class。我们将为我们的类命名为CarController

  1. 现在,您的项目结构应该如下截图所示:

如果您意外地在错误的包中创建类,您可以在 Eclipse 项目资源管理器之间拖放文件。有时,当您进行一些更改时,Eclipse 项目资源管理器视图可能无法正确呈现。刷新项目资源管理器有所帮助(激活项目资源管理器并按F5)。

  1. 在编辑窗口中打开您的控制器类,并在类定义之前添加@RestController注解。请参阅以下源代码。@RestController注解标识这个类将成为 RESTful Web 服务的控制器:
      package com.packt.cardatabase.web;

      import org.springframework.web.bind.annotation.RestController;

      @RestController
      public class CarController { 
      }
  1. 接下来,我们在我们的控制器类中添加一个新的方法。该方法使用@RequestMapping注解进行标注,定义了方法映射到的端点。接下来,您可以看到示例源代码。在这个例子中,当用户导航到/cars端点时,getCars()方法被执行:
      package com.packt.cardatabase.web;

      import org.springframework.web.bind.annotation.RestController;

      @RestController
      public class CarController {
        @RequestMapping("/cars")
        public Iterable<Car> getCars() {

        } 
      }

getCars()方法返回所有汽车对象,然后由 Jackson 库转换为 JSON 对象。

默认情况下,@RequestMapping处理所有 HTTP 方法(GETPUTPOST等)的请求。您可以使用以下@RequestMapping("/cars", method=GET)参数定义接受的方法。现在,这个方法只处理来自/cars端点的GET请求。

  1. 为了能够从数据库中返回汽车,我们必须将我们的CarRepository注入到控制器中。然后,我们可以使用存储库提供的findAll()方法来获取所有汽车。以下源代码显示了控制器代码:
      package com.packt.cardatabase.web;

      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;

      import com.packt.cardatabase.domain.Car;
      import com.packt.cardatabase.domain.CarRepository;

      @RestController
      public class CarController {
        @Autowired
        private CarRepository repository;

        @RequestMapping("/cars")
        public Iterable<Car> getCars() {
          return repository.findAll();
        }
      }
  1. 现在,我们准备运行我们的应用程序并导航到localhost:8080/cars。我们可以看到有些问题,应用程序似乎陷入了无限循环。这是由于我们的汽车和所有者表之间的一对多关系导致的。实际上会发生什么——首先,汽车被序列化,它包含一个所有者,然后被序列化,反过来,包含汽车,然后被序列化...依此类推。为了避免这种情况,我们必须在Owner类的cars字段上添加@JsonIgnore注解:
      // Owner.java

      @OneToMany(cascade = CascadeType.ALL, mappedBy="owner")
      @JsonIgnore
      private List<Car> cars;
  1. 现在,当您运行应用程序并导航到localhost:8080/cars时,一切都应该如预期般进行,并且您将以 JSON 格式从数据库中获取所有的汽车,如下截图所示:

我们已经完成了我们的第一个 RESTful Web 服务,它返回所有的汽车。Spring Boot 提供了一个更强大的方式来创建 RESTful Web 服务,这将在下一个主题中进行探讨。

使用 Spring Data REST

Spring Data REST 是 Spring Data 项目的一部分。它提供了一种简单快捷的方式来使用 Spring 实现 RESTful Web 服务。要开始使用 Spring Data REST,您必须将以下依赖项添加到pom.xml文件中:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>

默认情况下,Spring Data REST 会查找应用程序中的所有公共存储库,并为您的实体自动创建 RESTful Web 服务。

您可以在application.properties文件中定义服务的端点:

spring.data.rest.basePath=/api

现在,您可以从localhost:8080/api端点访问 RESTful Web 服务。通过调用服务的根端点,它返回可用的资源。Spring Data REST 以HAL超文本应用语言)格式返回 JSON 数据。HAL 格式提供了一套约定,用于在 JSON 中表示超链接,使得前端开发人员更容易使用您的 RESTful Web 服务:

我们可以看到有指向汽车和所有者实体服务的链接。Spring Data Rest 服务的路径名是从实体名称派生的。然后将名称变为复数形式并取消大写。例如,实体 Car 服务的路径名将被命名为cars。配置文件链接由 Spring Data Rest 生成,其中包含特定于应用程序的元数据。

现在,我们开始更仔细地检查不同的服务。有多种工具可用于测试和使用 RESTful Web 服务。在本书中,我们使用 Postman,但您也可以使用您熟悉的工具,如 cURL。Postman 可以作为桌面应用程序或作为浏览器插件获取。cURL 也可通过使用 Windows Ubuntu Bash 在 Windows 10 上使用。

如果您使用GET方法向http://localhost:8080/api/cars端点发出请求,您将获得所有cars的列表,如下面的屏幕截图所示:

在 JSON 响应中,您可以看到有一个cars数组,每辆汽车都包含特定的汽车数据。所有汽车还具有"_links"属性,这是一组链接,通过这些链接,您可以访问汽车本身或获取汽车的所有者。要访问特定的汽车,路径将是http://localhost:8080/api/cars/{id}

http://localhost:8080/api/cars/3/owner的请求返回汽车的所有者。响应现在包含所有者数据,指向所有者的链接以及用户拥有的其他cars的链接:

Spring Data Rest 服务提供所有 CRUD 操作。以下表格显示了您可以用于不同 CRUD 操作的 HTTP 方法:

HTTP 方法 CRUD
GET 读取
POST 创建
PUT/PATCH 更新
DELETE 删除

接下来,我们将看看如何通过使用我们的 RESTful web 服务从数据库中删除汽车。在删除操作中,您必须使用DELETE方法和将被删除的汽车的链接(http://localhost:8080/api/cars/{id})。以下屏幕截图显示了如何使用 cURL 删除 ID 为4的汽车。删除请求后,您可以看到数据库中现在只剩下两辆汽车:

当我们想要向数据库中添加新的汽车时,我们必须使用POST方法,链接是http://localhost:8080/api/cars。标头必须包含带有值Content-Type:application/json的 Content-Type 字段,并且新的汽车对象将嵌入在请求体中:

响应将发送一个新创建的汽车对象。现在,如果您再次对http://localhost:8080/api/cars路径发出GET请求,您可以看到新的汽车存在于数据库中:

要更新实体,我们必须使用PATCH方法和我们要更新的汽车的链接(http://localhost:8080/api/cars/{id})。标头必须包含带有值Content-Type:application/json的 Content-Type 字段,并且带有编辑数据的汽车对象将放在请求体中。如果您使用PATCH,您必须仅发送更新的字段。如果您使用PUT,您必须包含所有字段以请求。让我们编辑我们在上一个示例中创建的汽车。我们将颜色更改为白色,并填写我们留空的注册号码。

我们还将使用 owner 字段将所有者链接到汽车。owner 字段的内容是指向所有者的链接(http://localhost:8080/api/owners/{id})。以下屏幕截图显示了PATCH请求内容:

您可以看到,通过使用GET请求获取所有汽车后,汽车已更新:

在上一章中,我们创建了对我们的存储库的查询。这些查询也可以包含在我们的服务中。要包含查询,您必须将@RepositoryRestResource注释添加到存储库类中。查询参数使用@Param注释进行注释。以下源代码显示了我们带有这些注释的CarRepository

package com.packt.cardatabase.domain;

import java.util.List;

import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource
public interface CarRepository extends CrudRepository <Car, Long> {
  // Fetch cars by brand
  List<Car> findByBrand(@Param("brand") String brand);

  // Fetch cars by color
  List<Car> findByColor(@Param("color") String color);
}

现在,当你向http://localhost:8080/api/cars路径发出GET请求时,你会看到一个名为/search的新端点。调用http://localhost:8080/api/cars/search路径会返回以下响应:

从响应中,你可以看到我们的服务现在都有这两个查询。以下 URL 演示了如何按品牌获取汽车:

http://localhost:8080/api/cars/search/findByBrand?brand=Ford

总结

在本章中,我们使用 Spring Boot 创建了一个 RESTful web 服务。首先,我们创建了一个控制器和一个以 JSON 格式返回所有汽车的方法。接下来,我们使用 Spring Data REST 获得了一个具有所有 CRUD 功能的完全功能的 web 服务。我们涵盖了使用 CRUD 功能所需的不同类型的请求。最后,我们还将我们的查询包含在服务中。在下一章中,我们将使用 Spring Security 保护我们的后端。

问题

  1. REST 是什么?

  2. 你如何使用 Spring Boot 创建一个 RESTful web 服务?

  3. 你如何使用我们的 RESTful web 服务获取项目?

  4. 你如何使用我们的 RESTful web 服务删除项目?

  5. 你如何使用我们的 RESTful web 服务添加项目?

  6. 你如何使用我们的 RESTful web 服务更新项目?

  7. 你如何使用我们的 RESTful web 服务进行查询?

进一步阅读

Pack 还有其他关于学习 Spring Boot RESTful Web 服务的资源:

第四章:保护和测试您的后端

本章将解释如何保护和测试您的 Spring Boot 后端。我们将使用上一章中创建的数据库应用程序作为起点。

在本章中,我们将研究以下内容:

  • 如何使用 Spring Boot 保护您的 Spring Boot 后端

  • 如何使用 JWT 保护您的 Spring Boot 后端

  • 如何测试您的后端

技术要求

需要在之前章节创建的 Spring Boot 应用程序。

Spring Security

Spring Security (spring.io/projects/spring-security)为基于 Java 的 Web 应用程序提供安全服务。Spring Security 项目始于 2003 年,之前被称为 Spring 的 Acegi 安全系统。

默认情况下,Spring Security 启用以下功能:

  • 具有内存单个用户的AuthenticationManager bean。用户名为user,密码打印到控制台输出。

  • 忽略常见静态资源位置的路径,例如/css/images等。

  • 所有其他端点的 HTTP 基本安全。

  • 发布到 Spring ApplicationEventPublisher的安全事件。

  • 默认情况下启用常见的低级功能(HSTS、XSS、CSRF 等)。

您可以通过将以下依赖项添加到pom.xml文件中,将 Spring Security 包含在应用程序中:

 <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
 </dependency>

当您启动应用程序时,您可以从控制台看到 Spring Security 已创建一个内存用户,用户名为user。用户的密码可以在控制台输出中看到:

如果您对 API 端点进行GET请求,您将看到它现在是安全的,并且您将收到401 Unauthorized错误:

要能够成功进行GET请求,我们必须使用基本身份验证。以下截图显示了如何在 Postman 中进行操作。现在,通过身份验证,我们可以看到状态为 200 OK,并且响应已发送:

要配置 Spring Security 的行为,我们必须添加一个新的配置类,该类扩展了WebSecurityConfigurerAdapter。在应用程序的根包中创建一个名为SecurityConfig的新类。以下源代码显示了安全配置类的结构。@Configration@EnableWebSecurity注解关闭了默认的 Web 安全配置,我们可以在这个类中定义自己的配置。在configure(HttpSecurity http)方法中,我们可以定义应用程序中哪些端点是安全的,哪些不是。实际上,我们还不需要这个方法,因为我们可以使用所有端点都受保护的默认设置:

package com.packt.cardatabase;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {

  }

}

我们还可以通过在SecurityConfig类中添加userDetailsService()方法来向我们的应用程序添加内存用户。以下是该方法的源代码,它将创建一个用户名为user,密码为password的内存用户:

  @Bean
  @Override
  public UserDetailsService userDetailsService() {
      UserDetails user =
           User.withDefaultPasswordEncoder()
              .username("user")
              .password("password")
              .roles("USER")
              .build();

      return new InMemoryUserDetailsManager(user);
  } 

在开发阶段使用内存用户是很好的,但是真正的应用程序应该将用户保存在数据库中。要将用户保存到数据库中,您必须创建一个用户实体类和存储库。密码不应以明文格式保存到数据库中。Spring Security 提供了多种哈希算法,例如 BCrypt,您可以使用它们来哈希密码。以下步骤显示了如何实现这一点:

  1. domain包中创建一个名为User的新类。激活domain包,右键单击鼠标。从菜单中选择 New | Class,并将新类命名为User。之后,您的项目结构应如下截图所示:

  1. 使用@Entity注解对User类进行注释。添加类字段——ID、用户名、密码和角色。最后,添加构造函数、getter 和 setter。我们将所有字段设置为可为空,并使用@Column注解使用户名必须是唯一的。请参阅以下User.java字段和构造函数的源代码:
package com.packt.cardatabase.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false, updatable = false)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String role;

    public User() {
    }

  public User(String username, String password, String role) {
    super();
    this.username = username;
    this.password = password;
    this.role = role;
  }

以下是User.java源代码的其余部分,包括 getter 和 setter:

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getUsername() {
    return username;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public String getPassword() {
    return password;
  }

  public void setPassword(String password) {
    this.password = password;
  }

  public String getRole() {
    return role;
  }

  public void setRole(String role) {
    this.role = role;
  }
}
  1. domain包中创建一个名为UserRepository的新类。激活domain包,右键单击鼠标。从菜单中选择新建|类,并将新类命名为UserRepository

  2. 仓库类的源代码与我们在上一章中所做的类似,但有一个查询方法findByUsername,我们在接下来的步骤中需要。请参阅以下UserRepository源代码:

package com.packt.cardatabase.domain;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends CrudRepository<User, Long> { 
    User findByUsername(String username);
}
  1. 接下来,我们创建一个实现 Spring Security 提供的UserDetailsService接口的类。Spring Security 用于用户身份验证和授权。在根包中创建一个名为service的新包。激活根包,右键单击鼠标。从菜单中选择新建|包,并将新包命名为service

  1. 在我们刚刚创建的service包中创建一个名为UserDetailServiceImpl的新类。现在,您的项目结构应如下所示:

  1. 我们必须将UserRepository类注入到UserDetailServiceImpl类中,因为在 Spring Security 处理身份验证时需要从数据库中获取用户。loadByUsername方法返回所需的UserDetails对象进行身份验证。以下是UserDetailServiceImpl.java的源代码:
package com.packt.cardatabase.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.packt.cardatabase.domain.User;
import com.packt.cardatabase.domain.UserRepository;

@Service
public class UserDetailServiceImpl implements UserDetailsService {
  @Autowired
  private UserRepository repository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    { 
      User currentUser = repository.findByUsername(username);
        UserDetails user = new org.springframework.security.core
            .userdetails.User(username, currentUser.getPassword()
            , true, true, true, true, 
            AuthorityUtils.createAuthorityList(currentUser.getRole()));
        return user;
    }

}
  1. 在我们的安全配置类中,我们必须定义 Spring Security 应该使用数据库中的用户而不是内存中的用户。从SecurityConfig类中删除userDetailsService()方法以禁用内存中的用户。添加一个新的configureGlobal方法以启用来自数据库的用户。我们不应该将密码明文保存到数据库中。因此,我们将在configureGlobal方法中定义密码哈希算法。在本例中,我们使用 BCrypt 算法。这可以很容易地通过 Spring Security 的BCryptPasswordEncoder类实现。以下是SecurityConfig.java的源代码。现在,密码在保存到数据库之前必须使用 BCrypt 进行哈希处理:
package com.packt.cardatabase;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import com.packt.cardatabase.service.UserDetailServiceImpl;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  @Autowired
  private UserDetailServiceImpl userDetailsService; 

  @Autowired
  public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService)
    .passwordEncoder(new BCryptPasswordEncoder());
  }
}
  1. 最后,我们可以在我们的CommandLineRunner中将一对测试用户保存到数据库中。打开CardatabaseApplication.java文件,并在类的开头添加以下代码,将UserRepository注入到主类中:
@Autowired 
private UserRepository urepository;
  1. 使用哈希密码将用户保存到数据库。您可以使用在互联网上找到的任何 BCrypt 计算器:
  @Bean
  CommandLineRunner runner() {
    return args -> {
      Owner owner1 = new Owner("John" , "Johnson");
      Owner owner2 = new Owner("Mary" , "Robinson");
      orepository.save(owner1);
      orepository.save(owner2);

      repository.save(new Car("Ford", "Mustang", "Red", "ADF-1121", 
        2017, 59000, owner1));
      repository.save(new Car("Nissan", "Leaf", "White", "SSJ-3002", 
        2014, 29000, owner2));
      repository.save(new Car("Toyota", "Prius", "Silver", "KKO-0212", 
        2018, 39000, owner2));

 // username: user password: user
 urepository.save(new User("user",
      "$2a$04$1.YhMIgNX/8TkCKGFUONWO1waedKhQ5KrnB30fl0Q01QKqmzLf.Zi",
      "USER"));
 // username: admin password: admin
 urepository.save(new User("admin",
      "$2a$04$KNLUwOWHVQZVpXyMBNc7JOzbLiBjb9Tk9bP7KNcPI12ICuvzXQQKG", 
      "ADMIN"));
 };
  } 

运行应用程序后,您会看到数据库中现在有一个user表,并且保存了两条用户记录:

现在,如果您尝试在没有身份验证的情况下向/api端点发送GET请求,您将收到401 Unauthorized错误。您应该进行身份验证才能发送成功的请求。与前一个示例的不同之处在于,我们使用数据库中的用户进行身份验证。

您可以在以下截图中看到对/api端点的GET请求,使用admin用户:

使用 JWT 保护您的后端

在前一节中,我们介绍了如何在 RESTful Web 服务中使用基本身份验证。当我们要用 React 开发自己的前端时,这是不可用的。我们将在我们的应用程序中使用 JSON Web Tokens(JWT)身份验证。JWT 是在现代 Web 应用程序中实现身份验证的一种紧凑方式。JWT 非常小,因此可以在 URL、POST 参数或标头中发送。它还包含有关用户的所有必需信息。

JSON Web 令牌由三个由点分隔的不同部分组成。第一部分是标头,定义了令牌的类型和哈希算法。第二部分是有效载荷,通常在认证的情况下包含有关用户的信息。第三部分是签名,用于验证令牌在传输过程中未被更改。您可以看到以下 JWT 令牌的示例:

eyJhbGciOiJIUzI1NiJ9.
eyJzdWIiOiJKb2UifD.
ipevRNuRP6HflG8cFKnmUPtypruRC4fc1DWtoLL62SY

以下图表显示了 JWT 身份验证过程的主要思想:

成功认证后,用户发送的请求应始终包含在认证中收到的 JWT 令牌。

我们将使用 Java JWT 库(github.com/jwtk/jjwt),这是 Java 和 Android 的 JSON Web 令牌库;因此,我们必须将以下依赖项添加到pom.xml文件中。JWT 库用于创建和解析 JWT 令牌:

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.0</version>
</dependency>

以下步骤显示了如何在我们的后端启用 JWT 身份验证:

  1. service包中创建一个名为AuthenticationService的新类。在类的开头,我们将定义一些常量;EXPIRATIONTIME定义了令牌的过期时间(以毫秒为单位)。SIGNINGKEY是用于数字签名 JWT 的特定于算法的签名密钥。您应该使用 Base64 编码的字符串。PREFIX 定义了令牌的前缀,通常使用 Bearer 模式。addToken方法创建令牌并将其添加到请求的Authorization标头中。签名密钥使用 SHA-512 算法进行编码。该方法还使用Authorization值向标头添加Access-Control-Expose-Headers。这是因为我们默认情况下无法通过 JavaScript 前端访问Authorization标头。getAuthentication方法使用jjwt库提供的parser()方法从响应的Authorization标头中获取令牌。整个AuthenticationService源代码可以在此处看到:
package com.packt.cardatabase.service;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;

import static java.util.Collections.emptyList;

public class AuthenticationService {
  static final long EXPIRATIONTIME = 864_000_00; // 1 day in milliseconds
  static final String SIGNINGKEY = "SecretKey";
  static final String PREFIX = "Bearer";

  // Add token to Authorization header
  static public void addToken(HttpServletResponse res, String username) {
    String JwtToken = Jwts.builder().setSubject(username)
        .setExpiration(new Date(System.currentTimeMillis() 
            + EXPIRATIONTIME))
        .signWith(SignatureAlgorithm.HS512, SIGNINGKEY)
        .compact();
    res.addHeader("Authorization", PREFIX + " " + JwtToken);
  res.addHeader("Access-Control-Expose-Headers", "Authorization");
  }

  // Get token from Authorization header
  static public Authentication getAuthentication(HttpServletRequest request) {
    String token = request.getHeader("Authorization");
    if (token != null) {
      String user = Jwts.parser()
          .setSigningKey(SIGNINGKEY)
          .parseClaimsJws(token.replace(PREFIX, ""))
          .getBody()
          .getSubject();

      if (user != null) 
        return new UsernamePasswordAuthenticationToken(user, null,
            emptyList());
    }
    return null;
  }
}
  1. 接下来,我们将添加一个新的简单 POJO 类来保存认证凭据。在domain包中创建一个名为AccountCredentials的新类。该类有两个字段——usernamepassword。以下是该类的源代码。该类没有@Entity注释,因为我们不必将凭据保存到数据库中:
package com.packt.cardatabase.domain;

public class AccountCredentials {
  private String username;
  private String password;

  public String getUsername() {
    return username;
  }
  public void setUsername(String username) {
    this.username = username;
  }
  public String getPassword() {
    return password;
  }
  public void setPassword(String password) {
    this.password = password;
  } 
}
  1. 我们将使用过滤器类进行登录和身份验证。在根包中创建一个名为LoginFilter的新类,处理对/login端点的POST请求。LoginFilter类扩展了 Spring Security 的AbstractAuthenticationProcessingFilter,需要设置authenticationManager属性。认证由attemptAuthentication方法执行。如果认证成功,则执行succesfulAuthentication方法。然后,此方法将调用我们的服务类中的addToken方法,并将令牌添加到Authorization标头中:
package com.packt.cardatabase;

import java.io.IOException;
import java.util.Collections;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.packt.cardatabase.domain.AccountCredentials;
import com.packt.cardatabase.service.AuthenticationService;

public class LoginFilter extends AbstractAuthenticationProcessingFilter {

  public LoginFilter(String url, AuthenticationManager authManager) {
    super(new AntPathRequestMatcher(url));
    setAuthenticationManager(authManager);
  }

  @Override
  public Authentication attemptAuthentication(
  HttpServletRequest req, HttpServletResponse res)
      throws AuthenticationException, IOException, ServletException {
  AccountCredentials creds = new ObjectMapper()
        .readValue(req.getInputStream(), AccountCredentials.class);
  return getAuthenticationManager().authenticate(
        new UsernamePasswordAuthenticationToken(
            creds.getUsername(),
            creds.getPassword(),
            Collections.emptyList()
        )
    );
  }

  @Override
  protected void successfulAuthentication(
      HttpServletRequest req,
      HttpServletResponse res, FilterChain chain,
      Authentication auth) throws IOException, ServletException {
    AuthenticationService.addToken(res, auth.getName());
  }
}
  1. 在根包中创建一个名为AuthenticationFilter的新类。该类扩展了GenericFilterBean,这是任何类型过滤器的通用超类。此类将处理除/login之外的所有其他端点的身份验证。AuthenticationFilter使用我们的服务类中的addAuthentication方法从请求的Authorization标头中获取令牌:
package com.packt.cardatabase;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import com.packt.cardatabase.service.AuthenticationService;

public class AuthenticationFilter extends GenericFilterBean {
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
    Authentication authentication = AuthenticationService.getAuthentication((HttpServletRequest)request);

    SecurityContextHolder.getContext().
        setAuthentication(authentication);
    filterChain.doFilter(request, response);
  }
}
  1. 最后,我们必须对我们的SecurityConfig类的configure方法进行更改。在那里,我们定义了对/login端点的POST方法请求允许无身份验证,并且对所有其他端点的请求需要身份验证。我们还通过使用addFilterBefore方法定义了要在/login和其他端点中使用的过滤器:
  //SecurityConfig.java  
  @Override
    protected void configure(HttpSecurity http) throws Exception {
     http.cors().and().authorizeRequests()
      .antMatchers(HttpMethod.POST, "/login").permitAll()
          .anyRequest().authenticated()
          .and()
          // Filter for the api/login requests
          .addFilterBefore(new LoginFilter("/login",
           authenticationManager()),
                  UsernamePasswordAuthenticationFilter.class)
          // Filter for other requests to check JWT in header
          .addFilterBefore(new AuthenticationFilter(),
                  UsernamePasswordAuthenticationFilter.class);
    }
  1. 我们还将在安全配置类中添加CORS(跨源资源共享)过滤器。这对于前端是必需的,因为它会从其他来源发送请求。CORS 过滤器拦截请求,如果识别为跨源,它会向请求添加适当的标头。为此,我们将使用 Spring Security 的CorsConfigurationSource接口。在此示例中,我们将允许所有 HTTP 方法和标头。如果需要更精细的定义,您可以在此处定义允许的来源、方法和标头列表。将以下源代码添加到您的SecurityConfig类中以启用 CORS 过滤器:
  // SecurityConfig.java  
  @Bean
    CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = 
            new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(Arrays.asList("*"));
        config.setAllowedMethods(Arrays.asList("*"));
        config.setAllowedHeaders(Arrays.asList("*"));
        config.setAllowCredentials(true);
        config.applyPermitDefaultValues();

        source.registerCorsConfiguration("/**", config);
        return source;
  } 

现在,在运行应用程序之后,我们可以使用POST方法调用/login端点,在成功登录的情况下,我们将在Authorization标头中收到 JWT 令牌:

成功登录后,我们可以通过在Authorization标头中发送从登录接收到的 JWT 令牌来调用其他 RESTful 服务端点。请参见以下屏幕截图中的示例:

现在,我们已经实现了后端所需的所有功能。接下来,我们将继续进行后端单元测试。

Spring Boot 中的测试

当我们创建项目时,Spring Initializr 会自动将 Spring Boot 测试启动器包添加到pom.xml中。这是在 Spring Initializr 页面中没有任何选择的情况下自动添加的:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

Spring Boot 测试启动器为测试提供了许多方便的库,如 JUnit、Mockito、AssertJ 等。如果您查看,您的项目结构已经为测试类创建了自己的包:

默认情况下,Spring Boot 在测试中使用内存数据库。我们现在使用 MariaDB,但也可以通过将以下依赖项添加到pom.xml文件中来使用 H2 进行测试。范围定义了 H2 数据库仅用于运行测试;否则,应用程序将使用 MariaDB 数据库:

    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>test</scope>
    </dependency> 

如果您还想在测试中使用默认数据库,可以使用@AutoConfigureTestDatabase注解。

创建单元测试

对于单元测试,我们使用的是 JUnit,这是一个流行的基于 Java 的单元测试库。以下源代码显示了 Spring Boot 测试类的示例框架。@SpringBootTest注解指定该类是一个常规测试类,运行基于 Spring Boot 的测试。方法前的@Test注解定义了该方法可以作为测试用例运行。@RunWith(SpringRunner.class)注解提供了 Spring ApplicationContext并将 bean 注入到测试实例中:

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyTestsClass {

  @Test
  public void testMethod() {
    ...
  }

}

首先,我们将创建我们的第一个测试用例,该测试用例将在创建任何正式测试用例之前测试应用程序的主要功能。打开已为您的应用程序创建的CardatabaseApplicationTest测试类。有一个名为contextLoads的测试方法,我们将在其中添加测试。以下测试检查控制器的实例是否已成功创建和注入:

package com.packt.cardatabase;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import com.packt.cardatabase.web.CarController;

@RunWith(SpringRunner.class)
@SpringBootTest
public class CardatabaseApplicationTests {
  @Autowired
  private CarController controller;

  @Test
  public void contextLoads() {
    assertThat(controller).isNotNull();
  }

}

要在 Eclipse 中运行测试,请在项目资源管理器中激活测试类,然后右键单击鼠标。从菜单中选择 Run As | JUnit test。现在应该在 Eclipse 工作台的下部看到 JUnit 选项卡。测试结果显示在此选项卡中,测试用例已通过:

接下来,我们将为我们的汽车存储库创建单元测试,以测试 CRUD 操作。在根测试包中创建一个名为CarRepositoryTest的新类。如果测试仅关注 JPA 组件,则可以使用@DataJpaTest注解,而不是@SpringBootTest注解。使用此注解时,H2 数据库、Hibernate 和 Spring Data 会自动配置进行测试。SQL 日志记录也将被打开。测试默认是事务性的,并在测试用例结束时回滚。TestEntityManager用于处理持久化实体,并设计用于测试。您可以在以下看到 JPA 测试类骨架的源代码:

package com.packt.cardatabase;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.context.junit4.SpringRunner;

import com.packt.cardatabase.domain.Car;
import com.packt.cardatabase.domain.CarRepository;

@RunWith(SpringRunner.class)
@DataJpaTest
public class CarRepositoryTest {
  @Autowired
  private TestEntityManager entityManager;

  @Autowired
  private CarRepository repository;

   // Test cases..
}

我们将添加第一个测试用例来测试向数据库添加新汽车。使用TestEntityManager提供的persistAndFlush方法创建一个新的car对象并保存到数据库中。然后,我们检查如果成功保存,汽车 ID 不能为空。以下源代码显示了测试用例方法。将以下方法代码添加到您的CarRepositoryTest类中:

  @Test
  public void saveCar() {
    Car car = new Car("Tesla", "Model X", "White", "ABC-1234",
        2017, 86000);
    entityManager.persistAndFlush(car);

    assertThat(car.getId()).isNotNull();
  }

第二个测试用例将测试从数据库中删除汽车。创建一个新的car对象并保存到数据库中。然后,从数据库中删除所有汽车,最后,findAll()查询方法应返回一个空列表。以下源代码显示了测试用例方法。将以下方法代码添加到您的CarRepositoryTest类中:

  @Test
  public void deleteCars() {
    entityManager.persistAndFlush(new Car("Tesla", "Model X", "White",
        "ABC-1234", 2017, 86000));
    entityManager.persistAndFlush(new Car("Mini", "Cooper", "Yellow",
        "BWS-3007", 2015, 24500));

    repository.deleteAll();
    assertThat(repository.findAll()).isEmpty();
  } 

运行测试用例并在 Eclipse JUnit 选项卡上检查测试是否通过:

接下来,我们将展示如何测试 RESTful Web 服务 JWT 身份验证功能。对于测试控制器或任何公开的端点,我们可以使用MockMvc。通过使用MockMvc,服务器不会启动,但测试是在 Spring 处理 HTTP 请求的层中执行的,因此它模拟了真实情况。MockMvc提供了perform方法来发送请求。要测试身份验证,我们必须向请求体添加凭据。我们执行两个请求;第一个具有正确的凭据,我们检查状态是否正常。第二个请求包含不正确的凭据,我们检查是否收到 4XX 的 HTTP 错误:

package com.packt.cardatabase;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class CarRestTest {
  @Autowired
    private MockMvc mockMvc;

  @Test
  public void testAuthentication() throws Exception {
    // Testing authentication with correct credentials
        this.mockMvc.perform(post("/login")
          .content("{\"username\":\"admin\", \"password\":\"admin\"}")).
          andDo(print()).andExpect(status().isOk());

    // Testing authentication with wrong credentials
        this.mockMvc.perform(post("/login")
          .content("{\"username\":\"admin\", \"password\":\"wrongpwd\"}")).
          andDo(print()).andExpect(status().is4xxClientError());

  }

}

现在,当我们运行身份验证测试时,我们可以看到测试通过了:

现在,我们已经介绍了 Spring Boot 应用程序中测试的基础知识,您应该具备实现更多测试用例的所需知识。

摘要

在本章中,我们专注于保护和测试 Spring Boot 后端。首先使用 Spring Security 进行保护。前端将在接下来的章节中使用 React 进行开发;因此,我们实现了 JWT 身份验证,这是一种适合我们需求的轻量级身份验证方法。我们还介绍了测试 Spring Boot 应用程序的基础知识。我们使用 JUnit 进行单元测试,并为 JPA 和 RESTful Web 服务身份验证实现了测试用例。在下一章中,我们将为前端开发设置环境和工具。

问题

  1. 什么是 Spring Security?

  2. 如何使用 Spring Boot 保护后端?

  3. 什么是 JWT?

  4. 如何使用 JWT 保护后端?

  5. 如何使用 Spring Boot 创建单元测试?

  6. 如何运行和检查单元测试的结果?

进一步阅读

Packt 还有其他很好的资源,可以了解 Spring Security 和测试的知识:

第五章:设置环境和工具 - 前端

本章描述了使用 React 所需的开发环境和工具。这一章是为了能够开始前端开发。我们将使用 Facebook 制作的 Create React App 入门套件创建一个简单的 React 应用。

在本章中,我们将研究以下内容:

  • 安装 Node.js 和 VS Code

  • 使用create-react-app创建一个 React.js 应用

  • 运行 React.js 应用

  • 安装 React 开发者工具

技术要求

在本书中,我们使用 Windows 操作系统,但所有工具也适用于 Linux 和 macOS。

安装 Node.js

Node.js 是一个基于 JavaScript 的开源服务器端环境。Node.js 适用于多个操作系统,如 Windows,macOS 和 Linux。Node.js 是开发 React 应用所需的。

Node.js 安装包可以在nodejs.org/en/download/找到。为您的操作系统下载最新的长期支持LTS)版本。在本书中,我们使用 Windows 10 操作系统,您可以为其获取 Node.js MSI 安装程序,这样安装就非常简单。当您执行安装程序时,您将通过安装向导,并且可以使用默认设置进行操作:

安装完成后,我们可以检查一切是否正确。打开 PowerShell,或者您正在使用的终端,然后输入以下命令:

node -v
npm -v

这些命令应该显示已安装的版本,Node.js 和 npm:

npm 随 Node.js 安装而来,是 JavaScript 的包管理器。在接下来的章节中,当我们安装不同的节点模块到我们的 React 应用时,我们会经常使用它。还有另一个称为 Yarn 的包管理器,您也可以使用。

安装 VS Code

Visual Studio CodeVS Code)是一个用于多种编程语言的开源代码编辑器。VS Code 由 Microsoft 开发。还有许多不同的代码编辑器可用,如 Atom,Brackets 等,如果您熟悉其他编辑器,也可以使用其他编辑器。VS Code 适用于 Windows,macOS 和 Linux,您可以从code.visualstudio.com/下载它。

Windows 的安装是通过 MSI 安装程序完成的,您可以使用默认设置进行安装。以下截图显示了 VS Code 的工作台。左侧是活动栏,您可以使用它在不同视图之间导航。活动栏旁边是侧边栏,其中包含不同的视图,如项目文件资源管理器。

编辑器占据了工作台的其余部分:

VS Code 还有一个集成终端,您可以使用它来创建和运行 React 应用。终端可以在 View | Integrated Terminal 菜单中找到。在后续章节中,当我们创建更多的 React 应用时,您也可以使用它。

有很多可用于不同语言和框架的扩展。如果您从活动栏打开扩展管理器,可以搜索不同的扩展。一个真正方便的 React 开发扩展是 Reactjs Code Snippets,我们建议安装。它有多个可用于 React.js 应用的代码片段,可以加快开发过程。我们稍后会向您展示如何使用该扩展。这只是许多有用的扩展之一,您应该探索更多可能使您的生活更轻松的扩展。例如,ESLint 扩展可以帮助您快速找到拼写错误和语法错误,并使源代码的格式化更容易:

创建和运行一个 React 应用

当我们安装了 Node.js 和代码编辑器后,我们就可以创建我们的第一个 React.js 应用程序了。我们使用 Facebook 的create-react-app (github.com/facebook/create-react-app)。以下是制作第一个应用程序的步骤:

  1. 打开 PowerShell 或命令行工具,然后输入以下命令。该命令安装了create-react-app starter,我们将用它来开发 React 应用程序。命令中的参数-g表示全局安装。

如果您使用的是 npm 版本 5.2 或更高版本,您也可以使用npx代替npm

npm install -g create-react-app
  1. 安装完成后,我们通过输入以下命令来创建我们的第一个应用程序:
create-react-app myapp

  1. 应用程序创建后,将其移动到您的app文件夹中:
cd myapp
  1. 然后,您可以使用以下命令运行应用程序。该命令在端口3000中运行应用程序,并在浏览器中打开应用程序:
npm start
  1. 现在您的应用程序正在运行,您应该在浏览器中看到以下页面。npm start命令以开发模式启动应用程序:

您可以通过在 PowerShell 中按Ctrl + C来停止开发服务器。

要为生产构建应用程序的缩小版本,您可以使用npm run build命令,该命令将在build文件夹中构建您的应用程序。

修改 React 应用程序

通过选择文件 | 打开文件夹在 VS Code 中打开您的 React 应用程序文件夹。您应该在文件资源管理器中看到应用程序结构。在这个阶段中最重要的文件夹是src文件夹,其中包含 JavaScript 源代码:

在代码编辑器中的src文件夹中打开App.js文件。删除显示图像的行并保存文件。您暂时不需要了解有关此文件的更多信息。我们将在下一章中深入讨论这个主题:

现在,如果您查看浏览器,您应该立即看到图像已从页面中消失:

要调试 React 应用程序,我们还应该安装 React Developer Tools,它们适用于 Chrome 或 Firefox 浏览器。可以从 Chrome Web Store (chrome.google.com/webstore/category/extensions)安装 Chrome 插件,从 Firefox 插件站 (addons.mozilla.org)安装 Firefox 插件。安装了 React Developer Tools 后,当您导航到 React 应用程序时,您应该在浏览器的开发者工具中看到一个新的 React 标签。以下屏幕截图显示了 Chrome 浏览器中的开发者工具:

摘要

在本章中,我们安装了开始使用 React.js 进行前端开发所需的一切。首先,我们安装了 Node.js 和 VS Code 编辑器。然后,我们使用了create-react-app starter 套件来创建我们的第一个 React.js 应用程序。最后,我们运行了应用程序,并演示了如何修改它。这只是应用程序结构和修改的概述,我们将在接下来的章节中继续讨论。

问题

  1. 什么是 Node.js 和 npm?

  2. 如何安装 Node.js?

  3. 什么是 VS Code?

  4. 如何安装 VS Code?

  5. 如何使用create-react-app创建 React.js 应用程序?

  6. 如何运行 React.js 应用程序?

  7. 如何对应用程序进行基本修改?

进一步阅读

Packt 还有其他很好的资源可以学习 React:

第六章:开始使用 React

本章描述了 React 编程的基础知识。我们将介绍创建 React 前端基本功能所需的技能。在 JavaScript 中,我们使用 ES6 语法,因为它提供了许多使编码更清晰的功能。

在本章中,我们将看到以下内容:

  • 如何创建 React 组件

  • 如何在组件中使用状态和属性

  • 有用的 ES6 功能

  • JSX 是什么

  • 如何在 React 中处理事件和表单

技术要求

在本书中,我们使用的是 Windows 操作系统,但所有工具也适用于 Linux 和 macOS。

基本的 React 组件

根据 Facebook 的说法,React 是一个用于用户界面的 JavaScript 库。自版本 15 以来,React 已经在 MIT 许可证下开发。React 是基于组件的,组件是独立和可重用的。组件是 React 的基本构建块。当您开始使用 React 开发用户界面时,最好从创建模拟界面开始。这样,可以轻松地确定需要创建哪种组件以及它们如何交互。

从模拟的下图中,我们可以看到用户界面如何分割成组件。在这种情况下,将有一个应用根组件,一个搜索栏组件,一个表格组件和一个表格行组件:

然后,这些组件可以按以下树形层次结构排列。理解 React 的重要一点是,数据流是从父组件到子组件的:

React 使用虚拟 DOM 来选择性地重新渲染用户界面,这使得它更具成本效益。虚拟 DOM 是 DOM 的轻量级副本,对虚拟 DOM 的操作比真实 DOM 快得多。虚拟 DOM 更新后,React 将其与在更新运行之前从虚拟 DOM 中获取的快照进行比较。比较后,React 知道哪些部分已更改,只有这些部分才会更新到真实 DOM 中。

React 组件可以通过使用 JavaScript 函数或 ES6 JavaScript 类来定义。我们将在下一节更深入地了解 ES6。以下是一个简单的组件源代码,用于呈现Hello World文本。第一个代码块使用了 JavaScript 函数:

// Using JavaScript function
function Hello() {
  return <h1>Hello World</h1>;
}

这个例子使用类来创建一个组件:

// Using ES6 class
class Hello extends React.Component {
  render() {
    return <h1>Hello World</h1>;
  }
}

使用类实现的组件包含所需的render()方法。这个方法显示和更新组件的呈现输出。用户定义的组件名称应以大写字母开头。

让我们对组件的render方法进行更改,并添加一个新的标题元素进去:

class App extends Component {
  render() {
    return (
      <h1>Hello World!</h1>
      <h2>From my first React app</h2>
    );
  }
}

当您运行应用程序时,会出现“相邻的 JSX 元素必须包装在一个封闭标记中”的错误。要解决这个错误,我们必须将标题包装在一个元素中,比如div;自 React 版本 16.2 以来,我们还可以使用Fragments,它看起来像空的 JSX 标签:

// Wrap headers in div
class App extends Component {
  render() {
    return (
      <div>
        <h1>Hello World!</h1>
        <h2>From my first React app</h2>
      </div>
    );
  }
}

// Or using fragments
class App extends Component {
  render() {
    return (
      <>
        <h1>Hello World!</h1>
        <h2>From my first React app</h2>
      </>
    );
  }
}

让我们更仔细地看一下我们在上一章中使用create-react-app创建的第一个 React 应用程序。根文件夹中Index.js文件的源代码如下:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

在文件的开头,有一些import语句,用于加载组件或资源到我们的文件中。例如,第二行从node_modules文件夹中导入了react-dom包,第四行导入了App(根文件夹中的App.js文件)组件。react-dom包为我们提供了特定于 DOM 的方法。要将 React 组件呈现到 DOM 中,我们可以使用react-dom包中的render方法。第一个参数是将要呈现的组件,第二个参数是组件将要呈现的元素或容器。在这种情况下,root元素是<div id="root"></div>,可以在public文件夹中的index.html文件中找到。请参阅以下index.html文件:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1,
     shrink-to-fit=no">
    <meta name="theme-color" content="#000000">

    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">

    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

以下源代码显示了我们第一个 React 应用程序的App.js组件。您可以看到import也适用于图像和样式表等资产。在源代码的末尾,有一个export语句,导出组件,并且可以通过导入在其他组件中使用。每个文件只能有一个默认导出,但可以有多个命名导出:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

以下示例显示了如何导入默认和命名导出:

import React from 'react' // Import default value
import { Component } from 'react' // Import named value

导出如下:

export default React // Default export
export {Component} // Named export

ES6 基础

ES6(ECMAScript 2015)于 2015 年发布,引入了许多新功能。ECMAScript 是一种标准化的脚本语言,而 JavaScript 是其一种实现。在这里,我们将介绍 ES6 中发布的最重要的功能,这些功能将在接下来的部分中使用。

了解常量

常量或不可变变量可以通过使用const关键字来定义。使用const关键字时,变量内容不能被重新分配:

const PI = 3.14159;

const的作用域是块作用域,与let相同。这意味着const变量只能在定义它的块内使用。在实践中,块是花括号{ }之间的区域。以下示例代码显示了作用域的工作原理。第二个console.log语句会报错,因为我们试图在作用域之外使用total变量:

var count = 10;
if(count > 5) {
  const total = count * 2;
  console.log(total); // Prints 20 to console
}
console.log(total); // Error, outside the scope

值得知道的是,如果const是对象或数组,则内容可以更改。以下示例演示了这一点:

const myObj = {foo : 3};
myObj.foo = 5; // This is ok

箭头函数

箭头函数使函数声明更加紧凑。在 JavaScript 中定义函数的传统方式是使用function关键字。以下函数获取一个参数,然后返回参数值:

function hello(greeting) {
    return greeting;
}

通过使用 ES6 箭头函数,函数如下所示:

const hello = greeting => { greeting }

// function call
hello('Hello World'); // returns Hello World

如果有多个参数,必须使用括号将参数括起来,并用逗号分隔参数。以下函数获取两个参数并返回参数的总和。如果函数体是一个表达式,则不需要使用return关键字。该表达式总是从函数中隐式返回的:

const calcSum = (x, y) => { x + y }

// function call
calcSum(2, 3); // returns 5

如果函数没有任何参数,则语法如下:

() => { ... }

模板文字

模板文字可用于连接字符串。连接字符串的传统方式是使用加号运算符:

var person = {firstName: 'John', lastName: 'Johnson'};
var greeting = "Hello " + ${person.firstName} + " " + ${person.lastName};

使用模板文字,语法如下。您必须使用反引号( )而不是单引号或双引号:

var person = {firstName: 'John', lastName: 'Johnson'};
var greeting = `Hello ${person.firstName} ${person.lastName}`;

类和继承

ES6 中的类定义类似于 Java 或 C#等面向对象语言。定义类的关键字是class。类可以有字段、构造函数和类方法。以下示例代码显示了 ES6 类:

class Person {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }  
}

继承是使用extends关键字完成的。以下示例代码显示了一个继承Person类的Employee类。因此,它继承了父类的所有字段,并且可以具有特定于员工的自己的字段。在构造函数中,我们首先使用super关键字调用父类构造函数。这个调用是必需的,如果缺少它,您将会收到一个错误:

class Employee extends Person {
    constructor(firstName, lastName, title, salary) {
        super(firstName, lastName);
        this.title= title;
        this.salary = salary;
    }  
}

尽管 ES6 已经相当古老,但现代 Web 浏览器仍然只支持部分。Babel 是一个 JavaScript 编译器,用于将 ES6 编译为与所有浏览器兼容的旧版本。您可以在 Babel 网站上测试编译器(babeljs.io)。以下屏幕截图显示了箭头函数编译回旧的 JavaScript 语法:

JSX 和样式

JSX 是 JavaScript 的语法扩展。在 React 中使用 JSX 不是强制的,但有一些好处可以使开发更容易。例如,JSX 可以防止注入攻击,因为在渲染之前 JSX 中的所有值都会被转义。最有用的功能是可以通过花括号包裹 JavaScript 表达式在 JSX 中嵌入 JavaScript 表达式,这在接下来的章节中会经常使用。在这个例子中,我们可以在使用 JSX 时访问组件的 props。组件的 props 将在下一节中介绍:

class Hello extends React.Component {
  render() {
    return <h1>Hello World {this.props.user}</h1>;
  }
}

你也可以将 JavaScript 表达式作为 props 传递:

<Hello count={2+2} />

JSX 通过 Babel 编译为React.createElement()调用。你可以在 React JSX 元素中使用内部或外部样式。以下是两个内联样式的例子。第一个直接在div元素内定义样式:

<div style={{height: 20, width: 200}}>
  Hello
</div>

第二个例子首先创建样式对象,然后在div元素中使用。对象名称应该使用驼峰命名约定:

const divStyle = {
  color: 'red',
  height: 30
};

const MyComponent = () => (
  <div style={divStyle}>Hello</div>
);

如前一节所示,你可以向 React 组件导入样式表。要引用外部 CSS 文件中的类,应该使用className属性:

import './App.js';

...

<div className="App-header">
  This is my app
</div>

Props 和 state

Props 和 state 是渲染组件的输入数据。props 和 state 都是 JavaScript 对象,当 props 或 state 发生变化时,组件会重新渲染。

props 是不可变的,所以组件不能改变它的 props。props 是从父组件接收的。组件可以通过this.props对象访问 props。例如,看下面的组件:

class Hello extends React.Component {
  render() {
    return <h1>Hello World {this.props.user}</h1>;
  }
}

父组件可以通过以下方式向Hello组件发送 props:

<Hello user="John" />

Hello组件被渲染时,它会显示Hello World John文本。

状态可以在组件内部改变。状态的初始值在组件的构造函数中给出。可以通过this.state对象访问状态。状态的作用域是组件,因此不能在定义它的组件外部使用。如下例所示,props 作为参数传递给构造函数,状态在构造函数中初始化。然后可以使用花括号{this.state.user}在 JSX 中渲染状态的值:

class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.state = {user: 'John'}
  }

  render() {
    return <h1>Hello World {this.state.user}</h1>;
  }
}

状态可以包含不同类型的多个值,因为它是一个 JavaScript 对象,如下例所示:

  constructor(props) {
    super(props);
    this.state = {firstName: 'John', lastName: 'Johnson', age: 30}
  }

使用setState方法改变状态的值:

this.setState({firstName: 'Jim', age: 31});  // Change state value

不应该使用等号操作符来更新状态,因为这样 React 不会重新渲染组件。改变状态的唯一方法是使用setState方法,这会触发重新渲染:

this.state.firstName = 'Jim'; // WRONG

setState方法是异步的,因此你不能确定状态何时会更新。setState方法有一个回调函数,在状态更新后执行。

状态的使用是可选的,它增加了组件的复杂性。只有 props 的组件称为无状态组件。当具有相同输入时,它们总是呈现相同的输出,这意味着它们非常容易测试。同时具有状态和 props 的组件称为有状态组件。以下是一个简单无状态组件的示例,它是使用类定义的。也可以使用函数定义它:

export default class MyTitle extends Component {
  render() {
    return (
     <div>
      <h1>{this.props.text}</h1>
     </div>
    );
 };
};

// The MyTitle component can be then used in other component and text value is passed to props
<MyTitle text="Hello" />
// Or you can use other component's state
<MyTitle text={this.state.username} />

如果要更新依赖当前状态的状态值,应该向setState()方法传递更新函数而不是对象。一个常见的情况是计数器示例:

// This solution might not work correctly
incerementCounter = () => {
 this.setState({count: this.state.count + 1});
}

// The correct way is the following
incrementCounter = () => {
  this.setState((prevState) => {
    return {count: prevState.count + 1}
  });
}

组件生命周期方法

React 组件有许多生命周期方法可以重写。这些方法在组件生命周期的某些阶段执行。生命周期方法的名称是合乎逻辑的,你几乎可以猜到它们何时会被执行。具有前缀的生命周期方法在发生任何事情之前执行,而具有前缀的方法在发生某事之后执行。挂载是组件生命周期的一个阶段,也是组件创建并插入 DOM 的时刻。我们已经介绍的两个生命周期方法在组件挂载时执行:constructor()render()

在挂载阶段中一个有用的方法是componentDidMount(),它在组件挂载后调用。这个方法适合调用一些 REST API 来获取数据,例如。以下示例代码演示了如何使用componentDidMount()方法。

在下面的示例代码中,我们首先将this.state.user的初始值设置为John。然后,当组件挂载时,我们将值更改为Jim

class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.state = {user: 'John'}
  }

  componentDidMount() {
    this.setState({user: 'Jim'});
  }

  render() {
    return <h1>Hello World {this.state.user}</h1>;
  }
}

还有一个componentWillMount()生命周期方法,在组件挂载之前调用,但 Facebook 建议不要使用它,因为它可能用于内部开发目的。

当状态或属性已更新并且组件将被渲染之前,会调用shouldComponentUpdate()方法。该方法将新属性作为第一个参数,新状态作为第二个参数,并返回布尔值。如果返回的值为true,则组件将重新渲染;否则,它将不会重新渲染。这个方法允许你避免无用的渲染并提高性能:

shouldComponentUpdate(nextProps, nextState) {
  // This function should return a boolean, whether the component should re-render.
  return true; 
}

在组件从 DOM 中移除之前,会调用componentWillUnmount()生命周期方法。这是一个清理资源、清除定时器或取消请求的好时机。

错误边界是捕获其子组件树中 JavaScript 错误的组件。它们还应记录这些错误并在用户界面中显示备用内容。为此,有一个名为componentDidCatch()的生命周期方法。它与 React 组件一起工作,就像标准 JavaScriptcatch块一样。

使用 React 处理列表

对于列表处理,我们引入了一个新的 JavaScript 方法map(),当你需要操作列表时很方便。map()方法创建一个新数组,其中包含调用原始数组中每个元素的函数的结果。在下面的示例中,每个数组元素都乘以 2:

const arr = [1, 2, 3, 4];

const resArr = arr.map(x => x * 2); // resArr = [2, 4, 6, 8]

map()方法还有第二个参数index,在处理 React 中的列表时非常有用。React 中的列表项需要一个唯一的键,用于检测已更改、添加或删除的行。

以下示例显示了将整数数组转换为列表项数组并在ul元素中呈现这些列表项的组件:

class App extends React.Component {
  render() { 
    const data = [1, 2, 3, 4, 5];
    const rows = data.map((number, index) =>
     <li key={index}>Listitem {number}</li>
    );

    return (
     <div>
      <ul>{rows}</ul>
     </div>
    );
  }
}

以下屏幕截图显示了组件在呈现时的外观:

如果数据是对象数组,最好以表格格式呈现数据。思路与列表相同,但现在我们只需将数组映射到表格行并在表格元素中呈现这些行,如下面的代码所示:

class App extends Component {
  render() { 
    const data = [{brand: 'Ford', model: 'Mustang'}, 
    {brand:'VW', model: 'Beetle'}, {brand: 'Tesla', model: 'Model S'}];
    const tableRows = data.map((item, index) =>
     <tr key={index}><td>{item.brand}</td><td>{item.model}</td></tr>
    );

    return (
     <div>
      <table><tbody>{tableRows}</tbody></table>
     </div>
    );
  }
}

以下屏幕截图显示了组件在呈现时的外观:

使用 React 处理事件

React 中的事件处理与处理 DOM 元素事件类似。与 HTML 事件处理相比,不同之处在于 React 中事件命名使用驼峰命名法。以下示例代码向按钮添加了一个事件监听器,并在按下按钮时显示警报消息:

class App extends React.Component {
  // This is called when the button is pressed
  buttonPressed = () => {
    alert('Button pressed');
  }

  render() { 
    return (
     <div>
      <button onClick={this.buttonPressed}>Press Me</button>
     </div>
    );
  }
}

在 React 中,你不能从事件处理程序中返回false来阻止默认行为。相反,你应该调用preventDefault()方法。在下面的示例中,我们使用一个表单,并希望阻止表单提交:

class MyForm extends React.Component {
  // This is called when the form is submitted
  handleSubmit(event) {
    alert('Form submit');
    event.preventDefault();  // Prevents default behavior
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

使用 React 处理表单

使用 React 处理表单有点不同。当提交 HTML 表单时,它将导航到下一个页面。一个常见情况是,我们希望在提交后调用一个 JavaScript 函数,该函数可以访问表单数据并避免导航到下一个页面。我们已经在前一节中介绍了如何使用preventDefault()来避免提交。

让我们首先创建一个最简单的表单,其中包含一个输入字段和提交按钮。为了能够获取输入字段的值,我们使用onChange事件处理程序。当输入字段的值更改时,新值将保存到状态中。this.setState({text: event.target.value});语句从输入字段获取值并将其保存到名为text的状态中。最后,当用户按下提交按钮时,我们将显示输入的值。以下是我们第一个表单的源代码:

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {text: ''};
  }

  // Save input box value to state when it has been changed
  inputChanged = (event) => {
    this.setState({text: event.target.value});
  }

  handleSubmit = (event) => {
    alert(`You typed: ${this.state.text}`);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input type="text" onChange={this.inputChanged} 
            value={this.state.text}/>
        <input type="submit" value="Press me"/>
      </form>
    );
  } 
}

以下是我们的表单组件在按下提交按钮后的屏幕截图:

现在是时候看一下 React Developer Tools 了,这是用于调试 React 应用程序的方便工具。如果我们打开 React Developer Tools 并在 React 表单应用程序中输入内容,我们可以看到状态值的变化。我们可以检查当前的 props 和 state 值。以下屏幕截图显示了当我们在输入字段中输入内容时状态的变化:

通常,表单中会有多个输入字段。处理多个输入字段的一种方法是添加与输入字段数量相同的更改处理程序。但这会创建大量样板代码,我们要避免这种情况。因此,我们向输入字段添加名称属性,并且可以在更改处理程序中利用它来识别触发更改处理程序的输入字段。输入字段的名称属性值必须与我们想要保存值的状态的名称相同。

现在处理程序看起来像下面这样。如果触发处理程序的输入字段是名字字段,则event.target.namefirstName,并且输入的值将保存到名为firstName的状态中。通过这种方式,我们可以使用一个更改处理程序处理所有输入字段:

 inputChanged = (event) => {
    this.setState({[event.target.name]: event.target.value});
  }

以下是组件的完整源代码:

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {firstName: '', lastName: '', email: ''};
  }

  inputChanged = (event) => {
    this.setState({[event.target.name]: event.target.value});
  }

  handleSubmit = (event) => {
    alert(`Hello ${this.state.firstName} ${this.state.lastName}`);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>First name </label>
        <input type="text" name="firstName" onChange={this.inputChanged} 
            value={this.state.firstName}/><br/>
        <label>Last name </label>
        <input type="text" name="lastName" onChange={this.inputChanged} 
            value={this.state.lastName}/><br/>
        <label>Email </label>
        <input type="email" name="email" onChange={this.inputChanged} 
            value={this.state.email}/><br/>
        <input type="submit" value="Press me"/>
      </form>
    );
  } 
}

以下是我们的表单组件在按下提交按钮后的屏幕截图:

总结

在本章中,我们开始了解 React,我们将使用它来构建我们的前端。在开始使用 React 进行开发之前,我们涵盖了 React 组件、JSX、props 和 state 等基础知识。在我们的前端开发中,我们使用 ES6,这使我们的代码更清晰。我们了解了我们需要进一步开发的功能。我们还学会了如何处理 React 中的表单和事件。

问题

  1. 什么是 React 组件?

  2. 状态和 props 是什么?

  3. 数据在 React 应用程序中如何流动?

  4. 无状态组件和有状态组件有什么区别?

  5. JSX 是什么?

  6. 组件生命周期方法是什么?

  7. 我们应该如何处理 React 中的事件?

  8. 我们应该如何处理 React 中的表单?

进一步阅读

Packt 还有其他很好的资源可供学习 React:

第七章:使用 React 消费 REST API

本章解释了 React 的网络。我们将学习承诺,使异步代码更清晰和可读。对于网络,我们将使用fetch库。例如,我们使用 GitHub REST API 来演示如何在 React 中消费 RESTful Web 服务。

在本章中,我们将看看以下内容:

  • 使用承诺

  • 如何使用 Fetch

  • 如何向 REST API 发出请求

  • 如何处理来自 REST API 的响应

  • 如何创建一个消费 REST API 的 React 应用程序

技术要求

在本书中,我们使用的是 Windows 操作系统,但所有工具都适用于 Linux 和 macOS,因为 Node.js 和create-react-app必须安装。

使用承诺

处理异步操作的传统方法是使用回调函数来处理操作的成功或失败。根据调用的结果,将调用其中一个回调函数。以下示例展示了使用回调函数的思想:

function doAsyncCall(success, failure) {
    // Do some api call
    if (SUCCEED)
        success(resp);
    else
        failure(err);
}

success(response) {
    // Do something with response
}

failure(error) {
    // Handle error
}

doAsyncCall(success, failure);

承诺是表示异步操作结果的对象。使用承诺在进行异步调用时简化了代码。承诺是非阻塞的。

承诺可以处于三种状态之一:

  • 待定:初始状态

  • 完成:操作成功

  • 拒绝:操作失败

使用承诺,我们可以进行异步调用,如果我们使用的 API 支持承诺。在下一个示例中,异步调用完成后,当响应返回时,then中的函数将被执行,并将响应作为参数传递:

doAsyncCall()
.then(response => // Do something with the response);

您可以将then链接在一起,这意味着您可以依次运行多个异步操作:

doAsyncCall()
.then(response => // Get some result from the response)
.then(result => // Do something with the result);

您还可以通过使用catch()向承诺添加错误处理:

doAsyncCall()
.then(response => // Get some result from the response)
.then(result => // Do something with result);
.catch(error => console.error(error))

有一种更现代的处理异步调用的方法,使用了 ECMAScript 2017 引入的async/await,它还没有像承诺那样得到浏览器的广泛支持。async/await实际上是基于承诺的。要使用async/await,您必须定义一个可以包含等待表达式的async函数。以下是使用async/await进行异步调用的示例。正如您所看到的,您可以以类似于同步代码的方式编写代码:

doAsyncCall = async () => {
    const response = await fetch('http://someapi.com');
    const result = await response.json();
    // Do something with the result
}

对于错误处理,您可以使用async/awaittry…catch,如下例所示:

doAsyncCall = async () => {
  try {
    const response = await fetch('http://someapi.com');
    const result = await response.json();
    // Do something with the result
  }
  catch(err) {
    console.error(err);
  } 
}

使用 Fetch API

使用 Fetch API,您可以进行 Web 请求。Fetch API 的思想类似于传统的XMLHttpRequest,但 Fetch API 也支持承诺,使其更易于使用。

Fetch API 提供了一个fetch()方法,它有一个必需的参数,即您正在调用的资源的路径。对于 Web 请求,它将是服务的 URL。对于简单的GET方法调用,返回 JSON 响应,语法如下。fetch()方法返回一个包含响应的承诺。您可以使用json()方法从响应中解析 JSON 主体:

fetch('http://someapi.com')
.then(response => response.json())
.then(result => console.log(result));
.catch(error => console.error(error))

使用另一种 HTTP 方法,比如POST,你可以在fetch方法的第二个参数中定义它。第二个参数是一个对象,你可以在其中定义多个请求设置。以下源代码使用POST方法发出请求:

fetch('http://someapi.com', {method: 'POST'})
.then(response => response.json())
.then(result => console.log(result))
.catch(error => console.error(error));

您还可以在第二个参数中添加标头。以下fetch调用包含'Content-Type' : 'application/json'标头:

fetch('http://someapi.com', 
 {
  method: 'POST', 
  headers:{'Content-Type': 'application/json'}
 }
.then(response => response.json())
.then(result => console.log(result))
.catch(error => console.error(error));

如果您必须在请求体中发送 JSON 编码的数据,语法如下:

fetch('http://someapi.com', 
 {
  method: 'POST', 
  headers:{'Content-Type': 'application/json'},
  body: JSON.stringify(data)
 }
.then(response => response.json())
.then(result => console.log(result))
.catch(error => console.error(error));

您还可以使用其他库进行网络调用。一个非常流行的库是axiosgithub.com/axios/axios),你可以使用 npm 将其安装到你的 React 应用程序中。axios 有一些好处,比如自动转换 JSON 数据。以下代码显示了使用axios进行示例调用:

axios.get('http://someapi.com')
.then(response => console.log(response))
.catch(error => console.log(error));

axios有自己的调用方法,用于不同的 HTTP 方法。例如,如果您想发出DELETE请求,axios提供了axios.delete方法。

实际示例

我们将介绍使用一些开放的 REST API 的两个示例。首先,我们将制作一个 React 应用程序,显示伦敦的当前天气。天气是从OpenWeatherMap(openweathermap.org/)获取的。你需要注册到 OpenWeatherMap 获取 API 密钥。我们将使用免费账户,因为这对我们的需求已经足够。注册后,转到你的账户信息找到 API 密钥选项卡。在那里你会看到你需要用于 React 天气应用程序的 API 密钥:

让我们用create-react-app创建一个新的 React 应用程序。打开你正在使用的 PowerShell 或其他终端,并输入以下命令:

create-react-app weatherapp

移动到weatherApp文件夹:

cd weatherapp

用以下命令启动你的应用程序:

npm start

用 VS Code 打开你的项目文件夹,并在编辑器视图中打开App.js文件。删除<div className="App"></div>分隔符内的所有代码。现在你的源代码应该如下所示:

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
      </div>
    );
  }
}

export default App;

如果你已经在 VS Code 中安装了Reactjs code snippets,你可以通过输入con来自动创建一个默认构造函数。对于典型的 React 方法,还有许多不同的快捷方式,比如cdm代表componentDidMount()

首先,我们添加一个必要的构造函数和状态。我们将在我们的应用程序中显示温度、描述和天气图标,因此,我们定义了三个状态值。我们还将添加一个布尔状态来指示获取加载的状态。以下是构造函数的源代码:

  constructor(props) {
    super(props);
    this.state = {temp: 0, desc: '', icon: '', loading: true}
  }

当你使用 REST API 时,你应该首先检查响应,以便能够从 JSON 数据中获取值。在下面的示例中,你可以看到返回伦敦当前天气的地址。将地址复制到浏览器中,你可以看到 JSON 响应数据:

api.openweathermap.org/data/2.5/weather?q=London&units=Metric&APIkey=YOUR_KEY

从响应中,你可以看到可以使用main.temp来访问tempdescriptioniconweather数组中,该数组只有一个元素,我们可以使用weather[0].descriptionweather[0].icon来访问它:

componentDidMount()生命周期方法中使用fetch进行 REST API 调用。在成功响应后,我们将天气数据保存到状态中,并将loading状态更改为false。状态更改后,组件将重新渲染。我们将在下一步中实现render()方法。以下是componentDidMount()方法的源代码:

  componentDidMount() {
    fetch('http://api.openweathermap.org/data/2.5/weather?
      q=London&units=Metric
      &APIkey=c36b03a963176b9a639859e6cf279299')
    .then(response => response.json()) 
    .then(responseData => {
      this.setState({ 
         temp: responseData.main.temp,
         desc: responseData.weather[0].description,
         icon: responseData.weather[0].icon, 
         loading: false 
       })
     })
     .catch(err => console.error(err)); 
  }

在添加了componentDidMount()方法后,当组件挂载时会进行请求。我们可以使用 React Developer Tool 检查一切是否正确。在浏览器中打开你的应用程序,并打开浏览器开发者工具的 React 选项卡。现在你可以看到状态已更新为响应中的值。你还可以从网络选项卡中检查请求状态是否为 200 OK:

最后,我们实现render()方法来显示天气数值。我们使用条件渲染,否则,我们会因为第一次渲染调用中没有图像代码而出现错误,图像上传也不会成功。为了显示天气图标,我们必须在图标代码之前添加http://openweathermap.org/img/w/,在图标代码之后添加.png。然后,我们可以将连接的图像 URL 设置为img元素的src属性。温度和描述显示在段落元素中。°C HTML 实体显示摄氏度符号:

  render() {
    const imgSrc =    `http://openweathermap.org/img/w/${this.state.icon}.png`;

    if (this.state.loading) {
      return <p>Loading</p>;
    }
 else {
      return (
        <div className="App">
          <p>Temperature: {this.state.temp} °C</p>
          <p>Description: {this.state.desc}</p>
          <img src={imgSrc} alt="Weather icon" />
        </div>
      );
    }
  }

现在你的应用程序应该准备好了。当你在浏览器中打开它时,它应该看起来像下面的图片:

整个App.js文件的源代码如下所示:

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {temp: 0, desc: '', icon: ''}
  }

  componentDidMount() {
    fetch('http://api.openweathermap.org/data/2.5/weather?
      q=London&units=Metric&APIkey=YOUR_KEY')
    .then(response => response.json()) 
    .then(responseData => {
      this.setState({ 
         temp: responseData.main.temp,
        desc: responseData.weather[0].description,
        icon: responseData.weather[0].icon 
       }); 
    });
  }

  render() {
    const imgSrc = 'http://openweathermap.org/img/w/' + 
    this.state.icon + '.png';

    return (
      <div className="App">
        <p>Temperature: {this.state.temp}</p>
        <p>Description: {this.state.desc}</p>
        <img src={imgSrc} />
      </div>
    );
  }
}

export default App;

在第二个示例中,我们将使用 GitHub API 按关键字获取存储库。使用与上一个示例相同的步骤,创建一个名为restgithub的新 React 应用程序。启动应用程序并用 VS Code 打开项目文件夹。

App.js文件中的<div className="App"></div>分隔符中删除额外的代码,然后您的App.js代码应如下示例代码所示:

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
      </div>
    );
  }
}

export default App;

GitHub REST API 的 URL 如下:

https://api.github.com/search/repositories?q=KEYWORD

让我们通过在浏览器中输入 URL 并使用react关键字来检查 JSON 响应。从响应中,我们可以看到存储库作为名为items的 JSON 数组返回。从各个存储库中,我们将显示full_namehtml_url的值。我们将在表中呈现数据,并使用map函数将值转换为表行,就像在上一章中所示的那样:

我们将使用用户输入的关键字进行 REST API 调用。因此,我们不能在componentDidMount()方法中进行 REST API 调用,因为在那个阶段,我们没有用户输入可用。实现这一点的一种方法是创建一个输入字段和按钮。用户在输入字段中输入关键字,当按下按钮时进行 REST API 调用。我们需要两个状态,一个用于用户输入,一个用于 JSON 响应中的数据。以下是constructor的源代码。数据状态的类型是数组,因为存储库作为 JSON 数组返回在响应中:

  constructor(props) {
    super(props);
    this.state = { keyword: '', data: [] };
  }

接下来,我们将在render()方法中实现输入字段和按钮。我们还必须为输入字段添加一个更改监听器,以便能够将输入值保存到名为keyword的状态中。按钮有一个点击监听器,调用将使用给定关键字进行 REST API 调用的函数。

  fetchData = () => {
    // REST API call comes here
  }

  handleChange = (e) => {
    this.setState({keyword: e.target.value});
  }

  render() {
    return (
      <div className="App">
        <input type="text" onChange={this.handleChange} />
        <button onClick={this.fetchData} value={this.state.keyword} >Fetch</button>
      </div>
    );
  }

fetchData函数中,我们使用模板文字将urlkeyword状态连接起来。然后我们将响应中的items数组保存到名为data的状态中。以下是fetchData函数的源代码:

  fetchData = () => {
    const url = `https://api.github.com/search/repositories?
       q=${this.state.keyword}`;
    fetch(url)
    .then(response => response.json()) 
    .then(responseData => {
      this.setState({data : responseData.items }); 
    }); 
  } 

render方法中,我们首先使用map函数将data状态转换为表行。url存储库将是链接元素的href

  render() {
    const tableRows = this.state.data.map((item, index) => 
      <tr key={index}><td>{item.full_name}</td>
      <td><a href={item.html_url}>{item.html_url}</a></td></tr>); 

    return (
      <div className="App">
        <input type="text" onChange={this.handleChange} />
        <button onClick={this.fetchData} value={this.state.keyword} >Fetch</button>
        <table><tbody>{tableRows}</tbody></table>
      </div>
    );

以下屏幕截图显示了在 REST API 调用中使用 React 关键字时的最终应用程序:

整个App.js文件的源代码如下所示:

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = { keyword: '', data: [] };
  }

  fetchData = () => {
    const url = `https://api.github.com/search/repositories?
      q=${this.state.keyword}`;
    fetch(url)
    .then(response => response.json()) 
    .then(responseData => {
      this.setState({data : responseData.items }); 
    }); 
  }

  handleChange = (e) => {
    this.setState({keyword: e.target.value});
  }

  render() {
    const tableRows = this.state.data.map((item, index) => 
      <tr key={index}><td>{item.full_name}</td>
      <td><a href={item.html_url}>{item.html_url}</a></td></tr>); 

    return (
      <div className="App">
        <input type="text" onChange={this.handleChange} />
        <button onClick={this.fetchData} 
        value={this.state.keyword} >Fetch</button>
        <table><tbody>{tableRows}</tbody></table>
      </div>
    );
  }
}

摘要

在这一章中,我们专注于使用 React 进行网络编程。我们从使异步网络调用更容易实现的 promise 开始。这是一种更清洁的处理调用的方式,比传统的回调函数要好得多。在本书中,我们使用 Fetch API 进行网络编程,因此我们介绍了使用fetch的基础知识。我们实现了两个实用的 React 应用程序,调用了开放的 REST API,并在浏览器中呈现了响应数据。在下一章中,我们将介绍一些有用的 React 组件,这些组件将在我们的前端中使用。

问题

  1. 什么是 promise?

  2. 什么是fetch

  3. 您应该如何从 React 应用程序调用 REST API?

  4. 您应该如何处理 REST API 调用的响应?

进一步阅读

Packt 还有其他很好的资源可供学习 React:

第八章:React 的有用第三方组件

React 是基于组件的,我们可以找到许多有用的第三方组件,可以在我们的应用程序中使用。在本章中,我们将看一些组件,我们将在前端中使用。我们将看到如何找到合适的组件,以及如何在自己的应用程序中使用这些组件。

在本章中,我们将看以下内容:

  • 如何找到第三方 React 组件

  • 如何安装组件

  • 如何使用 React 表格组件

  • 如何使用模态窗口组件

  • 如何使用 Material UI 组件库

  • 如何在 React 中管理路由

技术要求

在本书中,我们使用的是 Windows 操作系统,但所有工具都适用于 Linux 和 macOS,因为 Node.js 和create-react-app必须安装。

使用第三方 React 组件

有许多不同目的的不错的 React 组件可用。我们的第一个任务是找到适合您需求的组件。搜索组件的一个好网站是 JS.coach (js.coach/)。您只需输入关键字,搜索,并从框架列表中选择 React。在下面的屏幕截图中,您可以看到搜索 React 表组件的结果:

另一个获取 React 组件的好来源是 Awesome React Components (github.com/brillout/awesome-react-components)。

组件通常有良好的文档,帮助您在自己的 React 应用程序中使用它们。让我们看看如何将第三方组件安装到我们的应用程序中并开始使用它。转到 JS.coach 网站,输入list以搜索输入字段,并按 React 进行过滤。从搜索结果中,您可以找到名为react-tiny-virtual-list的列表组件:

单击组件链接以查看有关组件的更详细信息。通常,您可以在那里找到安装说明,以及如何使用组件的一些简单示例。信息页面通常提供组件网站或 GitHub 存储库的地址,您可以在那里找到完整的文档:

从组件的信息页面可以看出,使用npm安装组件。命令的语法如下:

npm install component_name --save

或者,如果您使用 Yarn,则如下所示:

yarn add component_name

--save参数将组件的依赖项保存到 React 应用程序根文件夹中的package.json文件中。如果您使用的是 npm 5 或更高版本,则默认情况下会执行此操作,无需--save参数。对于 Yarn,您不必指定,因为它默认保存组件依赖项。

现在我们将react-tiny-virtual-list组件安装到我们在上一章中创建的myapp React 应用程序中。您必须转到应用程序的根文件夹,并输入以下命令:

npm install react-tiny-virtual-list --save

如果您打开应用程序根文件夹中的package.json文件,您会看到该组件现在已添加到依赖项中:

{
  "name": "myapp",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "¹⁶.3.2",
    "react-dom": "¹⁶.3.2",
    "react-scripts": "1.1.4",
    "react-tiny-virtual-list": "².1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}

安装的组件保存在应用程序的node_modules文件夹中。如果打开该文件夹,您应该会找到react-tiny-virtual-list文件夹:

现在,如果您将 React 应用程序源代码推送到 GitHub,则不应包括node_modules,因为该文件夹非常大。create-react-app包含一个.gitignore文件,该文件将node_modules文件夹从存储库中排除。.gitignore文件的内容如下:

# See https://help.github.com/ignore-files/ for more about ignoring files.

# dependencies
/node_modules

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

想法是,当您从 GitHub 克隆应用程序时,您键入npm install命令,该命令从package.json文件中读取依赖项,并将其下载到您的应用程序中。

开始使用已安装组件的最后一步是将其导入到您使用它的文件中:

import VirtualList from 'react-tiny-virtual-list';

React 表格

React Table (react-table.js.org)是用于 React 应用程序的灵活表格组件。它具有许多有用的功能,如过滤、排序和透视。让我们使用在上一章中创建的 GitHub REST API 应用程序:

  1. 安装react-table组件。打开 PowerShell 并移动到restgithub文件夹,这是应用程序的根文件夹。通过输入以下命令来安装组件:
 npm install react-table --save
  1. 使用 VS Code 打开App.js文件,并删除render()方法中的所有代码,除了包含按钮和输入字段的return语句。现在App.js文件应该如下所示:
      import React, { Component } from 'react';
      import './App.css';

      class App extends Component {
        constructor(props) {
          super(props);
          this.state = { keyword: '', data: [] };
        }

        fetchData = () => {
          const url = `https://api.github.com/search/repositories?
           q=${this.state.keyword}`;
          fetch(url)
          .then(response => response.json()) 
          .then(responseData => {
            this.setState({data : responseData.items }); 
          }); 
        }

        handleChange = (e) => {
          this.setState({keyword: e.target.value});
        }

        render() {
          return (
            <div className="App">
              <input type="text" onChange={this.handleChange} />
              <button onClick={this.fetchData} value=
               {this.state.keyword} >Fetch</button>
            </div>
          );
        }
      }

      export default App;
  1. App.js文件的开头添加以下行来导入react-table组件和样式表:
      import ReactTable from "react-table";
      import 'react-table/react-table.css';
  1. 要填充 React Table 的数据,你必须将数据传递给组件的数据属性。数据可以是数组或对象,因此我们可以使用我们的状态,称为data。列使用列属性进行定义,该属性是必需的:
      <ReactTable
        data={data}
        columns={columns}
      />
  1. 我们将通过在render()方法中创建列对象数组来定义我们的列。在列对象中,你至少需要定义列的标题和数据访问器。数据访问器的值来自我们的 REST API 响应数据。你可以看到我们的响应数据包含一个名为owner的对象,我们可以使用owner.field_name语法显示这些值:
      const columns = [{
         Header: 'Name',  // Header of the column  
         accessor: 'full_name' // Value accessor
        }, {
         Header: 'URL',
         accessor: 'html_url',
        }, {
         Header: 'Owner',
         accessor: 'owner.login',
      }]
  1. 将 React Table 组件添加到我们的render()方法中,然后该方法的源代码如下:
      render() {
        const columns = [{
          Header: 'Name', // Header of the column
          accessor: 'full_name' // Value accessor
        }, {
          Header: 'URL',
          accessor: 'html_url',
        }, {
          Header: 'Owner',
          accessor: 'owner.login',
        }]

        return (
          <div className="App">
            <input type="text" onChange={this.handleChange} />
            <button onClick={this.fetchData} 
             value={this.state.keyword} >Fetch</button>
            <ReactTable
              data={this.state.data}
              columns={columns}

            />
          </div>
        );
      }
  1. 运行应用程序并导航到localhost:3000。表看起来非常不错。它默认提供了排序和分页功能:

过滤默认情况下是禁用的,但你可以使用ReactTable组件中的filterable属性来启用它。你还可以设置表的页面大小:

<ReactTable
   data={this.state.data}
   columns={columns}
   filterable={true}
   defaultPageSize = {10}
/>

现在你应该在你的表中看到过滤元素。你可以使用任何列进行过滤,但也可以在列级别设置过滤和排序:

你可以从 React Table 网站上找到表格和列的不同属性。

单元格渲染器可用于自定义表格单元格的内容。以下示例显示了如何将按钮呈现为表格单元格。单元格渲染器中的函数将value作为参数传递,而在这种情况下,值将是列的访问器中定义的full_name。另一个选项是传递一个行,它将整个row对象传递给函数。然后你需要定义btnClick函数,当按钮被按下时将被调用,你可以对发送到函数的值进行操作:

render() {
  const columns = [{
    Header: 'Name', // Header of the column
    accessor: 'full_name' // Value accessor
  }, {
    Header: 'URL',
    accessor: 'html_url',
  }, {
    Header: 'Owner',
    accessor: 'owner.login',
  }, {
    id: 'button',
    sortable: false,
    filterable: false,
    width: 100,
    accessor: 'full_name',
    Cell: ({value}) => (<button className="btn btn-default btn-link" onClick=                            {() => {this.btnClick(value)}}>Press me</button>)
}]

以下是带有按钮的表格的屏幕截图:

模态窗口组件

模态窗口在创建 CRUD 应用程序时非常有用。我们将创建一个简单的购物清单应用程序,用户可以使用模态窗口添加新项目。我们在示例中将使用的模态窗口组件是react-skylightmarcio.github.io/react-skylight/):

  1. 创建一个名为shoppinglist的新 React 应用程序,并使用以下命令安装react-skylight
 npm install react-skylight --save
  1. 使用 VS Code 打开app文件夹,并在代码编辑器中打开App.js文件。在App.js组件中,我们只需要一个状态来保存购物清单项目。一个购物清单项目包含两个字段——产品和数量。我们还需要一个方法来向列表中添加新项目。以下是构造函数和向列表中添加新项目的方法的源代码。在addItem方法中,我们使用了扩展符号(...),用于在现有数组的开头添加新项目:
      constructor(props) {
        super(props);
        this.state={ items: [] };
      }

      addItem = (item) => {
        this.setState({items: [item, ...this.state.items]});
      }
  1. 添加一个新组件来添加购物项。在应用程序的根文件夹中创建一个名为AddItem.js的新文件。该组件将使用 React Skylight 模态表单,因此让我们导入react-skylight。在render()方法中的 React Skylight 组件内,我们将添加两个输入字段(产品和数量)和一个调用addItem函数的按钮。为了能够调用App.js组件中的addItem函数,我们必须在渲染AddItem组件时将其作为 prop 传递。在 React Skylight 组件之外,我们将添加一个按钮,当按下时打开模态表单。该按钮是组件初始渲染时唯一可见的元素,并调用 React Skylight 的show()方法来打开模态表单。我们还必须处理输入字段的更改事件,以便访问已输入的值。当模态表单内的按钮被点击时,将调用addItem函数,并使用 React Skylight 的hide()方法关闭模态表单。该函数从输入字段值创建一个对象,并调用App.js组件的addItem函数,最终向状态数组中添加一个新项目并重新渲染用户界面:
import React, { Component } from 'react';
import SkyLight from 'react-skylight';

class AddItem extends Component {
  constructor(props) {
    super(props);
  }

  // Create new shopping item and calls addItem function. 
  // Finally close the modal form
  addItem = () => {
    const item = {product: this.state.product,
     amount: this.state.amount};
    this.props.additem(item);
    this.addform.hide();
  }

  handleChange = (e) => {
    this.setState({[e.target.name]: e.target.value});
  }

  render() {
    return (
      <div>
        <section>
          <button onClick={() => this.addform.show()}>Add
           Item</button>
        </section>
        <SkyLight 
          hideOnOverlayClicked 
          ref={ref => this.addform = ref} 
          title="Add item">
          <input type="text" name="product"
           onChange={this.handleChange} 
           placeholder="product" /><br/>
          <input type="text" name="amount"
           onChange={this.handleChange} 
           placeholder="amount" /><br/>
          <button onClick={this.addItem}>Add</button>
        </SkyLight> 
      </div>
    );
  }
}

export default AddItem;
  1. App.js文件中修改render()方法。将AddItem组件添加到render()方法中,并将addItem函数作为 prop 传递给AddItem组件。在方法的开头,我们使用map函数将项目转换为listItems(<li></li>):
// App.js
render() {
  const listItems = this.state.items.map((item, index) => 
    <li key={index}>{item.product} {item.amount}</li>)

  return (
    <div className="App">
      <h2>Shopping list</h2>
      <AddItem additem={this.addItem}/>
      <ul>{listItems}</ul>
    </div>
  );
}

现在打开应用程序时,您将看到一个空列表和一个添加新项目的按钮:

当您按下“Add Item”按钮时,模态表单将打开:

在输入框中输入一些值,然后按下“Add”按钮。模态表单将关闭,并且新项目将显示在列表中:

Material UI 组件库

Material UI 是实现 Google 的 Material Design 的 React 组件库。它包含许多不同的组件,如按钮、列表、表格和卡片,您可以使用它们来获得一个漂亮和统一的用户界面。我们将继续使用购物清单应用程序,并开始使用 Material UI 来设计用户界面:

  1. 使用 VS Code 打开购物清单应用程序。在根文件夹中键入以下命令来安装 Material UI 到 PowerShell 或您正在使用的任何合适的终端中:
npm install @material-ui/core --save

OR with yarn

yarn add @material-ui/core
  1. 我们准备开始使用 Material UI 组件。首先,我们将更改AddItem.js文件中的按钮,以使用 Material UI 的Button组件。我们必须导入Button组件,然后在render()方法中使用它。Material UI 文档中可以找到Button的不同 props:
// Import RaisedButton
import RaisedButton from '@material-ui/core/Button';

// Use RaisedButton in render() method
render() {
  return (
    <div>
      <section>
        <Button onClick={() => this.addform.show()} 
         variant="raised" color="primary">
         Add Item</ Button>
      </section>
      <SkyLight 
        hideOnOverlayClicked 
        ref={ref => this.addform = ref} 
        title="Add item">
        <input type="text" name="product" 
         onChange={this.handleChange} 
         placeholder="product" /><br/>
        <input type="text" name="amount" 
         onChange={this.handleChange} 
         placeholder="amount" /><br/>
        <Button onClick={this.addItem} 
         variant="default"  >Add</ Button>
      </SkyLight> 
    </div>
  );
}

现在应用程序使用RaisedButton,效果如下:

  1. AddItem.js中的输入字段更改为使用 Material UI 的TextField组件。步骤与按钮相同。导入TextField组件,然后在render()方法中使用它:
// Import TextField component
import TextField from '@material-ui/core/TextField';

// Use TextField in render() method
render() {
  return (
    <div>
      <section>
        <Button onClick={() => this.addform.show()} 
         variant="raised" color="primary">
         Add Item</ Button>
      </section>
      <SkyLight 
        hideOnOverlayClicked 
        ref={ref => this.addform = ref} 
        title="Add item">
        <TextField type="text" name="product" 
          onChange={this.handleChange} 
          placeholder="product" /><br/>
        <TextField type="text" name="amount" 
          onChange={this.handleChange} 
          placeholder="amount" /><br/>
        <Button onClick={this.addItem} 
         variant="default"  >Add</ Button>     
      </SkyLight> 
    </div>
  );
}

在更改后,您的表单应如下所示:

  1. App.js文件中更改我们的列表,使用 Material UI 的ListListItem组件。导入这些组件,并在创建listItems和渲染List的地方使用ListItem。我们将在ListItemText组件的次要文本中显示产品的数量:
// Import List, ListItem and ListItemText components
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';

// Use List and ListItem in render() method
render() {
 // Use ListItem component here instead of li 
    const listItems = this.state.items.map((item, index) => 
     <ListItem key={index}>
     <ListItemText primary={item.product} secondary={item.amount} />
     </ListItem>)
  return (
    <div className="App">
      <h2>Shopping list</h2>
      <AddItem additem={this.addItem}/>
      <List>{listItems}</List>
    </div>
  );
}

现在用户界面如下所示。通过少量的工作,用户界面现在更加精致:

路由

React 中有多种可用的路由解决方案。最流行的解决方案是 React Router(github.com/ReactTraining/react-router)。对于 Web 应用程序,React Router 提供了一个名为react-router-dom的包。

要开始使用 React Router,我们必须使用以下命令进行安装:

npm install react-router-dom --save

react-router-dom中有四个不同的组件需要实现路由。BrowserRouter是用于基于 Web 的应用程序的路由器。Route组件在给定位置匹配时呈现定义的组件。以下是Route组件的两个示例。第一个示例在用户导航到/contact端点时呈现Contact组件。您还可以使用Route组件进行内联呈现,如第二个示例所示:

<Route path="/contact" component={Contact} />
// Route with inline rendering
<Route path="/links" render={() => <h1>Links</h1>} />

Switch组件包装多个Route组件。Link组件提供了应用程序的导航。以下示例显示了Contact链接,并在单击链接时导航到/contact端点:

<Link to="/contact">Contact</Link>

以下示例显示了如何在实践中使用这些组件。让我们使用create-react-app创建一个名为routerapp的新 React 应用程序。使用 VS Code 打开应用程序文件夹,并打开App.js文件以编辑视图。从react-router-dom包中导入组件,并从渲染方法中删除额外的代码。修改后,您的App.js源代码应如下所示:

import React, { Component } from 'react';
import './App.css';
import { BrowserRouter, Switch, Route, Link } from 'react-router-dom'

class App extends Component {
  render() {
    return (
      <div className="App">
      </div>
    );
  }
}

export default App;

让我们首先创建两个简单的组件,我们可以在路由中使用。在应用程序根文件夹中创建两个名为Home.jsContact.js的新文件。只需向render()方法中添加标题,以显示组件的名称。请参阅以下组件的代码:

//Contact.js
import React, { Component } from 'react';

class Contact extends Component {
  render() {
    return (
      <div>
        <h1>Contact.js</h1>
      </div>
    );
  }
}

export default Contact;

// Home.js
import React, { Component } from 'react';

class Home extends Component {
  render() {
    return (
      <div>
        <h1>Home.js</h1>
      </div>
    );
  }
}

export default Links;

打开App.js文件,让我们添加一个路由器,允许我们在组件之间导航:

import React, { Component } from 'react';
import './App.css';
import { BrowserRouter, Switch, Route, Link } from 'react-router-dom'
import Contact from './Contact';
import Home from './Home';

class App extends Component {
  render() {
    return (
      <div className="App">
        <BrowserRouter>
          <div>
            <Link to="/">Home</Link>{' '}
            <Link to="/contact">Contact</Link>{' '} 
            <Link to="/links">Links</Link>{' '} 
            <Switch>
              <Route exact path="/" component={Home} />
              <Route path="/contact" component={Contact} />
              <Route path="/links" render={() => <h1>Links</h1>} />
              <Route render={() => <h1>Page not found</h1>} />
            </Switch>
          </div>
        </BrowserRouter>
      </div>
    );
  }
}

export default App;

现在,当您启动应用程序时,您将看到链接和Home组件,该组件显示在根端点(localhost:3030/)中,如第一个Route组件中所定义。第一个Route组件中的exact关键字表示路径必须完全匹配。如果删除该关键字,则路由始终转到Home组件:

当您点击Contact链接时,将呈现Contact组件:

总结

在本章中,我们学习了如何使用第三方 React 组件。我们熟悉了几个我们将在前端中使用的组件。React Table 是带有内置功能(如排序、分页和过滤)的表组件。React Skylight 是我们将在前端中使用的模态表单组件,用于创建添加和编辑项目的表单。Material UI 是提供多个实现 Google Material Design 的用户界面组件的组件库。我们还学习了如何在 React 应用程序中使用 React Router 进行路由。在下一章中,我们将为前端开发构建一个环境。

问题

  1. 您应该如何找到 React 的组件?

  2. 您应该如何安装组件?

  3. 您应该如何使用 React Table 组件?

  4. 您应该如何使用 React 创建模态表单?

  5. 您应该如何使用 Material UI 组件库?

  6. 您应该如何在 React 应用程序中实现路由?

进一步阅读

Packt 有其他很好的资源可以学习 React:

第九章:为我们的 Spring Boot RESTful Web Service 设置前端

本章解释了开始开发前端部分所需的步骤。我们首先会定义我们正在开发的功能。然后我们将对用户界面进行模拟。作为后端,我们将使用我们在第四章中的 Spring Boot 应用程序,保护和测试您的后端。我们将使用后端的未安全版本开始开发。最后,我们将创建我们将在前端开发中使用的 React 应用程序。

在本章中,我们将看到以下内容:

  • 为什么以及如何进行模拟

  • 为前端开发准备我们的 Spring Boot 后端

  • 为前端创建 React 应用

技术要求

我们需要在第四章中创建的 Spring Boot 应用程序,保护和测试您的后端

Node.js 和create-react-app应该已安装。

模拟用户界面

在本书的前几章中,我们创建了一个提供 REST API 的汽车数据库后端。现在是时候开始构建我们应用程序的前端了。我们将创建一个从数据库中列出汽车并提供分页、排序和过滤的前端。有一个按钮可以打开模态表单,将新车添加到数据库中。在汽车表的每一行中,都有一个按钮可以从数据库中删除汽车。表行也是可编辑的,可以通过单击行中的“保存”按钮将修改保存到数据库中。前端包含一个链接或按钮,可以将表中的数据导出到 CSV 文件中。

让我们从用户界面创建一个模拟。有很多不同的应用程序可以用来创建模拟,或者你甚至可以使用铅笔和纸。您还可以创建交互式模拟以演示一些功能。如果您已经完成了模拟,那么在开始编写任何实际代码之前,与客户讨论需求就会更容易。有了模拟,客户也更容易理解前端的想法并对其产生影响。与真实的前端源代码相比,对模拟的修改真的很容易和快速。

以下截图显示了我们汽车列表前端的模拟:

当用户按下“New Car”按钮时打开的模态表单如下所示:

准备 Spring Boot 后端

我们将使用后端的未安全版本开始前端开发。在第一阶段,我们实现所有 CRUD 功能并测试这些功能是否正常工作。在第二阶段,我们在后端启用安全性,并进行所需的修改,最后我们实现身份验证。

使用 Eclipse 打开 Spring Boot 应用程序,我们在第四章中创建的,保护和测试您的后端。打开定义 Spring Security 配置的SecurityConfig.java文件。暂时注释掉当前配置,并允许每个人访问所有端点。参见以下修改:

  @Override
  protected void configure(HttpSecurity http) throws Exception {
   // Add this row to allow access to all endpoints
   http.cors().and().authorizeRequests().anyRequest().permitAll(); 

   /* Comment this out
   http.cors().and().authorizeRequests()
     .antMatchers(HttpMethod.POST, "/login").permitAll()
     .anyRequest().authenticated()
     .and()
     // Filter for the api/login requests
     .addFilterBefore(new LoginFilter("/login", authenticationManager()),
             UsernamePasswordAuthenticationFilter.class)
     // Filter for other requests to check JWT in header
     .addFilterBefore(new AuthenticationFilter(),
      UsernamePasswordAuthenticationFilter.class);
     */
    }

现在,如果您运行后端并使用 Postman 测试http:/localhost:8080/api/cars端点,您应该会在响应中获得所有汽车,如下截图所示:

为前端创建 React 项目

在开始编写前端代码之前,我们必须创建一个新的 React 应用程序:

  1. 打开 PowerShell 或任何其他适合的终端。通过输入以下命令创建一个新的 React 应用程序:
create-react-app carfront
  1. 通过输入以下命令运行应用程序:
npm start

或者,如果您正在使用 Yarn,请输入以下内容:

yarn start
  1. 使用 VS Code 打开app文件夹,删除任何额外的代码,并从App.js文件中更改标题文本。修改后,您的App.js文件源代码应如下所示:
import React, { Component } from 'react';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <h1 className="App-title">CarList</h1>
        </header> 
      </div>
    );
  }
}

export default App;
  1. 让我们也减少标题的高度,并将颜色更改为lightblue。打开App.css文件,你可以在App.js文件的样式中找到。将标题高度从 150 减少到 50,并将颜色更改为lightblue
.App-header {
  background-color:lightblue;
  height: 50px;
  padding: 20px;
  color: white;
}

现在你的前端起点应该如下所示:

总结

在本章中,我们开始开发我们的前端,使用我们在第四章中创建的后端,保护和测试您的后端。我们定义了前端的功能,并创建了用户界面的模拟。我们从未经保护的后端开始了前端开发,因此,我们对 Spring Security 配置类进行了一些修改。我们还创建了我们在开发过程中将要使用的 React 应用程序。在下一章中,我们将开始为我们的前端添加 CRUD 功能。

问题

  1. 为什么你应该做用户界面的模拟?

  2. 你应该如何做用户界面的模拟?

  3. 你应该如何从后端禁用 Spring Security?

进一步阅读

Packt 还有其他关于学习 React 的很棒的资源:

第十章:添加 CRUD 功能

本章描述了如何在我们的前端实现 CRUD 功能。我们将使用第八章中学到的组件,React 有用的第三方组件。我们将从后端获取数据并在表中呈现数据。然后,我们将实现删除、编辑和添加功能。最后,我们将添加功能以将数据导出到 CSV 文件。

在本章中,我们将讨论以下内容:

  • 如何从后端获取数据并在前端呈现数据

  • 如何使用 REST API 删除、添加和更新数据

  • 如何向用户显示提示消息

  • 如何从 React 应用程序导出数据到 CSV 文件

技术要求

我们在第四章中创建的 Spring Boot 应用程序需要与上一章的修改(未经保护的后端)一起使用。

我们还需要在上一章中创建的 React 应用程序(carfront)。

创建列表页面

在第一阶段,我们将创建列表页面,显示带分页、过滤和排序功能的汽车。运行 Spring Boot 后端,可以通过向http://localhost:8080/api/cars URL 发送GET请求来获取汽车,如第三章中所示,使用 Spring Boot 创建 RESTful Web 服务

让我们检查来自响应的 JSON 数据。汽车数组可以在 JSON 响应数据的_embedded.cars节点中找到:

现在,一旦我们知道如何从后端获取汽车,我们就准备好实现列表页面来显示汽车。以下步骤描述了这一实践:

  1. 打开 VS Code 中的carfront React 应用程序(在上一章中创建的 React 应用程序)。

  2. 当应用程序有多个组件时,建议为它们创建一个文件夹。在src文件夹中创建一个名为components的新文件夹。使用 VS Code,可以通过右键单击侧边栏文件资源管理器中的文件夹,并从菜单中选择“新建文件夹”来创建文件夹:

  1. components文件夹中创建一个名为Carlist.js的新文件,现在您的项目结构应如下所示:

  1. 在编辑器视图中打开Carlist.js文件,并编写组件的基本代码,如下所示:
import React, { Component } from 'react';

class Carlist extends Component {

  render() {
    return (
      <div></div>
    );
  }
}

export default Carlist;
  1. 我们需要一个从 REST API 获取的cars状态,因此,我们必须添加构造函数并定义一个数组类型的状态值:
constructor(props) {
  super(props);
  this.state = { cars: []};
} 
  1. componentDidMount()生命周期方法中执行fetch。来自 JSON 响应数据的汽车将保存到名为cars的状态中:
  componentDidMount() {
    fetch('http://localhost:8080/api/cars')
    .then((response) => response.json()) 
    .then((responseData) => { 
      this.setState({ 
        cars: responseData._embedded.cars,
      }); 
    })
    .catch(err => console.error(err)); 
  }
  1. 使用 map 函数将car对象转换为render()方法中的表行,并添加表元素:
render() {
  const tableRows = this.state.cars.map((car, index) => 
    <tr key={index}>
      <td>{car.brand}</td>
      <td>{car.model}</td>
      <td>{car.color}</td>
      <td>{car.year}</td>
      <td>{car.price}</td>
    </tr>
  );

  return (
    <div className="App">
      <table>
        <tbody>{tableRows}</tbody>
      </table>
    </div>
  );
}

现在,如果使用npm start命令启动 React 应用程序,应该会看到以下列表页面:

当我们创建更多的 CRUD 功能时,URL 服务器可能会重复多次,并且当后端部署到本地主机以外的服务器时,它将发生变化。因此,最好将其定义为常量。然后,当 URL 值更改时,我们只需在一个地方进行修改。让我们在我们应用程序的根文件夹中创建一个名为constants.js的新文件。在编辑器中打开文件,并将以下行添加到文件中:

export const SERVER_URL = 'http://localhost:8080/'

然后,我们将其导入到我们的Carlist.js文件中,并在fetch方法中使用它:

//Carlist.js
// Import server url (named import)
import {SERVER_URL} from '../constants.js'

// Use imported constant in the fetch method
fetch(SERVER_URL + 'api/cars')

最后,您的Carlist.js文件源代码应如下所示:

import React, { Component } from 'react';
import {SERVER_URL} from '../constants.js'

class Carlist extends Component {
  constructor(props) {
    super(props);
    this.state = { cars: []};
  }

  componentDidMount() {
    fetch(SERVER_URL + 'api/cars')
    .then((response) => response.json()) 
    .then((responseData) => { 
      this.setState({ 
        cars: responseData._embedded.cars,
      }); 
    })
    .catch(err => console.error(err)); 
  }

  render() {
    const tableRows = this.state.cars.map((car, index) => 
      <tr key={index}><td>{car.brand}</td>
       <td>{car.model}</td><td>{car.color}</td>
       <td>{car.year}</td><td>{car.price}</td></tr>);

    return (
      <div className="App">
        <table><tbody>{tableRows}</tbody></table>
      </div>
    );
  }
}

export default Carlist;

现在我们将使用 React Table 来获得分页、过滤和排序功能。通过在终端中按Ctrl + C停止开发服务器,并输入以下命令来安装 React Table。安装完成后,重新启动应用程序:

npm install react-table --save

react-table和样式表导入到您的Carlist.js文件中:

import ReactTable from "react-table";
import 'react-table/react-table.css';

然后从render()方法中删除tabletableRows。React Table 的data属性是this.state.cars,其中包含获取的汽车。我们还必须定义表的columns,其中accessorcar对象的字段,header是标题的文本。为了启用过滤,我们将表的filterable属性设置为true。请参阅以下render()方法的源代码:

  render() {
    const columns = [{
      Header: 'Brand',
      accessor: 'brand'
    }, {
      Header: 'Model',
      accessor: 'model',
    }, {
      Header: 'Color',
      accessor: 'color',
    }, {
      Header: 'Year',
      accessor: 'year',
    }, {
      Header: 'Price €',
      accessor: 'price',
    },]

    return (
      <div className="App">
        <ReactTable data={this.state.cars} columns={columns} 
          filterable={true}/>
      </div>
    );
  }

使用 React Table 组件,我们用少量的编码获得了表的所有必要功能。现在列表页面看起来像下面这样:

删除功能

可以通过向http://localhost:8080/api/cars/[carid]端点发送DELETE方法请求从数据库中删除项目。如果我们查看 JSON 响应数据,我们可以看到每辆汽车都包含一个指向自身的链接,并且可以从_links.self.href节点访问,如下截图所示:

以下步骤显示了如何实现删除功能:

  1. 我们将为表中的每一行创建一个按钮,按钮的访问器将是_links.self.href,我们可以使用它来调用我们即将创建的删除函数。但首先,使用Cell向表中添加一个新列来渲染按钮。请参阅以下源代码。我们不希望为按钮列启用排序和过滤,因此这些属性被设置为false。按钮在按下时调用onDelClick函数,并将汽车的链接作为参数发送:
  const columns = [{
    Header: 'Brand',
    accessor: 'brand'
  }, {
    Header: 'Model',
    accessor: 'model',
  }, {
    Header: 'Color',
    accessor: 'color',
  }, {
    Header: 'Year',
    accessor: 'year',
  }, {
    Header: 'Price €',
    accessor: 'price',
  }, {
    id: 'delbutton',
    sortable: false,
    filterable: false,
    width: 100,
    accessor: '_links.self.href',
    Cell: ({value}) => (<button onClick={()=>{this.onDelClick(value)}}>Delete</button>)
  }]
  1. 实现onDelClick函数。但首先,让我们从componentDidMount()方法中取出fetchCars函数。这是因为我们希望在汽车被删除后也调用fetchCars函数,以向用户显示更新后的汽车列表。创建一个名为fetchCars()的新函数,并将componentDidMount()方法中的代码复制到新函数中。然后从componentDidMount()函数中调用fetchCars()函数以最初获取汽车:
componentDidMount() {
  this.fetchCars();
}

fetchCars = () => {
  fetch(SERVER_URL + 'api/cars')
  .then((response) => response.json()) 
  .then((responseData) => { 
    this.setState({ 
      cars: responseData._embedded.cars,
    }); 
  })
  .catch(err => console.error(err)); 
}
  1. 实现onDelClick函数。我们向汽车链接发送DELETE请求,当删除成功删除时,我们通过调用fetchCars()函数刷新列表页面:
// Delete car
onDelClick = (link) => {
  fetch(link, {method: 'DELETE'})
  .then(res => this.fetchCars())
  .catch(err => console.error(err)) 
}

当您启动应用程序时,前端应该如下截图所示,当按下删除按钮时,汽车将从列表中消失:

在成功删除或出现错误时,向用户显示一些反馈会很好。让我们实现一个提示消息来显示删除的状态。为此,我们将使用react-toastify组件(github.com/fkhadra/react-toastify)。通过在您使用的终端中键入以下命令来安装该组件:

npm install react-toastify --save

安装完成后,启动您的应用程序并在编辑器中打开Carlist.js文件。我们必须导入ToastContainertoast和样式表以开始使用react-toastify。将以下导入语句添加到您的Carlist.js文件中:

import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

ToastContainer是用于显示提示消息的容器组件,应该在render()方法中。在ToastContainer中,您可以使用autoClose属性以毫秒为单位定义提示消息的持续时间。在render()方法的返回语句中添加ToastContainer组件,就在ReactTable之后:

return (
  <div className="App">
     <ReactTable data={this.state.cars} columns={columns} 
       filterable={true}/>
     <ToastContainer autoClose={1500} } /> 
   </div>
);

然后,我们将在onDelClick()函数中调用 toast 方法来显示提示消息。您可以定义消息的类型和位置。成功删除时显示成功消息,在出现错误时显示错误消息:

// Delete car
onDelClick = (link) => {
  fetch(link, {method: 'DELETE'})
  .then(res => {
    toast.success("Car deleted", {
      position: toast.POSITION.BOTTOM_LEFT
    });
    this.fetchCars();
  })
  .catch(err => {
    toast.error("Error when deleting", {
      position: toast.POSITION.BOTTOM_LEFT
    });
    console.error(err)
  }) 
 }

现在,当汽车被删除时,您将看到提示消息,如下截图所示:

为了避免意外删除汽车,按下删除按钮后最好有一个确认对话框。我们将使用react-confirm-alert组件(github.com/GA-MO/react-confirm-alert)来实现这一点。如果您的应用程序正在运行,请通过在终端中按下Ctrl + C来停止开发服务器,并输入以下命令来安装react-confirm-alert。安装后,重新启动应用程序:

npm install react-confirm-alert --save

confirmAlert和 CSS 文件导入Carlist组件:

import { confirmAlert } from 'react-confirm-alert';
import 'react-confirm-alert/src/react-confirm-alert.css' 

创建一个名为confirmDelete的新函数,用于打开确认对话框。如果对话框的“是”按钮被按下,将调用onDelClick函数并删除汽车:

confirmDelete = (link) => {
  confirmAlert({
    message: 'Are you sure to delete?',
    buttons: [
      {
        label: 'Yes',
        onClick: () => this.onDelClick(link)
      },
      {
        label: 'No',
      }
    ]
  })
}

然后,将删除按钮的onClick事件中的函数更改为confirmDelete

render() {
  const columns = [{
    Header: 'Brand',
    accessor: 'brand',
  }, {
    Header: 'Model',
    accessor: 'model',
  }, {
    Header: 'Color',
    accessor: 'color',
  }, {
    Header: 'Year',
    accessor: 'year',
  }, {
    Header: 'Price €',
    accessor: 'price',
  }, {
    id: 'delbutton',
    sortable: false,
    filterable: false,
    width: 100,
    accessor: '_links.self.href',
    Cell: ({value}) => (<button onClick=
      {()=>{this.confirmDelete(value)}}>Delete</button>)
  }]

如果您现在按下删除按钮,确认对话框将被打开,只有当您按下“是”按钮时,汽车才会被删除:

添加功能

下一步是为前端创建添加功能。我们将使用 React Skylight 模态组件来实现这一点。我们已经介绍了在第八章中使用 React Skylight 的用法,React 的有用的第三方组件。我们将在用户界面中添加“新车”按钮,当按下时打开模态表单。模态表单包含保存汽车所需的所有字段,以及用于保存和取消的按钮。

通过在终端中按下Ctrl + C来停止开发服务器,并输入以下命令来安装 React Skylight。安装后,重新启动应用程序:

npm install react-skylight --save 

以下步骤显示了如何使用模态表单组件创建添加功能:

  1. components文件夹中创建一个名为AddCar.js的新文件,并将组件类基本代码写入文件中,如下所示。添加react-skylight组件的导入:
import React from 'react';
import SkyLight from 'react-skylight';

class AddCar extends React.Component {
  render() {
    return (
      <div>
      </div> 
    );
  }
}

export default AddCar;
  1. 引入一个包含所有汽车字段的状态:
constructor(props) {
   super(props);
   this.state = {brand: '', model: '', year: '', color: '', price: ''};
}
  1. render()方法中添加一个表单。表单包含ReactSkylight模态表单组件,其中包含按钮和收集汽车数据所需的输入字段。打开模态窗口的按钮将显示在 carlist 页面上,必须在ReactSkylight之外。所有输入字段都应该有一个name属性,其值与将保存值的状态的名称相同。输入字段还具有onChange处理程序,通过调用handleChange函数将值保存到状态:
handleChange = (event) => {
   this.setState(
     {[event.target.name]: event.target.value}
   );
}

render() {
    return (
      <div>
        <SkyLight hideOnOverlayClicked ref="addDialog">
          <h3>New car</h3>
          <form>
            <input type="text" placeholder="Brand" name="brand" 
              onChange={this.handleChange}/><br/> 
            <input type="text" placeholder="Model" name="model" 
              onChange={this.handleChange}/><br/>
            <input type="text" placeholder="Color" name="color" 
              onChange={this.handleChange}/><br/>
            <input type="text" placeholder="Year" name="year" 
              onChange={this.handleChange}/><br/>
            <input type="text" placeholder="Price" name="price" 
              onChange={this.handleChange}/><br/>
            <button onClick={this.handleSubmit}>Save</button>
            <button onClick={this.cancelSubmit}>Cancel</button>     
          </form> 
        </SkyLight>
        <div>
            <button style={{'margin': '10px'}} 
              onClick={() => this.refs.addDialog.show()}>New car</button>
        </div>
      </div> 
    );
  1. AddCar组件插入Carlist组件中,以查看是否可以打开该表单。打开Carlist.js文件以查看编辑器视图,并导入AddCar组件:
import AddCar from './AddCar.js';
  1. Carlist.js文件中实现addCar函数,该函数将向后端api/cars端点发送POST请求。请求将包括新的car对象在主体内以及'Content-Type': 'application/json'头。需要头部是因为使用JSON.stringify()方法将car对象转换为 JSON 格式:
// Add new car
addCar(car) {
  fetch(SERVER_URL + 'api/cars', 
    { method: 'POST', 
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(car)
    })
  .then(res => this.fetchCars())
  .catch(err => console.error(err))
} 
  1. AddCar组件添加到render()方法中,并将addCarfetchCars函数作为 props 传递给AddCar组件,允许我们从AddCar组件中调用这些函数。现在CarList.js文件的返回语句应该如下所示:
// Carlist.js 
return (
  <div className="App">
    <AddCar addCar={this.addCar} fetchCars={this.fetchCars}/>
    <ReactTable data={this.state.cars} columns={columns} 
      filterable={true} pageSize={10}/>
    <ToastContainer autoClose={1500}/> 
  </div>
);

如果您启动前端应用程序,它现在应该看起来像下面这样,如果您按下“新车”按钮,它应该打开模态表单:

  1. AddCar.js文件中实现handleSubmitcancelSubmit函数。handleSubmit函数创建一个新的car对象并调用addCar函数,该函数可以通过 props 访问。cancelSubmit函数只是关闭模态表单。
// Save car and close modal form
handleSubmit = (event) => {
   event.preventDefault();
   var newCar = {brand: this.state.brand, model: this.state.model, 
     color: this.state.color, year: this.state.year, 
     price: this.state.price};
   this.props.addCar(newCar); 
   this.refs.addDialog.hide(); 
}

// Cancel and close modal form
cancelSubmit = (event) => {
  event.preventDefault(); 
  this.refs.addDialog.hide(); 
}

现在,您可以通过按下“新车”按钮打开模态表单。然后,您可以填写表单数据,并按“保存”按钮。到目前为止,表单看起来不好看,但我们将在下一章中进行样式设置:

列表页面已刷新,并且新车可以在列表中看到:

编辑功能

我们将通过将表格更改为可编辑并向每行添加保存按钮来实现编辑功能。保存按钮将调用向后端发送PUT请求以将更改保存到数据库的函数:

  1. 添加单元格渲染器,将表格单元格更改为可编辑状态。打开Carlist.js文件并创建一个名为renderEditable的新函数。请参阅以下函数的源代码。单元格将是div元素,contentEditable属性使其可编辑。suppressContentEditableWarning抑制了当标记为可编辑的元素具有子元素时出现的警告。当用户离开表格单元格时,将执行onBlur中的函数,并在这里我们将更新状态:
renderEditable = (cellInfo) => {
  return (
    <div
      style={{ backgroundColor: "#fafafa" }}
      contentEditable
      suppressContentEditableWarning
      onBlur={e => {
        const data = [...this.state.cars];
        data[cellInfo.index][cellInfo.column.id] = 
         e.target.innerHTML;
        this.setState({ cars: data });
      }}
      dangerouslySetInnerHTML={{
        __html: this.state.cars[cellInfo.index][cellInfo.column.id]
      }} 
    />
  );
} 
  1. 定义要编辑的表格列。这是使用 React Table 中列的Cell属性完成的,该属性定义了如何呈现列的单元格:
const columns = [{
  Header: 'Brand',
  accessor: 'brand',
  Cell: this.renderEditable
}, {
  Header: 'Model',
  accessor: 'model',
  Cell: this.renderEditable
}, {
  Header: 'Color',
  accessor: 'color',
  Cell: this.renderEditable
}, {
  Header: 'Year',
  accessor: 'year',
  Cell: this.renderEditable
}, {
  Header: 'Price €',
  accessor: 'price',
  Cell: this.renderEditable
}, {
  id: 'delbutton',
  sortable: false,
  filterable: false,
  width: 100,
  accessor: '_links.self.href',
  Cell: ({value}) => (<button onClick={()=>{this.onDelClick(value)}}>Delete</button>)
}]

现在,如果您在浏览器中打开应用程序,您会发现表格单元格是可编辑的:

  1. 要更新汽车数据,我们必须向http://localhost:8080/api/cars/[carid] URL 发送PUT请求。链接与删除功能相同。请求包含更新后的car对象在请求体内,并且我们在添加功能中使用的'Content-Type': 'application/json'标头。创建一个名为updateCar的新函数,函数的源代码显示在以下代码片段中。该函数接收两个参数,更新后的car对象和请求 URL。成功更新后,我们将向用户显示提示消息:
// Update car
updateCar(car, link) {
  fetch(link, 
  { method: 'PUT', 
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(car)
  })
  .then( res =>
    toast.success("Changes saved", {
      position: toast.POSITION.BOTTOM_LEFT
    }) 
  )
  .catch( err => 
    toast.error("Error when saving", {
      position: toast.POSITION.BOTTOM_LEFT
    }) 
  )
}
  1. 将“保存”按钮添加到表格行。当用户按下按钮时,它调用updateCar函数并传递两个参数。第一个参数是row,它是行中所有值的object(=car object)。第二个参数是value,它设置为_links.href.self,这将是我们在请求中需要的汽车的 URL:
const columns = [{
  Header: 'Brand',
  accessor: 'brand',
  Cell: this.renderEditable
}, {
  Header: 'Model',
  accessor: 'model',
  Cell: this.renderEditable
}, {
  Header: 'Color',
  accessor: 'color',
  Cell: this.renderEditable
}, {
  Header: 'Year',
  accessor: 'year',
  Cell: this.renderEditable
}, {
  Header: 'Price €',
  accessor: 'price',
  Cell: this.renderEditable
}, {
  id: 'savebutton',
  sortable: false,
  filterable: false,
  width: 100,
  accessor: '_links.self.href',
  Cell: ({value, row}) => 
    (<button onClick={()=>{this.updateCar(row, value)}}>
     Save</button>)
}, {
  id: 'delbutton',
  sortable: false,
  filterable: false,
  width: 100,
  accessor: '_links.self.href',
  Cell: ({value}) => (<button onClick=
    {()=>{this.onDelClick(value)}}>Delete</button>)
}]

现在,如果您编辑表格中的值并按下“保存”按钮,您应该会看到提示消息,并且更新的值将保存到数据库中:

其他功能

我们还将实现一个功能,即将数据导出为 CSV。有一个名为react-csv的包(github.com/abdennour/react-csv),可用于将数据数组导出到 CSV 文件。

如果您的应用程序已启动,请通过在终端中按Ctrl + C停止开发服务器,并键入以下命令以安装react-csv。安装后,重新启动应用程序:

npm install react-csv --save

react-csv包含两个组件—CSVLinkCSVDownload。我们将在我们的应用程序中使用第一个,因此将以下导入添加到Carlist.js文件中:

import { CSVLink } from 'react-csv';

CSVLink组件接受data属性,其中包含要导出到 CSV 文件的数据数组。您还可以使用separator属性定义数据分隔符(默认分隔符为逗号)。在render()方法的return语句中添加CSVLink组件。data属性的值现在将是this.state.cars

// Carlist.js render() method
return (
  <div className="App">
    <CSVLink data={this.state.cars} separator=";">Export CSV</CSVLink>
    <AddCar addCar={this.addCar} fetchCars={this.fetchCars}/>
    <ReactTable data={this.state.cars} columns={columns} 
       filterable={true} pageSize={10}/>
    <ToastContainer autoClose={6500}/> 
  </div>
);

在浏览器中打开应用程序,您应该在我们的应用程序中看到导出 CSV 链接。样式不好看,但我们将在下一章中处理。如果您点击链接,您将在 CSV 文件中获得数据:

现在所有功能都已实现。

总结

在本章中,我们实现了应用程序的所有功能。我们从后端获取汽车数据,并在 React Table 中显示这些数据,该表提供分页、排序和过滤功能。然后我们实现了删除功能,并使用 toast 组件向用户提供反馈。添加功能是使用 React Skylight 模态表单组件实现的。在编辑功能中,我们利用了 React Table 的可编辑表格功能。最后,我们实现了将数据导出到 CSV 文件的功能。在下一章中,我们将开始使用 Material UI 组件库来完善我们的用户界面。在下一章中,我们将使用 React Material-UI 组件库来设计我们的前端界面。

问题

  1. 如何使用 React 的 REST API 获取和展示数据?

  2. 如何使用 React 的 REST API 删除数据?

  3. 如何使用 React 的 REST API 添加数据?

  4. 如何使用 React 的 REST API 更新数据?

  5. 如何使用 React 显示 toast 消息?

  6. 如何使用 React 将数据导出到 CSV 文件?

进一步阅读

Packt 还有其他很棒的资源可以学习 React:

第十一章:使用 React Material-UI 对前端进行样式设置

本章将解释如何在我们的前端中使用 Material-UI 组件。我们将使用Button组件来显示样式化按钮。模态表单输入字段将被TextField组件替换,该组件具有许多很好的功能。Material-UI 提供了Snackbar组件,可以向最终用户显示提示消息。我们将用Snackbar替换react-toastify组件,以获得统一的外观。最后,我们将使用AppBar组件代替 React 应用程序标题。

在本章中,我们将查看以下内容:

  • 什么是 Material-UI?

  • 如何在我们的前端中使用 Material-UI 组件

  • 如何在 React 应用程序中删除未使用的组件

技术要求

我们在第四章中创建的 Spring Boot 应用程序,Securing and Testing Your Backend,需要与上一章的修改(未经保护的后端)一起使用。

我们还需要在上一章中使用的 React 应用程序(carfront)。

使用 Button 组件

通过在您正在使用的终端中键入以下命令并在安装完成后启动您的应用程序来安装 Material-UI:

npm install @material-ui/core --save

让我们首先将所有按钮更改为使用 Material-UI 的Button组件。将Button导入AddCar.js文件:

// AddCar.js
import Button from '@material-ui/core/Button';

将按钮更改为使用Button组件。在列表页面中,我们使用主按钮,在模态表单中使用轮廓按钮:

  render() {
    return (
      <div>
        <SkyLight hideOnOverlayClicked ref="addDialog">
          <h3>New car</h3>
          <form>
            <input type="text" placeholder="Brand" name="brand" 
            onChange={this.handleChange}/><br/> 
            <input type="text" placeholder="Model" name="model" 
            onChange={this.handleChange}/><br/>
            <input type="text" placeholder="Color" name="color" 
            onChange={this.handleChange}/><br/>
            <input type="text" placeholder="Year" name="year" 
            onChange={this.handleChange}/><br/>
            <input type="text" placeholder="Price" name="price" 
            onChange={this.handleChange}/><br/><br/>
            <Button variant="outlined" color="primary" 
            onClick={this.handleSubmit}>Save</Button> 
            <Button variant="outlined" color="secondary" 
            onClick={this.cancelSubmit}>Cancel</Button> 
          </form> 
        </SkyLight>
        <div>
            <Button variant="raised" color="primary" 
            style={{'margin': '10px'}} 
            onClick={() => this.refs.addDialog.show()}>
            New Car</Button>
        </div>
      </div> 
    );

现在,列表页面按钮应该如下所示:

模态表单按钮应该如下所示:

我们在汽车表中使用了平面变体按钮,并将按钮大小定义为小。请参见以下表列的源代码:

// Carlist.js render() method
const columns = [{
  Header: 'Brand',
  accessor: 'brand',
  Cell: this.renderEditable
}, {
  Header: 'Model',
  accessor: 'model',
  Cell: this.renderEditable
}, {
  Header: 'Color',
  accessor: 'color',
  Cell: this.renderEditable
}, {
  Header: 'Year',
  accessor: 'year',
  Cell: this.renderEditable
}, {
  Header: 'Price €',
  accessor: 'price',
  Cell: this.renderEditable
}, {
  id: 'savebutton',
  sortable: false,
  filterable: false,
  width: 100,
  accessor: '_links.self.href',
  Cell: ({value, row}) => (<Button size="small" variant="flat" color="primary" 
    onClick={()=>{this.updateCar(row, value)}}>Save</Button>)
}, {
  id: 'delbutton',
  sortable: false,
  filterable: false,
  width: 100,
  accessor: '_links.self.href',
  Cell: ({value}) => (<Button size="small" variant="flat" color="secondary" 
    onClick={()=>{this.confirmDelete(value)}}>Delete</Button>)
}]

现在,表格应该如下所示:

使用 Grid 组件

Material-UI 提供了一个Grid组件,可用于为您的 React 应用程序获取网格布局。我们将使用Grid来获取新项目按钮和导出 CSV 链接在同一行上。

将以下导入添加到Carlist.js文件中以导入Grid组件:

import Grid from '@material-ui/core/Grid';

接下来,我们将AddCarCSVLink包装在Grid组件中。Grid组件有两种类型——容器和项目。这两个组件都包装在项目的Grid组件中。然后,两个项目的Grid组件都包装在容器的Grid组件中:

// Carlist.js render() method
return (
  <div className="App">
    <Grid container>
      <Grid item>
        <AddCar addCar={this.addCar} fetchCars={this.fetchCars}/>
      </Grid>
      <Grid item style={{padding: 20}}>
         <CSVLink data={this.state.cars} separator=";">Export CSV</CSVLink>
      </Grid>
    </Grid>

    <ReactTable data={this.state.cars} columns={columns} 
      filterable={true} pageSize={10}/>
    <ToastContainer autoClose={1500}/> 
  </div>
);

现在,您的应用程序应该如下所示,按钮现在放在一行中:

使用 TextField 组件

在这一部分,我们将使用 Material-UI 的TextField组件来更改模态表单中的文本输入。将以下导入语句添加到AddCar.js文件中:

import TextField from '@material-ui/core/TextField';

然后,在表单中将输入更改为TextField组件。我们使用label属性来设置TextField组件的标签:

render() {
  return (
    <div>
      <SkyLight hideOnOverlayClicked ref="addDialog">
        <h3>New car</h3>
        <form>
          <TextField label="Brand" placeholder="Brand" 
            name="brand" onChange={this.handleChange}/><br/> 
          <TextField label="Model" placeholder="Model" 
            name="model" onChange={this.handleChange}/><br/>
          <TextField label="Color" placeholder="Color" 
            name="color" onChange={this.handleChange}/><br/>
          <TextField label="Year" placeholder="Year" 
            name="year" onChange={this.handleChange}/><br/>
          <TextField label="Price" placeholder="Price" 
            name="price" onChange={this.handleChange}/><br/><br/>
          <Button variant="outlined" color="primary" 
            onClick={this.handleSubmit}>Save</Button> 
          <Button variant="outlined" color="secondary" 
            onClick={this.cancelSubmit}>Cancel</Button> 
        </form> 
      </SkyLight>
      <div>
         <Button variant="raised" color="primary" 
            style={{'margin': '10px'}} 
            onClick={() => this.refs.addDialog.show()}>New Car</Button>
      </div>
    </div> 
  );

修改后,模态表单应该如下所示:

使用 AppBar 组件

在这一部分,我们将用AppBar组件替换 React 应用程序标题。导入AppBarToolbar组件:

import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';

App.js文件中删除div标题元素。将AppBar组件添加到render()方法中,并将Toolbar组件放在其中。Toolbar组件包含应用栏中显示的文本:

// App.js
import React, { Component } from 'react';
import './App.css';
import Carlist from './components/Carlist';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';

class App extends Component {
  render() {
    return (
      <div className="App">
        <AppBar position="static" color="default">
          <Toolbar>CarList</ Toolbar>
        </ AppBar>
        <Carlist /> 
      </div>
    );
  }
}

export default App;

现在,您的前端应该如下所示:

使用 SnackBar 组件

我们已经通过使用react-toastify组件实现了提示消息。Material-UI 提供了一个名为SnackBar的组件,可以用于向最终用户显示消息。为了在我们的应用程序中获得统一的外观,让我们使用该组件来显示消息。

我们现在可以从Carlist.js文件中移除react-toastify的导入,也可以通过在你正在使用的终端中输入以下命令来移除组件:

npm remove react-toastify

要开始使用Snackbar组件,请将以下导入添加到Carlist.js文件中:

import Snackbar from '@material-ui/core/Snackbar';

我们需要为Snackbar添加两个新的状态值,一个用于消息,一个用于状态。将这两个状态值添加到构造函数中。状态值称为open,它定义了Snackbar是否可见:

constructor(props) {
  super(props);
  this.state = { cars: [], open: false, message: ''};
}

然后,我们将Snackbar组件添加到render()方法中。autoHideDuration属性定义了在调用onClose之前等待的毫秒数。要显示Snackbar,我们只需要将open状态值设置为true并设置消息:

// Carlist.js render() method's return statement
return (
  <div className="App">
    <Grid container>
      <Grid item>
        <AddCar addCar={this.addCar} fetchCars={this.fetchCars}/>
      </Grid>
      <Grid item style={{padding: 20}}>
        <CSVLink data={this.state.cars} separator=";">Export CSV</CSVLink>
      </Grid>
    </Grid>

    <ReactTable data={this.state.cars} columns={columns} 
      filterable={true} pageSize={10}/>
    <Snackbar 
      style = {{width: 300, color: 'green'}}
      open={this.state.open} onClose={this.handleClose} 
      autoHideDuration={1500} message={this.state.message} />
  </div>
);

接下来,我们必须实现handleClose函数,该函数在onClose事件中调用。该函数只是将open状态值设置为false

handleClose = (event, reason) => {
  this.setState({ open: false });
};

然后,我们用setState()方法替换了 toast 消息,该方法将open值设置为true,并将显示的文本设置为message状态:

// Delete car
onDelClick = (link) => {
  fetch(link, {method: 'DELETE'})
  .then(res => {
    this.setState({open: true, message: 'Car deleted'});
    this.fetchCars();
  })
  .catch(err => {
    this.setState({open: true, message: 'Error when deleting'});
    console.error(err)
  }) 
}

// Update car
updateCar(car, link) {
  fetch(link, 
  { method: 'PUT', 
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(car)
  })
  .then( res =>
    this.setState({open: true, message: 'Changes saved'})
  )
  .catch( err => 
    this.setState({open: true, message: 'Error when saving'})
  )
}

以下是使用Snackbar组件显示消息的屏幕截图:

总结

在本章中,我们使用 Material-UI 完成了我们的前端。Material-UI 是实现了 Google 的 Material Design 的 React 组件库。我们用 Material-UI 的Button组件替换了所有按钮。我们使用 Material-UI 的TextField组件为我们的模态表单赋予了新的外观。我们移除了 React 应用程序标题,改用了AppBar组件。现在,向最终用户显示的消息使用Snackbar组件。经过这些修改,我们的前端看起来更加专业和统一。在下一章中,我们将专注于前端测试。

问题

  1. 什么是 Material-UI?

  2. 你应该如何使用不同的 Material-UI 组件?

  3. 你应该如何移除未使用的组件?

进一步阅读

Packt 还有其他很好的资源可以学习 React:

第十二章:测试您的前端

本章解释了测试 React 应用程序的基础知识。我们将概述使用 Jest,这是 Facebook 开发的 JavaScript 测试库。我们还将介绍 Enzyme,这是由 Airbnb 开发的用于 React 的测试实用程序。我们将看看如何创建新的测试套件和测试。我们还将介绍如何运行测试并发现测试的结果。

在本章中,我们将看以下内容:

  • Jest 的基础知识

  • 如何创建新的测试套件和测试

  • Enzyme 测试实用程序的基础知识

  • 如何安装 Enzyme

  • 如何使用 Enzyme 创建测试

技术要求

我们需要在第四章中创建的 Spring Boot 应用程序,Securing and Testing Your Backend(GitHub:github.com/PacktPublishing/Hands-On-Full-Stack-Development-with-Spring-Boot-2.0-and-React/tree/master/Chapter%204)。

我们还需要在上一章中使用的 React 应用程序(GitHub:github.com/PacktPublishing/Hands-On-Full-Stack-Development-with-Spring-Boot-2.0-and-React/tree/master/Chapter%2011)。

使用 Jest

Jest 是一个用于 JavaScript 的测试库,由 Facebook 开发(facebook.github.io/jest/en/)。Jest 广泛用于 React,并为测试提供了许多有用的功能。您可以创建快照测试,从中可以获取 React 树的快照并调查状态的变化。Jest 还具有模拟功能,您可以使用它来测试例如异步 REST API 调用。Jest 还提供了在测试用例中所需的断言函数。

我们将首先看看如何为基本的 JavaScript 函数创建一个简单的测试用例,该函数执行一些简单的计算。以下函数以两个数字作为参数,并返回数字的乘积:

// multi.js
export const calcMulti = (x, y) => {
    x * y;
}

以下代码显示了前述函数的 Jest 测试。测试用例从运行测试用例的test方法开始。test方法有一个别名,称为it,我们稍后在 React 示例中将使用它。测试方法获取两个必需的参数-测试名称和包含测试的函数。当您想要测试值时,使用expecttoBe是所谓的匹配器,用于检查函数的结果是否等于匹配器中的值。Jest 中有许多不同的匹配器可用,您可以从其文档中找到这些:

// multi.test.js
import {calcMulti} from './multi';

test('2 * 3 equals 6', () => {
  expect(calcMulti(2, 3)).toBe(6);
});

Jest 与create-react-app一起提供,因此我们无需进行任何安装或配置即可开始测试。建议为测试文件创建一个名为_test_的文件夹。测试文件应具有.test.js扩展名。如果您在 VS Code 文件资源管理器中查看 React 前端,您会发现在src文件夹中已经自动创建了一个测试文件,名为App.test.js

测试文件的源代码如下:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

it('renders without crashing', () => {
  const div = document.createElement('div');
  ReactDOM.render(<App />, div);
  ReactDOM.unmountComponentAtNode(div);
});

以下测试文件创建了一个div元素到 DOM 并将App组件挂载到它上。最后,组件从div中卸载。因此,它只是测试您的App组件是否可以渲染并且测试运行程序是否正常工作。it是 Jest 中test函数的别名,第一个参数是测试的名称,第二个参数是要执行和测试的函数。

您可以通过在终端中输入以下命令来运行测试:

npm test

或者如果您使用 Yarn,请输入以下内容:

yarn test

在执行完测试并且一切正常工作后,您将在终端中看到以下信息:

快照测试

快照测试是一个有用的工具,用于测试用户界面中是否存在不需要的更改。当执行快照测试时,Jest 会生成快照文件。下次执行测试时,将新的快照与先前的快照进行比较。如果文件内容之间存在更改,测试用例将失败,并在终端中显示错误消息。

要开始快照测试,请执行以下步骤:

  1. 安装react-test-render包。--save-dev参数意味着此依赖项保存到package.json文件的devDependencies部分,仅用于开发目的。如果在安装阶段键入npm install --production命令,则不会安装devDependencies部分中的依赖项。因此,所有仅在开发阶段需要的依赖项都应使用--save-dev参数进行安装:
npm install react-test-renderer --save-dev
  1. 您的package.json文件应如下所示,并且已将新的devDependecies部分添加到文件中:
{
  "name": "carfront",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@material-ui/core": "¹.0.0",
    "@material-ui/icons": "¹.0.0",
    "material-ui": "⁰.20.1",
    "react": "¹⁶.3.2",
    "react-confirm-alert": "².0.2",
    "react-csv": "¹.0.14",
    "react-dom": "¹⁶.3.2",
    "react-scripts": "1.1.4",
    "react-skylight": "⁰.5.1",
    "react-table": "⁶.8.2"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {
    "react-test-renderer": "¹⁶.3.2"
  }
}
  1. renderer导入到您的测试文件中:
import renderer from 'react-test-renderer';

让我们在App.test.js文件中添加一个新的快照测试用例。该测试用例将创建我们的AddCar组件的快照测试:

  1. AddCar组件导入到我们的测试文件中:
import AddCar from './components/AddCar';
  1. 在文件中已经存在的第一个测试用例之后添加以下测试代码。该测试用例从我们的App组件中获取快照,然后比较快照是否与先前的快照不同:
it('renders a snapshot', () => {
  const tree = renderer.create(<AddCar/>).toJSON();
  expect(tree).toMatchSnapshot();
});
  1. 通过在终端中输入以下命令再次运行测试用例:
npm test
  1. 现在您可以在终端中看到以下消息。测试套件告诉我们测试文件的数量,测试告诉我们测试用例的数量:

当首次执行测试时,将创建一个_snapshots_文件夹。该文件夹包含从测试用例生成的所有快照文件。现在,您可以看到已生成一个快照文件,如下面的截图所示:

快照文件现在包含了我们的AddCar组件的 React 树。您可以从这里的开头看到快照文件的一部分:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders a snapshot 1`] = `
<div>
  <section
    className="skylight-wrapper "
  >
    <div
      className="skylight-overlay"
      onClick={[Function]}
      style={
        Object {
          "backgroundColor": "rgba(0,0,0,0.3)",
          "display": "none",
          "height": "100%",
          "left": "0px",
          "position": "fixed",
          "top": "0px",
          "transitionDuration": "200ms",
          "transitionProperty": "all",
          "transitionTimingFunction": "ease",
          "width": "100%",
          "zIndex": "99",
        }
      }
  />
...continue

使用 Enzyme

Enzyme 是用于测试 React 组件输出的 JavaScript 库,由 Airbnb 开发。Enzyme 具有一个非常好的用于 DOM 操作和遍历的 API。如果您使用过 jQuery,那么很容易理解 Enzyme API 的思想。

要开始使用 Enzyme,请执行以下步骤:

  1. 通过在终端中输入以下命令进行安装。这将安装enzyme库和适配器库,适用于 React 版本 16. 旧版 React 版本也有可用的适配器:
npm install enzyme enzyme-adapter-react-16 --save-dev
  1. src文件夹中创建一个名为AddCar.test.js的新测试文件(测试套件)。现在我们将为我们的AddCar组件创建一个 Enzyme 浅渲染测试。第一个测试用例渲染组件并检查是否有五个TextInput组件,因为应该有五个。wrapper.find找到渲染树中与TextInput匹配的每个节点。在 Enzyme 测试中,我们可以使用 Jest 进行断言,这里我们使用toHaveLength来检查找到的节点数是否等于五。浅渲染测试将组件作为一个单元进行测试,并不会渲染任何子组件。对于这种情况,浅渲染就足够了。否则,您也可以使用mount进行完整的 DOM 渲染:
import React from 'react';
import AddCar from './components/AddCar';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });

describe('<AddCar />', () => {
  it('renders five <TextInput /> components', () => {
    const wrapper = shallow(<AddCar />);
    expect(wrapper.find('TextField')).toHaveLength(5);
  });
});
  1. 现在,如果您运行测试,您可以在终端中看到以下消息。您还可以看到测试套件的数量为两,因为有新的测试文件并且所有测试都通过了:

您还可以使用simulate方法使用 Enzyme 测试事件。以下示例显示了如何测试AddCar组件中TextField品牌的onChange事件。此示例还显示了如何访问组件的状态。我们首先使用wrapper.find查找第一个TextField,用于汽车品牌。然后,我们设置TextField的值,并使用simulate方法模拟更改事件。最后,我们检查品牌状态的值,该值现在应该包含Ford

describe('<AddCar />', () => {
  it('test onChange', () => {
    const wrapper = shallow(<AddCar />);
    const brandInput = wrapper.find('TextField').get(0);
    brandInput.instance().value = 'Ford';
    usernameInput.simulate('change');
    expect(wrapper.state('brand')).toEqual('Ford');
  });
});

摘要

在本章中,我们对如何测试 React 应用程序进行了基本概述。Jest 是 Facebook 开发的一个测试库,因为我们使用create-react-app创建了我们的应用程序,所以它已经可用于我们的前端。我们使用 Jest 创建了一些测试,并运行这些测试,以查看如何检查测试的结果。我们安装了 Enzyme,这是一个用于 React 的测试工具。使用 Enzyme,您可以轻松测试 React 组件的渲染和事件。在下一章中,我们将保护我们的应用程序,并在前端添加登录功能。

问题

  1. Jest 是什么?

  2. 您应该如何使用 Jest 创建测试用例?

  3. 您应该如何使用 Jest 创建快照测试?

  4. Enzyme 是什么?

  5. 您应该如何安装 Enzyme?

  6. 您应该如何使用 Enzyme 进行渲染测试?

  7. 您应该如何使用 Enzyme 测试事件?

进一步阅读

Packt 还有其他关于学习 React 和测试的优质资源。

第十三章:保护您的应用程序

本章解释了在后端使用 JWT 身份验证时如何对前端实施身份验证。首先,我们为后端打开安全性以启用 JWT 身份验证。然后,我们为登录功能创建一个组件。最后,我们修改我们的 CRUD 功能,以在请求的Authorization标头中发送令牌到后端。

在本章中,我们将研究以下内容:

  • 如何在前端创建登录功能

  • 如何在身份验证后实现条件渲染

  • 启用 JWT 身份验证时,CRUD 功能需要什么

  • 如何在身份验证失败时显示消息

技术要求

我们在第四章中创建的 Spring Boot 应用程序,保护和测试您的后端(GitHub:github.com/PacktPublishing/Hands-On-Full-Stack-Development-with-Spring-Boot-2.0-and-React/tree/master/Chapter%204)。

我们在上一章中使用的 React 应用程序(GitHub:github.com/PacktPublishing/Hands-On-Full-Stack-Development-with-Spring-Boot-2.0-and-React/tree/master/Chapter%2011)。

保护后端

我们已经在前端实现了对未受保护的后端的 CRUD 功能。现在,是时候再次为我们的后端打开安全性,并返回到我们在第四章中创建的版本,保护和测试您的后端

  1. 使用 Eclipse IDE 打开后端项目,并在编辑器视图中打开SecurityConfig.java文件。我们将安全性注释掉,并允许每个人访问所有端点。现在,我们可以删除该行,并从原始版本中删除注释。现在,您的SecurityConfig.java文件的configure方法应如下所示:
@Override
  protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable().cors().and().authorizeRequests()
    .antMatchers(HttpMethod.POST, "/login").permitAll()
    .anyRequest().authenticated()
    .and()
    // Filter for the api/login requests
    .addFilterBefore(new LoginFilter("/login", authenticationManager()),
       UsernamePasswordAuthenticationFilter.class)
    // Filter for other requests to check JWT in header
    .addFilterBefore(new AuthenticationFilter(),
       UsernamePasswordAuthenticationFilter.class);
}

让我们测试一下当后端再次被保护时会发生什么。

  1. 通过在 Eclipse 中按下“运行”按钮来运行后端,并从控制台视图中检查应用程序是否正确启动。通过在终端中键入npm start命令来运行前端,浏览器应该打开到地址localhost:3000

  2. 现在您应该看到列表页面和表格为空。如果您打开开发者工具,您会注意到请求以403 Forbidden HTTP 错误结束。这实际上是我们想要的,因为我们尚未对前端进行身份验证:

保护前端

使用 JWT 对后端进行了身份验证。在第四章中,保护和测试您的后端,我们创建了 JWT 身份验证,并且/login端点允许每个人在没有身份验证的情况下访问。在前端的登录页面中,我们必须首先调用/login端点以获取令牌。之后,令牌将包含在我们发送到后端的所有请求中,就像在第四章中演示的那样,保护和测试您的后端

让我们首先创建一个登录组件,要求用户提供凭据以从后端获取令牌:

  1. components文件夹中创建一个名为Login.js的新文件。现在,您的前端文件结构应如下所示:

  1. 在 VS Code 编辑器中打开文件,并将以下基本代码添加到登录组件中。我们还导入SERVER_URL,因为它在登录请求中是必需的:
import React, { Component } from 'react';
import {SERVER_URL} from '../constants.js';

class Login extends Component {
  render() {
    return (
      <div>        
      </div>
    );
  }
}

export default Login;
  1. 我们需要三个用于身份验证的状态值。两个用于凭据(usernamepassword),一个布尔值用于指示身份验证状态。身份验证状态的默认值为false。在constructor中创建constructor并在其中引入状态:
constructor(props) {
  super(props);
  this.state = {username: '', password: '', 
    isAuthenticated: false};
}
  1. 在用户界面中,我们将使用 Material-UI 组件库,就像我们在用户界面的其余部分中所做的那样。我们需要凭据的文本字段组件和一个调用登录函数的按钮。将组件的导入添加到login.js文件中:
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
  1. 将导入的组件添加到用户界面中,方法是将它们添加到render()方法中。我们需要两个TextField组件,一个用于用户名,一个用于密码。需要一个RaisedButton组件来调用我们稍后将实现的login函数:
render() {
  return (
    <div>
      <TextField name="username" placeholder="Username" 
      onChange={this.handleChange} /><br/> 
      <TextField type="password" name="password" 
       placeholder="Password" 
      onChange={this.handleChange} /><br/><br/> 
      <Button variant="raised" color="primary" 
       onClick={this.login}>
        Login
     </Button>
    </div>
  );
}
  1. 实现TextField组件的更改处理程序,以将键入的值保存到状态中:
handleChange = (event) => {
  this.setState({[event.target.name] : event.target.value});
}
  1. 如第四章所示,保护和测试您的后端,登录是通过调用/login端点使用POST方法并在请求体内发送用户对象来完成的。如果身份验证成功,我们将在响应的Authorization标头中获得一个令牌。然后,我们将令牌保存到会话存储中,并将isAuthenticated状态值设置为true。会话存储类似于本地存储,但在页面会话结束时会被清除。当isAuthenticated状态值改变时,用户界面将被重新渲染:
login = () => {
  const user = {username: this.state.username, password: this.state.password};
  fetch(SERVER_URL + 'login', {
    method: 'POST',
    body: JSON.stringify(user)
  })
  .then(res => {
    const jwtToken = res.headers.get('Authorization');
    if (jwtToken !== null) {
      sessionStorage.setItem("jwt", jwtToken);
      this.setState({isAuthenticated: true});
    }
  })
  .catch(err => console.error(err)) 
}
  1. 我们可以实现条件渲染,如果isAuthenticated状态为false,则渲染Login组件,如果isAuthenticated状态为true,则渲染Carlist组件。我们首先必须将Carlist组件导入Login组件中:
import Carlist from './Carlist';

然后对render()方法进行以下更改:

render() {
  if (this.state.isAuthenticated === true) {
    return (<Carlist />)
  }
  else {
    return (
      <div>
        <TextField type="text" name="username" 
         placeholder="Username" 
        onChange={this.handleChange} /><br/> 
        <TextField type="password" name="password" 
         placeholder="Password" 
        onChange={this.handleChange} /><br/><br/> 
        <Button variant="raised" color="primary" 
         onClick={this.login}>
          Login
        </Button>
      </div>
    );
  }
}
  1. 要显示登录表单,我们必须在App.js文件中渲染Login组件而不是Carlist组件:
// App.js
import React, { Component } from 'react';
import './App.css';
import Login from './components/Login';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';

class App extends Component {
  render() {
    return (
      <div className="App">
        <AppBar position="static" color="default">
          <Toolbar>CarList</ Toolbar>
        </ AppBar>
        <Login /> 
      </div>
    );
  }
}

export default App;

现在,当您的前端和后端正在运行时,您的前端应该如下所示:

如果您使用user/useradmin/admin凭据登录,您应该看到汽车列表页面。如果打开开发者工具,您会看到令牌现在保存在会话存储中:

汽车列表仍然是空的,但这是正确的,因为我们还没有将令牌包含在请求中。这对于 JWT 身份验证是必需的,我们将在下一阶段实现:

  1. 在 VS Code 编辑器视图中打开Carlist.js文件。要获取汽车,我们首先必须从会话存储中读取令牌,然后将带有令牌值的Authorization标头添加到请求中。您可以在此处查看获取函数的源代码:
// Carlist.js 
// Fetch all cars
fetchCars = () => {
  // Read the token from the session storage
 // and include it to Authorization header
  const token = sessionStorage.getItem("jwt");
  fetch(SERVER_URL + 'api/cars', 
  {
    headers: {'Authorization': token}
  })
  .then((response) => response.json()) 
  .then((responseData) => { 
    this.setState({ 
      cars: responseData._embedded.cars,
    }); 
  })
  .catch(err => console.error(err)); 
}
  1. 如果您登录到前端,您应该看到汽车列表中填充了来自数据库的汽车:

  1. 从开发者工具中检查请求内容;您会看到它包含带有令牌值的Authorization标头:

所有其他 CRUD 功能需要相同的修改才能正常工作。修改后,删除函数的源代码如下所示:

// Delete car
onDelClick = (link) => {
  const token = sessionStorage.getItem("jwt");
  fetch(link, 
    { 
      method: 'DELETE',
      headers: {'Authorization': token}
    }
  )
  .then(res => {
    this.setState({open: true, message: 'Car deleted'});
    this.fetchCars();
  })
  .catch(err => {
    this.setState({open: true, message: 'Error when deleting'});
    console.error(err)
  }) 
}

修改后,添加函数的源代码如下所示:

// Add new car
addCar(car) {
  const token = sessionStorage.getItem("jwt");
  fetch(SERVER_URL + 'api/cars', 
  { method: 'POST', 
      headers: {
        'Content-Type': 'application/json',
        'Authorization': token
      },
      body: JSON.stringify(car)
  })
  .then(res => this.fetchCars())
  .catch(err => console.error(err))
} 

最后,更新函数的源代码如下所示:

// Update car
updateCar(car, link) {
  const token = sessionStorage.getItem("jwt");
  fetch(link, 
  { method: 'PUT', 
    headers: {
      'Content-Type': 'application/json',
      'Authorization': token
    },
    body: JSON.stringify(car)
  })
  .then( res =>
    this.setState({open: true, message: 'Changes saved'})
  )
  .catch( err => 
    this.setState({open: true, message: 'Error when saving'})
  )
} 

现在,在您登录到应用程序后,所有 CRUD 功能都可以正常工作。

在最后阶段,我们将实现一个错误消息,如果身份验证失败,将向最终用户显示该消息。我们使用 Material-UI 的SnackBar组件来显示消息:

  1. 将以下导入添加到Login.js文件中:
import Snackbar from '@material-ui/core/Snackbar';
  1. 打开 Snackbar 的状态,就像我们在第十章中所做的那样,添加 CRUD 功能
// Login.js  
constructor(props) {
  super(props);
  this.state = {username: '', password: '', 
  isAuthenticated: false, open: false};
}

我们还需要一个状态处理程序来关闭Snackbaropen状态,以在SnackbarautoHideDuration属性中设置的时间后关闭Snackbar

handleClose = (event) => {
  this.setState({ open: false });
}
  1. Snackbar添加到render()方法中:
<Snackbar 
  open={this.state.open} onClose={this.handleClose} 
  autoHideDuration={1500} 
  message='Check your username and password' />
  1. 如果身份验证失败,请将open状态值设置为true
login = () => {
  const user = {username: this.state.username, 
      password: this.state.password};
  fetch('http://localhost:8080/login', {
    method: 'POST',
    body: JSON.stringify(user)
  })
  .then(res => {
    const jwtToken = res.headers.get('Authorization');
    if (jwtToken !== null) {
      sessionStorage.setItem("jwt", jwtToken);
      this.setState({isAuthenticated: true});
    }
    else {
      this.setState({open: true});
    }
  })
  .catch(err => console.error(err)) 
}

如果您现在使用错误的凭据登录,您可以看到提示消息:

注销功能要实现起来简单得多。您基本上只需从会话存储中删除令牌,并将isAuthenticated状态值更改为false,如下面的源代码所示:

logout = () => {
    sessionStorage.removeItem("jwt");
    this.setState({isAuthenticated: false});
}

然后通过条件渲染,您可以渲染Login组件而不是Carlist

如果要使用 React Router 实现菜单,可以实现所谓的安全路由,只有在用户经过身份验证时才能访问。以下源代码显示了安全路由,如果用户经过身份验证,则显示路由组件,否则将重定向到登录页面:

const SecuredRoute = ({ component: Component, ...rest, isAuthenticated }) => (
  <Route {...rest} render={props => (
    isAuthenticated ? (
      <Component {...props}/>
    ) : (
      <Redirect to={{
        pathname: '/login',
        state: { from: props.location }
      }}/>
    )
  )}/>
)

这是使用在前面示例中定义的SecuredRouteSwitch路由的示例。LoginContact组件可以在没有身份验证的情况下访问,但Shop需要身份验证:

 <Switch>
    <Route path="/login" component={Login} />
    <Route path="/contact" component={Contact} />
    <SecuredRoute isAuthenticated={this.state.isAuthenticated} 
      path="/shop" component={Shop} />
    <Route render={() => <h1>Page not found</h1>} />
  </Switch>

摘要

在本章中,我们学习了如何在使用 JWT 身份验证时为我们的前端实现登录功能。成功身份验证后,我们使用会话存储保存从后端收到的令牌。然后在发送到后端的所有请求中使用该令牌,因此,我们必须修改我们的 CRUD 功能以正确使用身份验证。在下一章中,我们将部署我们的应用程序到 Heroku,并演示如何创建 Docker 容器。

问题

  1. 您应该如何创建登录表单?

  2. 您应该如何使用 JWT 登录到后端?

  3. 您应该如何将令牌存储到会话存储中?

  4. 您应该如何在 CRUD 功能中将令牌发送到后端?

进一步阅读

Packt 还有其他很好的资源可供学习 React:

第十四章:部署您的应用程序

本章将解释如何将后端和前端部署到服务器。有各种云服务器或 PaaS(平台即服务)提供商可用,如 Amazon(AWS)、DigitalOcean 和 Microsoft Azure。在本书中,我们使用 Heroku,它支持 Web 开发中使用的多种编程语言。我们还将向您展示如何在部署中使用 Docker 容器。

在这一章中,我们将看到以下内容:

  • 部署 Spring Boot 应用程序的不同选项

  • 如何将 Spring Boot 应用程序部署到 Heroku

  • 如何将 React 应用程序部署到 Heroku

  • 如何创建 Spring Boot 和 MariaDB Docker 容器

技术要求

我们在第四章中创建的 Spring Boot 应用程序,Securing and Testing Your Backend,是必需的(GitHub:github.com/PacktPublishing/Hands-On-Full-Stack-Development-with-Spring-Boot-2.0-and-React/tree/master/Chapter%204)。

我们在上一章中使用的 React 应用程序也是必需的(GitHub:github.com/PacktPublishing/Hands-On-Full-Stack-Development-with-Spring-Boot-2.0-and-React/tree/master/Chapter%2011)。

Docker 安装是必要的。

部署后端

如果您要使用自己的服务器,部署 Spring Boot 应用程序的最简单方法是使用可执行的 JAR 文件。如果您使用 Maven,可以在命令行中键入mvn clean install命令来生成可执行的 JAR 文件。该命令会在build文件夹中创建 JAR 文件。在这种情况下,您不必安装单独的应用程序服务器,因为它嵌入在 JAR 文件中。然后,您只需使用java命令运行 JAR 文件,java -jar your_appfile.jar。嵌入式 Tomcat 版本可以在pom.xml文件中使用以下行进行定义:

<properties>
  <tomcat.version>8.0.52</tomcat.version>
</properties>

如果您使用单独的应用程序服务器,您必须创建一个 WAR 包。这有点复杂,您必须对应用程序进行一些修改。以下是创建 WAR 文件的步骤:

  1. 通过扩展SpringBootServletIntializer并重写configure方法修改应用程序主类:
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure
        (SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(Application.class, args);
    }
}
  1. pom.xml文件中将打包从 JAR 更改为 WAR:
<packaging>war</packaging>
  1. 将以下依赖项添加到pom.xml文件中。然后,Tomcat 应用程序将不再是嵌入式的:
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-tomcat</artifactId>
  <scope>provided</scope>
</dependency>

现在,当您构建应用程序时,将生成 WAR 文件。它可以通过将文件复制到 Tomcat 的/webapps文件夹来部署到现有的 Tomcat。

现在,云服务器是向最终用户提供应用程序的主要方式。接下来,我们将把后端部署到 Heroku 云服务器(www.heroku.com/)。Heroku 提供免费账户,您可以用来部署自己的应用程序。使用免费账户,应用程序在 30 分钟不活动后会休眠,并且重新启动应用程序需要一点时间。但是免费账户足够用于测试和爱好目的。

对于部署,您可以使用 Heroku 的基于 Web 的用户界面。以下步骤介绍了部署过程:

  1. 在你创建了 Heroku 账户之后,登录 Heroku 网站。导航到显示应用程序列表的仪表板。有一个名为“New”的按钮,打开一个菜单。从菜单中选择“Create new app”:

  1. 为您的应用命名,选择一个区域,并按“Create app”按钮:

  1. 选择部署方法。有几种选项;我们使用 GitHub 选项。在该方法中,您首先必须将应用程序推送到 GitHub,然后将 GitHub 存储库链接到 Heroku:

  1. 搜索要部署到的存储库,然后按“连接”按钮:

  1. 选择自动部署和手动部署之间。自动选项在您将新版本推送到连接的 GitHub 存储库时自动部署您的应用程序。您还必须选择要部署的分支。我们现在将使用手动选项,在您按下“部署分支”按钮时部署应用程序:

  1. 部署开始,您可以看到构建日志。您应该看到一条消息,说您的应用程序已成功部署:

现在,您的应用程序已部署到 Heroku 云服务器。如果您使用 H2 内存数据库,这就足够了,您的应用程序应该可以工作。我们正在使用 MariaDB;因此,我们必须安装数据库。

在 Heroku 中,我们可以使用 JawsDB,它作为附加组件在 Heroku 中可用。JawsDB 是一个Database as a Service (DBaaS)提供商,提供 MariaDB 数据库,可以在 Heroku 中使用。以下步骤描述了如何开始使用数据库:

  1. 在 Heroku 应用程序页面的资源选项卡中键入JawsDB到附加组件搜索字段中:

  1. 从下拉列表中选择 JawsDB Maria。您可以在附加组件列表中看到 JawsDB。点击 JawsDB,您可以看到数据库的连接信息:

  1. application.properties文件中更改数据库连接定义,使用 JawsDB 连接信息页面上的值。在这个例子中,我们使用明文密码,但建议使用例如Java Simplified Encryption (JASYPT)库来加密密码:
spring.datasource.url=jdbc:mariadb://n7qmaptgs6baip9z.chr7pe7iynqr.eu-west-1.rds.amazonaws.com:3306/ebp6gq2544v5gcpc
spring.datasource.username=bdcpogfxxxxxxx
spring.datasource.password=ke68n28xxxxxxx
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
  1. 使用免费帐户,我们可以最多同时有 10 个连接到我们的数据库;因此,我们还必须将以下行添加到application.properties文件中:
spring.datasource.max-active=10
  1. 将更改推送到 GitHub 并在 Heroku 中部署您的应用程序。现在,您的应用程序已准备就绪,我们可以用 Postman 进行测试。应用程序的 URL 是https://carbackend.herokuapp.com/,但您也可以使用您自己的域名。如果我们向/login端点发送POST请求并附带凭据,我们可以在响应头中获得令牌。所以,一切似乎都正常工作:

您还可以使用 HeidiSQL 连接到 JawsDB 数据库,我们可以看到我们的 car 数据库已经创建:

您可以通过从“更多”菜单中选择“查看日志”来查看应用程序日志:

应用程序日志视图如下所示。

部署前端

在本节中,我们将把 React 前端部署到 Heroku。将 React 应用程序部署到 Heroku 的最简单方法是使用 Heroku Buildpack for create-react-app (github.com/mars/create-react-app-buildpack)。为了部署,我们必须安装 Heroku CLI,这是 Heroku 的命令行工具。您可以从https://devcenter.heroku.com/articles/heroku-cli下载安装包。安装完成后,您可以从 PowerShell 或您正在使用的终端使用 Heroku CLI。以下步骤描述了部署过程:

  1. 使用 VS Code 打开您的前端项目,并在编辑器中打开constant.js文件。将SERVER_URL常量更改为匹配我们后端的 URL,并保存更改:
export const SERVER_URL = 'https://carbackend.herokuapp.com/'
  1. 为您的项目创建一个本地 Git 存储库并提交文件,如果您还没有这样做。使用 Git 命令行工具导航到您的项目文件夹,并键入以下命令:
git init
git add .
git commit -m "Heroku deployment"
  1. 以下命令创建一个新的 Heroku 应用程序,并要求输入 Heroku 的凭据。将[APPNAME]替换为您自己的应用程序名称。命令执行后,您应该在 Heroku 仪表板中看到新的应用程序:
heroku create [APPNAME] --buildpack https://github.com/mars/create-react-app-buildpack.git
  1. 通过在 PowerShell 中输入以下命令将您的代码部署到 Heroku:
git push heroku master

部署准备就绪后,您应该在 PowerShell 中看到“验证部署...完成”消息,如下面的屏幕截图所示:

现在,您可以转到 Heroku 仪表板并查看前端的 URL;您还可以通过在 Heroku CLI 中输入heroku open命令来打开它。如果导航到前端,您应该看到登录表单:

使用 Docker 容器

Docker 是一个容器平台,使软件开发、部署和交付更加简单。容器是轻量级和可执行的软件包,包括运行软件所需的一切。在本节中,我们正在从 Spring Boot 后端创建一个容器,如下所示:

  1. 将 Docker 安装到您的工作站。您可以在www.docker.com/get-docker找到安装包。有多个平台的安装包,如果您使用 Windows 操作系统,可以使用默认设置通过安装向导进行安装。

  2. Spring Boot 应用程序只是一个可执行的 JAR 文件,可以使用 Java 执行。可以使用以下 Maven 命令创建 JAR 文件:

mvn clean install

您还可以使用 Eclipse 通过打开“Run | Run configurations...”菜单来运行 Maven 目标。在“Base directory”字段中选择您的项目,使用“Workspace”按钮。在“Goals”字段中输入 clean install 并按“Run”按钮:

  1. 构建完成后,您可以从/target文件夹中找到可执行的 JAR 文件:

  1. 您可以通过以下命令运行 JAR 文件来测试构建是否正确:
 java -jar .\cardatabase-0.0.1-SNAPSHOT.jar
  1. 您将看到应用程序的启动消息,最后,您的应用程序正在运行:

容器是通过使用 Dockerfile 定义的。

  1. 在项目的根文件夹中创建一个名为Dockerfile的新 Dockerfile。以下行显示了 Dockerfile 的内容。我们使用 Alpine Linux。EXPOSE定义应在容器外发布的端口。COPY将 JAR 文件复制到容器的文件系统并将其重命名为app.jarENTRYPOINT定义 Docker 容器运行的命令行参数。

还有一个 Maven 插件可用于构建 Docker 镜像。它由 Spotify 开发,可以在github.com/spotify/docker-maven-plugin找到。

以下行显示了Dockerfile的内容。

FROM openjdk:8-jdk-alpine
VOLUME /tmp
EXPOSE 8080
ARG JAR_FILE
COPY target/cardatabase-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
  1. 使用以下命令创建容器。使用-t参数,我们可以为容器指定一个友好的名称:
docker build -t carbackend .

在构建命令结束时,您应该看到“成功构建”消息:

  1. 使用docker image ls命令检查容器列表:

  1. 使用以下命令运行容器:
docker run -p 4000:8080 carbackend

Spring Boot 应用程序启动,但以错误结束,因为我们正在尝试访问本地主机数据库。现在本地主机指向容器本身,并且没有安装 MariaDB。

  1. 我们将为 MariaDB 创建自己的容器。您可以使用以下命令从 Docker Hub 拉取最新的 MariaDB 容器:
docker pull mariadb:lates
  1. 运行 MariaDB 容器。以下命令设置 root 用户密码并创建一个新的名为cardb的数据库,这是我们 Spring Boot 应用程序所需的:
docker run --name cardb -e MYSQL_ROOT_PASSWORD=pwd -e MYSQL_DATABASE=cardb mariadb
  1. 我们必须对 Spring Boot 的application.properties文件进行一些更改。将datasource的 URL 更改为以下内容。在下一步中,我们将指定我们的应用可以使用mariadb名称访问数据库容器。更改后,您必须构建您的应用程序并重新创建 Spring Boot 容器:
spring.datasource.url=jdbc:mariadb://mariadb:3306/cardb
  1. 我们可以运行我们的 Spring Boot 容器,并使用以下命令将 MariaDB 容器链接到它。该命令现在定义了我们的 Spring Boot 容器可以使用mariadb名称访问 MariaDB 容器:
docker run -p 8080:8080 --name carapp --link cardb:mariadb -d carbackend
  1. 我们还可以通过输入docker logs carapp命令来访问我们的应用程序日志。我们可以看到我们的应用程序已成功启动,并且演示数据已插入到存在于 MariaDB 容器中的数据库中:

摘要

在本章中,我们学习了如何部署 Spring Boot 应用程序。我们了解了 Spring Boot 应用程序的不同部署选项,并将应用程序部署到了 Heroku。接下来,我们使用 Heroku Buildpack for create-react-app 将 React 前端部署到了 Heroku,这使得部署过程更快。最后,我们使用 Docker 从 Spring Boot 应用程序和 MariaDB 数据库创建了容器。在下一章中,我们将介绍一些您应该探索的更多技术和最佳实践。

问题

  1. 你应该如何创建一个 Spring Boot 可执行的 JAR 文件?

  2. 你应该如何将 Spring Boot 应用部署到 Heroku?

  3. 你应该如何将 React 应用部署到 Heroku?

  4. 什么是 Docker?

  5. 你应该如何创建 Spring Boot 应用容器?

  6. 你应该如何创建 MariaDB 容器?

进一步阅读

Packt 还有其他很好的资源,可以学习关于 React,Spring Boot 和 Docker 的知识:

第十五章:最佳实践

本章介绍了一些你应该了解的要点,如果你想成为一名全栈开发者,或者如果你想在软件开发职业中进一步发展。我们还将介绍一些在软件开发领域工作时要牢记的最佳实践。

在本章中,我们将看到以下内容:

  • 你应该了解哪些技术

  • 对你来说重要的最佳实践是什么

接下来学习什么

要成为一名全栈开发者,你必须能够同时处理后端和前端。这听起来是一个相当具有挑战性的任务,但如果你专注于正确的事情,不试图掌握一切,是可能的。现在,可用的技术栈非常庞大,你可能经常想知道接下来应该学习什么。有多种因素可能会给你一些关于下一步去哪里的提示。一种方法是浏览工作机会,看看公司正在寻找哪些技术。

开始学习新技术有多种方式,没有正确的路径。编程网络课程的使用是一个非常流行的起点,它为你提供了开始学习过程的基本知识。这个过程是永无止境的,因为技术一直在发展和变化。

如果你想成为一名全栈开发者,必须了解以下技术。这不是完整的列表,但是一个很好的起点。

HTML

HTML 是 Web 开发中最基本的东西,你应该学习它。你不必掌握 HTML 的所有细节,但你应该对它有一个良好的基本了解。HTML 5 引入了许多值得学习的新功能。

CSS

CSS 也是一个非常基本的东西要学习。好处是有很多关于 HTML 和 CSS 的好教程可用。学习一些 CSS 库的使用也是一个好主意,比如广泛使用的 Bootstrap。CSS 预处理器,比如 SASS 和 LESS,也值得学习。

HTTP

在开发 Web 应用和 RESTful Web 服务时,HTTP 协议是关键部分。你必须了解 HTTP 的基础知识,知道它的限制。你还应该知道有哪些方法存在,以及如何在不同的编程语言中使用它们。

JavaScript

JavaScript 绝对是你应该掌握的编程语言。没有 JavaScript 技能,要与现代前端开发一起工作是非常困难的。学习 ES6 也是很好的,因为它使 JavaScript 编码更清洁、更高效。

后端编程语言

不了解一些编程语言是很难生存的。如果 JavaScript 用于前端开发,它也可以在后端与 Node.js 一起使用。这就是 Node.js 的好处;你可以在前端和后端使用一种编程语言。其他流行的后端开发语言包括 Java、C#、Python 和 PHP。所有这些语言也有很好的后端框架可以使用。

一些前端库或框架

在这本书中,我们在前端使用了 React.js,这是目前一个流行的选择,但还有许多其他也是不错的选择,比如 Angular 和 Vue.js。

数据库

你还应该知道如何使用后端编程语言与数据库。数据库可以是 SQL 或 NoSQL 数据库,了解这两种选项是很好的。你还应该知道如何优化你正在使用的数据库以及你正在执行的查询的性能。

版本控制

版本控制是你无法离开的东西。现在,Git 是一个非常流行的版本控制系统,了解如何使用它非常重要。还有仓库管理服务,比如 GitHub 和 GitLab,也是值得了解的。

有用的工具

还有许多不同的工具可以帮助你使开发过程更加高效。在这里,我们只提到了一些可能对你有用的工具。Gulp.js 是一个开源的 JavaScript 工具包,用于自动化开发过程中的任务。Grunt 类似于 JavaScript 任务运行器,可以用来自动化你的过程。Webpack 是一个 JavaScript 模块打包工具,可以从你的依赖项创建静态资源。create-react-app,在之前的章节中使用过,实际上在内部使用了 Webpack。

安全

你必须了解网络安全的基础知识,以及如何在 Web 开发中处理这些问题。开始学习的一个好方法是阅读OWASP 十大最严重的 Web 应用安全风险www.owasp.org/index.php/Category:OWASP_Top_Ten_Project)。然后你必须学习如何使用你正在使用的框架来处理这些问题。

最佳实践

软件开发总是团队合作,因此团队中的每个人都使用共同的最佳实践非常重要。在这里,我们将介绍一些你必须考虑的基本事项。这不是完整的清单,但我们试图集中在一些你应该知道的基本事项上。

编码规范

编码规范是描述代码应该如何在特定编程语言中编写的指南。它使代码更易读和更易维护。命名规范定义了变量、方法等应该如何命名。命名非常重要,因为它帮助开发人员理解程序中某个单元的目的。布局规范定义了源代码的结构应该如何看起来,例如缩进和空格的使用。注释规范定义了源代码应该如何被注释。通常最好使用一些标准化的注释方式,比如 Java 中的 Javadoc。

大多数软件开发环境和编辑器都提供了帮助你遵循代码规范的工具。你也可以使用代码格式化工具,比如用于 JavaScript 的 prettier。

选择合适的工具

你应该始终选择最适合你软件开发过程的工具。这样可以使你的过程更加高效,也有助于你的开发生命周期。有许多工具可以自动化开发过程中的任务,这是避免重复任务中出现错误的好方法。当然,你使用的工具将取决于过程和你正在使用的技术。

选择合适的技术

在开始开发应用程序时,要决定的第一件事之一是选择哪些技术(编程语言、框架、数据库等)应该使用。通常情况下,选择你一直使用的技术会让人感到安全,但这并不总是最佳选择。应用程序本身通常对你可以使用的技术做出一些限制。例如,如果你必须创建一个移动应用程序,有几种技术可以使用。但如果你必须开发一个你已经做过很多次的类似应用程序,使用你已经很熟悉的技术可能更明智。

尽量减少编码量

一个常见的好习惯是尽量减少编码量。这是非常明智的,因为这样可以使代码的维护和测试变得更容易。DRY不要重复自己)是软件开发中的一个常见原则。DRY 的基本思想是通过避免代码中的重复来减少代码量。将源代码分割成较小的组件总是一个很好的做法,因为较小的单元总是更容易管理。当然,最佳的结构取决于你使用的编程语言。还有一个好的声明是保持简单,愚蠢KISS),这应该指导你朝着正确的方向前进。

总结

在本章中,我们介绍了如果你想成为全栈开发人员,你应该熟悉的技术。你应该掌握的知识量听起来很多,但你不必成为我们描述的所有技术的大师。了解一些软件开发的最佳实践也是很好的,因为这样你就可以避免常见的错误,你的源代码将更易读和更易于维护。

问题

  1. 编码规范为什么重要?

  2. 为什么你应该尽量避免过多编码?

  3. 命名规范为什么重要?

进一步阅读

Packt 还有其他很好的资源可以学习全栈开发:

第十六章:评估

第一章

答案 1:Spring Boot 是基于 Java 的 Web 应用程序框架,基于 Spring。使用 Spring Boot,您可以开发独立的 Web 应用程序,带有嵌入式应用服务器。

答案 2:Eclipse 是开源的集成开发环境IDE),主要用于 Java 编程,但也支持多种其他编程语言。

答案 3:Maven 是开源软件项目管理工具。Maven 可以管理软件开发项目中的构建、文档、测试等。

答案 4:开始一个新的 Spring Boot 项目最简单的方法是在 Spring Initializr 网页上创建它。这将为您的项目创建一个包含所需模块的框架。

答案 5:如果您使用 Eclipse IDE,只需激活主类并点击运行按钮。您也可以使用 Maven 命令mvn spring-boot:run来运行应用程序。

答案 6:Spring Boot starter 包为您提供日志记录功能。您可以在application.properties设置文件中定义日志记录级别。

答案 7:在运行应用程序后,您可以在 Eclipse IDE 控制台中看到错误和日志消息。

第二章

答案 1:ORM 是一种技术,允许您使用面向对象的编程范式从数据库中提取和操作数据。JPA 为 Java 开发人员提供了对象关系映射。Hibernate 是基于 Java 的 JPA 实现。

答案 2:实体类只是一个标准的带有@Entity注解的 Java 类。在类内部,您必须实现构造函数、字段、getter 和 setter。将作为唯一 id 的字段用@Id注解标注。

答案 3:您必须创建一个新的接口,该接口扩展 Spring Data 的CrudRepository接口。在类型参数中,您定义实体和id字段的类型,例如,<Car, Long>

答案 4:CrudRepository为您的实体提供了所有 CRUD 操作。您可以使用CrudRepository创建、读取、更新和删除实体。

答案 5:您必须创建实体类并使用@OneToMany@ManyToOne注解链接实体。

答案 6:您可以在主应用程序类中使用CommandLineRunner添加演示数据。

答案 7:在您的application.properties文件中定义 H2 控制台的端点并启用它。然后您可以通过浏览器访问定义的端点来访问 H2 控制台。

答案 8:您必须在pom.xml文件中添加 MariaDB 依赖,并在application.properties文件中定义数据库连接设置。如果您使用了 H2 数据库依赖,还需要从pom.xml文件中删除它。

第三章

答案 1:REST 是一种用于创建 Web 服务的架构风格,并定义了一组约束。

答案 2:使用 Spring Data REST starter 包是使用 Spring Boot 创建 RESTful web 服务的最简单方法。默认情况下,Spring Data REST 会找到所有公共存储库,并为您的实体自动创建 RESTful Web 服务。

答案 3:通过向实体的端点发送GET请求。例如,如果您有一个名为Car的实体类,Spring Data REST 将创建一个名为/cars的端点,用于获取所有汽车。

答案 4:通过向单个实体项目的端点发送DELETE请求。例如,/cars/1删除 id 为1的汽车。

答案 5:通过向实体的端点发送POST请求。标头必须包含Content-Type字段,值为application/json,新项目将嵌入在请求体中。

答案 6:通过向实体的端点发送PATCH请求。标头必须包含Content-Type字段,值为application/json,更新的项目将嵌入在请求体中。

答案 7:您必须使用@RepositoryRestResource注解注释您的存储库。使用@Param注解注释查询参数。

第四章

答案 1:Spring Security 为基于 Java 的 Web 应用程序提供安全服务。

答案 2:您必须将 Spring Security 启动包依赖添加到您的pom.xml文件中。您可以通过创建安全配置类来配置 Spring Security。

答案 3JWTJSON Web Token)是在现代 Web 应用程序中实现身份验证的一种紧凑方式。令牌的大小很小,因此可以在 URL 中、POST参数中或标头中发送。

答案 4:您可以使用 Java JWT 库,这是 Java 的 JSON Web Token 库。认证服务类添加和读取令牌。过滤器类处理登录和认证过程。

答案 5:您必须将 Spring Boot 测试启动器包添加到您的pom.xml文件中。Spring Boot 测试启动器包提供了许多很好的测试工具,例如 JUnit,AssertJ 和 Mockito。使用 JUnit 时,基本测试类使用@SpringBootTest注解,并且测试方法应该以@Test注解开头。

答案 6:可以通过在 Eclipse IDE 中运行测试类(Run | JUnit test)轻松执行测试用例。测试结果可以在 JUnit 选项卡中看到。

第五章

答案 1:Node.js 是一个基于 JavaScript 的开源服务器端环境。Npm 是 JavaScript 的包管理器。

答案 2:您可以从nodejs.org/en/download找到多个操作系统的安装包和说明。

答案 3Visual Studio CodeVSCode)是一个面向多种编程语言的开源代码编辑器。

答案 4:您可以从code.visualstudio.com找到多个操作系统的安装包和说明。

答案 5:您必须使用 npm 全局安装create-react-app。然后使用以下命令创建一个应用程序create-react-app projectname

答案 6:您可以使用以下命令运行应用程序npm startyarn start

答案 7:您可以通过修改App.js文件开始,当您保存修改时,可以立即在 Web 浏览器中看到更改。

第六章

答案 1:组件是 React 应用程序的基本构建块。React 组件可以使用 JavaScript 函数或 ES6 类创建。

答案 2:props 和 state 是呈现组件的输入数据。它们是 JavaScript 对象,当 props 或 state 发生变化时,组件会重新呈现。

答案 3:数据流从父组件到子组件。

答案 4:只有 props 的组件称为无状态组件。既有 props 又有状态的组件称为有状态组件。

答案 5:JSX 是 JavaScript 的语法扩展,建议与 React 一起使用。

答案 6:组件生命周期方法在组件生命周期的特定阶段执行。

答案 7:处理 DOM 元素事件类似。React 中的区别在于事件命名使用驼峰命名约定,例如onClickonSubmit

答案 8:通常情况下,我们希望在表单提交后调用具有对表单数据的访问权限的 JavaScript 函数。因此,我们必须使用preventDefault()函数禁用默认行为。您可以使用输入字段的onChange事件处理程序将输入字段的值保存到状态中。

第七章

答案 1:Promise 是表示异步操作结果的对象。使用 Promise 在进行异步调用时简化了代码。

答案 2:Fetch API 提供了fetch()方法,您可以使用它来使用 JavaScript 进行异步调用。

答案 3:REST API 的fetch()调用建议在componentDidMount()生命周期方法中执行,该方法在组件挂载后被调用。

答案 4:您可以使用fetch()方法的 promises 访问响应数据。响应中的数据保存到状态中,当状态改变时组件重新渲染。

第八章

答案 1:您可以从多个来源找到 React 组件,例如,js.coach/github.com/brillout/awesome-react-components

答案 2:您可以使用 npm 或 yarn 软件包管理器安装 React 组件。使用 npm 时,我们使用以下命令npm install <componentname>

答案 3:您必须安装 React Table 组件。安装后,您可以在render()方法中使用ReactTable组件。您必须使用ReactTable的 props 定义数据和列。数据可以是对象或数组。

答案 4:在 React 应用中创建模态表单的一种方法是使用 React Skylight 组件(marcio.github.io/react-skylight/)。

答案 5:您必须使用以下命令npm install @material-ui/core安装 Material-UI 组件库。安装库后,您可以开始使用组件。不同组件的文档可以在material-ui.com找到。

答案 6:可以使用 React Router 组件(github.com/ReactTraining/react-router)来实现路由。

第九章

答案 1:通过模拟,与客户讨论需求要比在开始编写任何实际代码之前更容易。与真实前端源代码相比,对模拟的修改非常容易和快速。

答案 2:有很多适合的应用程序可以轻松进行模拟。您也可以使用纸和铅笔来创建模拟。

答案 3:您可以修改安全配置类以允许在没有身份验证的情况下访问所有端点。

第十章

答案 1:首先,您必须使用fetch()方法调用 REST API。然后,您可以使用fetch()方法的 promises 访问响应数据。响应中的数据保存到状态中,当状态改变时组件重新渲染。

答案 2:您必须使用fetch()方法发送DELETE方法请求。调用的端点是您想要删除的项目的链接。

答案 3:您必须使用fetch()方法发送POST方法请求到实体端点。添加的项目应嵌入在请求体中,并且您必须使用Content-Type头和application/json值。

答案 4:您必须使用fetch()方法发送PATCH方法请求。调用的端点是您想要更新的项目的链接。更新的项目应嵌入在请求体中,并且您必须使用Content-Type头和application/json值。

答案 5:您可以使用一些第三方 React 组件来显示类似 React Toastify 的提示消息。

答案 6:您可以使用一些第三方 React 组件将数据导出到 CSV 文件,例如 React CSV。

第十一章

答案 1:Material-UI 是用于 React 的组件库,它实现了 Google 的 Material Design。

答案 2:首先,您必须使用以下命令安装 Material-UI 库npm install @material-ui/core。然后,您可以开始使用库中的组件。不同组件的文档可以在material-ui.com/找到。

答案 3:您可以使用以下 npm 命令删除组件npm remove <componentname>

第十二章

答案 1:Jest 是 Facebook 开发的 JavaScript 测试库。

答案 2:使用.test.js扩展名创建一个测试文件。在文件中实现您的测试用例,您可以使用以下命令运行测试npm test

答案 3:对于快照测试,您必须安装react-test-render包,并将renderer导入到您的测试文件中。在文件中实现您的快照测试用例,您可以使用以下命令运行测试npm test

答案 4:Enzyme 是一个用于测试 React 组件输出的 JavaScript 库。

答案 5:使用以下 npm 命令 npm install enzyme enzyme-adapter-react-16 --save-dev

答案 6:您必须将EnzymeAdapter组件导入到您的测试文件中。然后,您可以创建测试用例来呈现一个组件。使用 Enzyme,您可以使用 Jest 进行断言。

答案 7:Enzyme 提供了simulate方法,可用于测试事件。

第十三章

答案 1:您必须创建一个新的组件,用于呈现用户名和密码的输入字段。该组件还包含一个按钮,当按钮被按下时调用/login端点。

答案 2:登录组件的调用是使用POST方法进行的,并且用户对象嵌入在主体中。如果身份验证成功,后端将在授权标头中发送令牌回来。

答案 3:可以使用sessionStorage.setItem()方法将令牌保存到会话存储中。

答案 4:令牌必须包含在请求的Authorization标头中。

第十四章

答案 1:您可以使用以下 Maven 命令创建可执行的 JAR 文件 mvn clean install

答案 2:部署 Spring Boot 应用程序的最简单方法是将应用程序源代码推送到 GitHub,并从 Heroku 使用 GitHub 链接。

答案 3:将 React 应用程序部署到 Heroku 的最简单方法是使用 Heroku Buildpack for create-react-app (github.com/mars/create-react-app-buildpack)。

答案 4:Docker 是一个容器平台,使软件开发、部署和交付更容易。容器是轻量级和可执行的软件包,包括运行软件所需的一切。

答案 5:Spring Boot 应用程序只是一个可执行的 JAR 文件,可以使用 Java 执行。因此,您可以为 Spring Boot 应用程序创建 Docker 容器,方式与为任何 Java JAR 应用程序创建方式相似。

答案 6:您可以使用以下 Docker 命令从 Docker Hub 拉取最新的 MariaDB 容器docker pull mariadb:latest

第十五章

答案 1:这使得代码更易读和更易于维护。它还使团队合作更容易,因为每个人在编码中都使用相同的结构。

答案 2:这使得代码更易读和更易于维护。代码的测试更容易。

答案 3:这使得代码更易读和更易于维护。它还使团队合作更容易,因为每个人在编码中都使用相同的命名约定。

posted @ 2024-05-24 10:55  绝不原创的飞龙  阅读(35)  评论(0编辑  收藏  举报