vlambda博客
学习文章列表

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》添加CRUD功能

Chapter 10. Adding CRUD Functionalities

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

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

  • How to fetch data from the backend and present it in the frontend
  • How to delete, add, and update data using the REST API
  • How to show toast messages to the user
  • How to export data to the CSV file from the React app

Technical requirements


我们在第 4 章中创建的 Spring Boot 应用程序,保护和测试您的后端< /em> 需要与上一章的修改(不安全的后端)。

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

Creating the list page

在第一阶段,我们将创建列表 page 来显示汽车with 分页、过滤和排序功能。运行您的 Spring Boot 后端,可以通过向 http://localhost:8080/api/cars< 发送 GET 请求来获取汽车/code> URL,如Chapter 3, 用春季启动

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

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》添加CRUD功能

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

  1. Open the carfront React app with the VS Code (the React app created in the previous chapter).
  2. When the app has multiple components, it is recommended to create a folder for them. Create a new folder, called components, in the src folder. With the VS Code, you can create a folder by right-clicking the folder in the sidebar file explorer and selecting New Folder from the menu:
读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》添加CRUD功能
  1. Create a new file, called Carlist.js, in the components folder and now your project structure should look like the following:
读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》添加CRUD功能

  1. Open the Carlist.js file in the editor view and write the base code of the component, shown as follows:
import React, { Component } from 'react';

class Carlist extends Component {

  render() {
    return (
      <div></div>
    );
  }
}

export default Carlist;
  1. We need a state for the cars that are fetched from the REST API, therefore, we have to add the constructor and define one array-type state value:
constructor(props) {
  super(props);
  this.state = { cars: []};
} 
  1. Execute fetch in the componentDidMount() life cycle method. The cars from the JSON response data will be saved to the state, called 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. Use the map function to transform car objects into table rows in the render() method and add the table element:
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 应用程序,您应该会看到以下列表页面:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》添加CRUD功能

当我们创建更多的 CRUD 功能时,URL 服务器可以重复多次,并且当 后端部署到 span>server 不是本地主机。因此,最好将其定义为常数。然后,当 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,其中accessor的字段car 对象和 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 组件,我们获得了所有必要的功能 到我们的表中,只需少量编码。现在列表页面如下所示:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》添加CRUD功能

The delete functionality

可以通过发送 DELETE 方法请求从 database 删除项目http://localhost:8080/api/cars/[carid] 端点。如果我们查看 JSON 响应数据,我们可以看到每辆车都包含一个指向 自身 的链接,并且可以从_links.self.href 节点,如以下屏幕截图所示:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》添加CRUD功能

以下步骤显示了如何实现删除功能:

  1. We will create a button for each row in the table and the accessor of the button will be _links.self.href, which we can use to call the delete function that we will create soon. But first, add a new column to the table using Cell to render the button. See the following source code. We don't want to enable sorting and filtering for the button column, therefore these props are set to be false. The button invokes the onDelClick function when pressed and sends a link to the car as an argument:
  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. Implement the onDelClick function. But first, let's take the fetchCars function out from the componentDidMount() method. That is needed because we want to call the fetchCars function also after the car has been deleted to show an updated list of the cars to the user. Create a new function, called fetchCars(), and copy the code from the componentDidMount() method into a new function. Then call the fetchCars() function from the componentDidMount() function to fetch cars initially:
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. Implement the onDelClick function. We send the DELETE request to a car link, and when the delete succeeds, we refresh the list page by calling the fetchCars() function:
// Delete car
onDelClick = (link) => {
  fetch(link, {method: 'DELETE'})
  .then(res => this.fetchCars())
  .catch(err => console.error(err)) 
}

当您启动您的应用程序时,前端应类似于以下屏幕截图,当按下 Delete 按钮时,汽车将从列表中消失:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》添加CRUD功能

在成功删除或有任何错误时向用户显示一些反馈会很好。让我们实现一个 toast 消息来显示删除状态。为此,我们将使用 react-toastify 组件 (https://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 是容器组件 for 显示 toast 消息,应该是在 render() 方法中。在ToastContainer中,可以定义toast消息的duration,单位为毫秒使用 autoClose 道具。在 render() 方法的 return 语句中添加 ToastContainer 组件,就在 ReactTable 之后

return (
  <div className="App">
     <ReactTable data={this.state.cars} columns={columns} filterable={true}/>
     <ToastContainer autoClose={1500} } /> 
   </div>
);

然后,我们将调用 onDelClick() 函数中的 toast 方法来显示 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)
  }) 
 }

现在您将看到删除汽车后的 toast 消息,如下面的屏幕截图所示:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》添加CRUD功能

为了避免汽车的意外deletion,它会nice 在按下删除按钮后有一个确认对话框。我们将使用 react-confirm-alert 组件(https://github.com/GA-MO/react-confirm-alert)。如果您的应用正在运行,请按Ctrl + C< 停止开发服务器/span> 在终端中输入以下命令来安装 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 的新函数,用于打开确认对话框。如果按下对话框的 Yes 按钮,则调用 onDelClick 函数,汽车将被删除:

confirmDelete = (link) => {
  confirmAlert({
    message: 'Are you sure to delete?',
    buttons: [
      {
        label: 'Yes',
        onClick: () => this.onDelClick(link)
      },
      {
        label: 'No',
      }
    ]
  })
}

然后,将 Delete 按钮的 onClick 事件中的函数更改为 确认删除

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>)
  }]

如果您现在按下 Delete 按钮,确认 dialog 将被打开 并且 只有按下 才会删除汽车按钮:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》添加CRUD功能

The add functionality

下一步是为前端创建一个 add 功能。我们将使用 React Skylight 模态组件来实现它。我们已经第 8 章适用于 React 的有用的第三方 React 组件。我们将 New Car 按钮添加到用户界面,当按下它时会打开模态表单。模态表单包含保存汽车所需的所有字段以及保存和取消按钮。

在终端中按Ctrl + C停止开发服务器,然后键入以下命令来安装 React Skylight。安装后,重启应用:

npm install react-skylight --save

以下步骤展示了如何使用模态表单组件创建添加功能:

  1. Create a new file, called AddCar.js, in the components folder and write a component-class base code to the file, as shown here. Add the import for the react-skylight component:
import React from 'react';
import SkyLight from 'react-skylight';

class AddCar extends React.Component {
  render() {
    return (
      <div>
      </div> 
    );
  }
}

export default AddCar;
  1. Introduce a state that contains all car fields:
constructor(props) {
   super(props);
   this.state = {brand: '', model: '', year: '', color: '', price: ''};
}
  1. Add a form inside the render() method. The form contains the ReactSkylight modal form component with buttons and the input fields that are needed to collect the car data. The button that opens the modal window, and will be shown in the carlist page, must be outside ReactSkylight. All input fields should have the name attribute with a value that is the same as the name of the state the value will be saved to. Input fields also have the onChange handler, which saves the value to state by invoking the handleChange function:
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. Insert the AddCar component to the Carlist component to see whether that form can be opened. Open the Carlist.js file to editor view and import the AddCar component:
import AddCar from './AddCar.js';
  1. Implement the addCar function to the Carlist.js file that will send the POST request to the backend api/cars endpoint. The request will include the new car object inside the body and the 'Content-Type': 'application/json' header. The header is needed because the car object is converted to JSON format using the JSON.stringify() method:
// 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. Add the AddCar component to the render() method and pass the addCar and fetchCars functions as props to the AddCar component that allows us to call these functions from the AddCar component. Now the return statement of the CarList.js file should look like the following:
// 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>
);

如果您启动前端应用程序,它现在应该如下所示,如果您按下 New Car 按钮,它应该会打开模态表单:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》添加CRUD功能

  1. Implement the handleSubmit and cancelSubmit functions to the AddCar.js file. The handleSubmit function creates a new car object and calls the addCar function, which can be accessed using props. The cancelSubmit function just closes the modal form:
// 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(); 
}

现在,您可以通过按 New Car modal 表单代码> 按钮。然后您可以用数据填写表单,然后按 Save 按钮。到目前为止,表单 does 看起来不太好,但我们将在下一章中设置它的样式:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》添加CRUD功能

列表页面刷新,可以在列表中看到新车:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》添加CRUD功能

The edit functionality

我们将通过将表格更改为可编辑并将保存按钮添加到每一行来实现 edit 功能。保存按钮将调用发送PUT请求的函数用于将更改保存到数据库的后端:

  1. Add the cell renderer, which changes the table cells to editable. Open the Carlist.js file and create a new function called renderEditable. See the source code for the following function. The cell will be the div element and the contentEditable attribute makes it editable. suppressContentEditableWarning suppresses the warning that comes when the element with the child is marked to be editable. The function in onBlur is executed when the user leaves the table cell, and this is where we will update the state:
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. Define the table columns that are going to be editable. This is done using the Cell attribute of the column in React Table, which defines how the cell of the column will be rendered:
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>)
}]

现在,如果您在浏览器中打开应用程序,您可以看到表格单元格是可编辑的:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》添加CRUD功能

  1. To update car data, we have to send the PUT request to the http://localhost:8080/api/cars/[carid] URL. The link will be the same as with the delete functionality. The request contains the updated car object inside the body, and the 'Content-Type': 'application/json' header that we had in the add functionality. Create a new function, called updateCar, and the source code of the function is shown in the following code snippet. The function gets two arguments, the updated car object and the request URL. After the successful update, we will show a toast message to the user:
// 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. Add the Save button to the table rows. When the user presses the button, it calls the updateCar function and passes two arguments. The first argument is row, which is all values in the row as an object (=car object). The second argument is value, which is set to be _links.href.self, which will be the URL of the car that we need in the request:
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>)
}]

如果您现在编辑 table 中的值并按 Save 按钮,您应该会看到 toast 消息,并且更新的值已保存 到数据库:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》添加CRUD功能

Other functionalities

我们还将实现的一项功能是数据的 CSV 导出。有一个包,叫做 react-csv (https://github.com/abdennour/react-csv),可用于将数据数组导出到 CSV 文件。

如果您的应用已启动,请按Ctrl server em> + 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>
);

在浏览器中打开应用程序,您应该会在我们的应用程序中看到 Export CSV 链接。样式不是很好,但我们将在下一章中处理它。如果您点击链接,您将获得 CSV 文件中的数据:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》添加CRUD功能

现在所有的功能都已经实现了。

Summary


在本章中,我们为我们的应用程序实现了所有功能。我们首先从后端获取汽车并将它们显示在 React Table 中,它提供了分页、排序和过滤功能。然后我们实现了删除功能,并使用 toast 组件向用户提供反馈。添加功能是使用 React Skylight modal-form 组件实现的。在编辑功能中,我们利用了 React Table 功能,使表格可编辑。最后,我们实现了将数据导出到 CSV 文件的功能。在下一章中,我们将开始使用 Material UI 组件库来完善我们的用户界面。在下一章中,我们将使用 React Material-UI 组件库来设计我们的前端。

Questions


  1. How should you fetch and present data using the REST API with React?
  2. How should you delete data using the REST API with React?
  3. How should you add data using the REST API with React?
  4. How should you update data using the REST API with React?
  5. How should you show toast messages with React?
  6. How should you export data to a CSV file with React?