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

51 阅读46分钟

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

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第十章:添加 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 的知识: