React + Spring Rest
REACT + Spring Rest
2021/11/02 (內容更新)
2021/11/09 (內容更新)
2021/11/15 (內容更新)
2021/11/19 (內容更新)
讀取資料
請先參考
First React (吳濟聰老師「行動裝置程式設計」課程教材)
Functional Component (吳濟聰老師「行動裝置程式設計」課程教材)
先參考ProductList.js,產生一個CustomerList.js
/src/customer/CustomerList.js
import React from 'react';
import { Box, List, ListItem, ListItemText } from '@mui/material';
export default function CustomerList() {
const customers= [
{name:"Ben", weight:60},
{name:"Mary", weight:50},
];
return (
<Box sx={{
width: '100vw',
height: '100vh',
backgroundColor: 'background.paper',
color: 'black',
textAlign: 'left'
}}>
<List subheader="customer list" aria-label="customer list">
{customers.map((customer, index) =>
<ListItem divider key={index}>
<ListItemText primary={customer.name} secondary={"體重:"+customer.weight}></ListItemText>
</ListItem>)}
</List>
</Box>
);
}
在javascript中,可以利用axios的get去呼叫API (詳參: Using Axios with React),因為要等待API資料回傳,所以,在axios.get前,加了await,在await後的指令就會等待資料回傳後才會繼續執行。(詳參: 鐵人賽:JavaScript Await 與 Async)
useEffect(() => {
async function fetchData () {
const result = await axios.get("http://localhost:8080/customer");
console.log(result);
}
fetchData();
},[]);
使用axios前要先安裝:
npm install axios
src/customer/CustomerList.js
import React, {useEffect} from 'react';
import { Box, List, ListItem, ListItemText } from '@mui/material';
import axios from 'axios';
export default function CustomerList() {
const customers= [
{name:"Ben", weight:60},
{name:"Mary", weight:50},
];
useEffect(() => {
async function fetchData () {
const result = await axios.get("http://localhost:8080/customer");
console.log(result);
}
fetchData();
},[]);
return (
<Box sx={{
width: '100vw',
height: '100vh',
backgroundColor: 'background.paper',
color: 'black',
textAlign: 'left'
}}>
<List subheader="customer list" aria-label="customer list">
{customers.map((customer, index) =>
<ListItem divider key={index}>
<ListItemText primary={customer.name} secondary={"體重:"+customer.weight}></ListItemText>
</ListItem>)}
</List>
</Box>
);
}
請記得,要將App.js的內容改為
import './App.css';
//import ProductList from './product/ProductList';
import CustomerList from './customer/CustomerList';
function App() {
return (
<div className="App">
<header className="App-header">
<CustomerList/>
</header>
</div>
);
}
export default App;
Proxy
執行時,會看不到結果,事實上是因為cross-origin requests (CORS)被擋了,因為spring是跑在8080 port而react跑在3000 port,spring因為資訊安全的關係,拒絕了react的要求。
Access to XMLHttpRequest at 'http://localhost:8080/customer' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
所以,請打開react專案裡的package.json,增加proxy的設定,讓spring認為是在同樣的8080 port。
**不過,這樣的作法只適合在測試的情況之下使用 。
其他的解決方法:
可以將react跟spring放在一起(詳參: Webapp with Create React App and Spring Boot)。
Why you should use React DevServer Proxy
No more CORS errors and need for preflight requests
Easy handling of HTTP/HTTPS forwarding
Easy to simulate the production environment
Easy to control error logging
Simple to work with relative paths in frontend
如果系統必須佈在兩個不同的伺服器上,就要設定CORS
新版的react,proxy設定與上面文章說的,有點不一樣 (請參考:Proxying API Requests in Development 或 https://coursework.vschool.io/setting-up-a-full-stack-react-application/)
記得要重開react server,proxy的設定才會生效
在spring server端的url跟react端不能重複,否則會搞混
例如:
如果有http://localhost:8080/question
也有http://localhost:3000/question (開始使用router時)
proxy就不會導到8080了
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material": "^5.0.6",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"axios": "^0.24.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"web-vitals": "^0.2.4",
"workbox-background-sync": "^5.1.4",
"workbox-broadcast-update": "^5.1.4",
"workbox-cacheable-response": "^5.1.4",
"workbox-core": "^5.1.4",
"workbox-expiration": "^5.1.4",
"workbox-google-analytics": "^5.1.4",
"workbox-navigation-preload": "^5.1.4",
"workbox-precaching": "^5.1.4",
"workbox-range-requests": "^5.1.4",
"workbox-routing": "^5.1.4",
"workbox-strategies": "^5.1.4",
"workbox-streams": "^5.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"proxy": "http://localhost:8080",
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
因為設了proxy,要讓proxy生效,要更改呼叫的方式。這樣才能透過proxy去呼叫locahost:8080下的服務。
const result = await axios.get("/customer");
這樣的話,就可以在console.log裡看到內容了。
但是,還沒顯示在畫面中,要把內容放在畫面中的片段程式,如:
const [customers, setCustomers] = useState([]);
useEffect(() => {
async function fetchData () {
const result = await axios.get("/customer");
//console.log(result);
setCustomers(result.data);
}
fetchData();
},[]);
作業
修改CustomerList,將讀取到的customer資料,顯示在畫面中。想想看,你對useState、useEffect的使用是不是有足夠的了解....
新增資料
請先參考Form (吳濟聰老師「行動裝置程式設計」課程教材)
新增的部分會需要使用axios.post,因為post需要傳值,所以,會有第二個參數,這個參數是一個javascript物件,axios會把這個物件轉換成JSON格式傳送給遠端的REST服務。如果REST服務有回傳值,axios.post也會收到回傳值。
要注意,每一個變數的名稱,要和後端Customer.java的變數名稱一致,否則rest controller會無法自動對應。因為我們的post並沒有回傳值,所以,直接執行post。
const update = async function(){
try{
await axios.post("/customer",customer);
}
catch(e){
console.log(e);
}
props.close();
}
修改跟新增共用同一個元件,我們把檔案命名為CustomerAddEdit
src/customer/CustomerAddEdit.js
import React, {useState} from 'react';
import { Button, Dialog, TextField } from '@mui/material';
import axios from 'axios';
export default function CustomerAddEdit(props) {
const [customer, setCustomer] = useState({name:"", weight:0});
const handleClick = function(e){
setCustomer({...customer,[e.target.name]:e.target.value})
}
const update = async function(){
try{
await axios.post("/customer",customer);
}
catch(e){
console.log(e);
}
props.close();
}
return (
<Dialog open={props.open}>
<TextField label ="名稱" name ="name" variant="outlined" value={customer.name} onChange={handleClick}/>
<TextField label ="體重" type="number" name ="weight" variant="outlined" value={customer.weight} onChange={handleClick}/>
<Button variant="contained" color="primary" onClick={update}>新增</Button>
<Button variant="contained" color="secondary" onClick={()=>props.close()}>取消</Button>
</Dialog>
);
}
在ProductList裡,增加FAB,透過props去開啟或關閉對話框。
src/customer/CustomerList.js
import React, {useEffect, useState} from 'react';
import { Box, Fab, List, ListItem, ListItemText } from '@mui/material';
import { Add as AddIcon } from '@mui/icons-material';
import axios from 'axios';
import CustomerAddEdit from './CustomerAddEdit';
export default function CustomerList() {
const [customers, setCustomers] = useState([]);
const [open, setOpen] = useState(false);
useEffect(() => {
async function fetchData () {
const result = await axios.get("/customer");
console.log(result);
setCustomers(result.data);
}
fetchData();
},[open]);
const addData = function() {
setOpen(true);
}
const close = function() {
setOpen(false);
}
return (
<Box sx={{
width: '100vw',
height: '100vh',
backgroundColor: 'background.paper',
color: 'black',
textAlign: 'left'
}}>
<List subheader="customer list" aria-label="customer list">
{customers.map((customer, index) =>
<ListItem divider key={index}>
<ListItemText primary={customer.name} secondary={"體重:"+customer.weight}></ListItemText>
</ListItem>)}
</List>
<Fab color="primary" aria-label="新增" onClick={addData}
sx={{
position: "fixed",
bottom: (theme) => theme.spacing(2),
right: (theme) => theme.spacing(2)
}}
>
<AddIcon />
</Fab>
<CustomerAddEdit open={open} close={close}/>
</Box>
);
}
要注意一件事,useEffect的第二個參數,如果是個空陣列,useEffect只會執行一次,如果沒有第二個參數的話,會監控所有的state,因為customers的內容是被REST服務更新,會造成useEffect一直在執行,所以,要設定為監控特定變數,因為,每次新增都會更動open,所以,我們就監控這個變數:
useEffect(() => {
async function fetchData () {
const result = await axios.get('/customer');
setCustomers(result.data);
}
fetchData();
},[open]);
作業
想一想你們的學期專案,開始規劃資料表、畫面。
不同畫面的串連可先參考Router & Material UI (吳濟聰老師「行動裝置程式設計」課程教材)
注意,react router現在是第6版,原本的內容還來不及完全改成第6版
Delete
接下來是增加「刪除」,刪除的部分是將id當作url的一部分,傳送給遠端的REST服務。如果REST服務有回傳值,axios.delete()也會收到回傳值。
await axios.delete("/customer/"+id);
增加一個delete icon,設定onClick時,呼叫deleteData。
<List subheader="customer list" aria-label="customer list">
{customers.map((customer, index) =>
<ListItem divider key={index}>
<ListItemText primary={customer.name} secondary={"體重:"+customer.weight}></ListItemText>
<IconButton edge="end" aria-label="delete" onClick={()=>deleteData(customer.id)}>
<DeleteIcon />
</IconButton>
</ListItem>)}
</List>
import相關元件
import { Box, Fab, IconButton, List, ListItem, ListItemText } from '@mui/material';
import { Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon } from '@mui/icons-material';
deleteData()中為了重整畫面,改變open的內容。
const deleteData = async function(id) {
await axios.delete("/customer/"+id);
setOpen(true);
setOpen(false);
}
也可以增加了一個state變數:deleted
const [deleted, setDeleted] = useState(false);//if record deleted alter this state
完成刪除時,更動這個state:
setDeleted(currentDeleted=>setDeleted(!currentDeleted));
useEffect要監控這個變數:
useEffect(() => {
async function fetchData () {
const result = await axios.get('/customer');
setCustomers(result.data);
}
fetchData();
},[open, deleted]);
Update
先增加一個update icon。
<List subheader="customer list" aria-label="customer list">
{customers.map((customer, index) =>
<ListItem divider key={index}>
<ListItemText primary={customer.name} secondary={"體重:"+customer.weight}></ListItemText>
<IconButton edge="end" aria-label="update" onClick={()=>updateData(customer)}>
<EditIcon />
</IconButton>
<IconButton edge="end" aria-label="delete" onClick={()=>deleteData(customer.id)}>
<DeleteIcon />
</IconButton>
</ListItem>)}
</List>
比較複雜的方法是讓新增與修改共用同一個form,好處是,如果以後增加欄位,只要在一個form裡處理即可。因為我們共用一個form,所以,把元件的名稱也更動為CustomerAddEdit,並將要修改的內容利用prop傳到CustomerAddEdit
<CustomerAddEdit open={open} close={close} customer={currentCustomer}/>
新增updateData(),並且將收到的變數
const updateData = function(customer) {
setCurrentCustomer(customer);
setOpen(true);
}
在CustomerAddEdit裡,很直覺的會這樣改:
export default function CustomerAddEdit(props) {
console.log(props.customer);
const [customer, setCustomer] = useState(props.customer);
console.log沒問題,可是畫面上的內容錯誤,原因是useState只執行一次。所以,要利用useEffect去更新內容。
useEffect(()=>setCustomer(props.customer),[props.customer]);
先利用id是空白或者是非空白來判斷是增加還是編輯
if (customer.id){
await axios.put("/customer",customer);
}
else {
await axios.post("/customer",customer);
}
按鈕的部分
<Dialog open={props.open}>
<DialogContent>
<TextField label ="名稱" name ="name" variant="outlined" value={customer.name} onChange={handleClick}/>
<TextField label ="體重" type="number" name ="weight" variant="outlined" value={customer.weight} onChange={handleClick}/>
</DialogContent>
<DialogActions>
<Button variant="contained" color="primary" onClick={update}>{customer.id?"修改":"新增"}</Button>
<Button variant="contained" color="secondary" onClick={()=>props.close()}>取消</Button>
</DialogActions>
</Dialog>
接下來,會發現,執行過修改之後,按新增會以為是修改,因為currentCustomer需要清空:
const addData = function() {
setCurrentCustomer({name:"", weight:0});
setOpen(true);
}
const updateData = function(customer) {
setCurrentCustomer(customer);
setOpen(true);
}
作業
想一想你們的學期專案,開始規劃資料表、畫面。
不同畫面的串連請參考Router & Material UI (吳濟聰老師「行動裝置程式設計」課程教材)
注意,react router現在是第6版,原本的內容還來不及完全改成第6版
REACT + Spring Rest (class component)
2019/01/02 (class components的寫法)
2019/01/16 (補充相關內容)
React
** 將 Spring Rest Controller 及 React 串連起來
在專案目錄下,新增一個frontend目錄,在frontend目錄下執行
create-react-app .
將src/index.js的內容改為。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import ReactApp from './components/react-app.js';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<ReactApp />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();
在src目錄下,再新增components目錄,並新增react-app.jsx (因為會使用到axios,要記得安裝axios) ,在constructor裡定義state裡有一個employee的陣列及Axios的設定,employees的內容則是在componentsDidMount()裡利用axios取得,並儲存在state裡面,最後,利用元件的 props功能,傳遞了employees給EmployeeList類別(詳参: React的prop與state)。
import React from 'react';
import axios from 'axios';
import EmployeeList from './employee-list'
export default class ReactApp extends React.Component {
constructor(props) {
super(props);
this.state = {employees: []};
this.Axios = axios.create({
baseURL: "/employee",
headers: {'content-type': 'application/json'}
});
}
componentDidMount() {
let _this = this;
this.Axios.get('/')
.then(function (response) {
console.log(response);
_this.setState({employees: response.data});
})
.catch(function (error) {
console.log(error);
});
}
render() {
return (
<div>
<EmployeeList employees={this.state.employees}/>
</div>
)
}
}
在src/components/employee-list.jsx裡,利用從ReactApp透過props傳到EmployeeList的employees,將陣列裡的每一個內容再利用map轉成每一個employee,再透過props,傳遞了key及employee給Employee類別(詳参: React的prop與state)
import React from 'react';
import Employee from './employee.jsx'
export default class EmployeeList extends React.Component{
render() {
var employees = this.props.employees.map((employee, i) =>
<Employee key={i} employee={employee}/>
);
return (
<table>
<tbody>
<tr>
<th>ID</th>
<th>Name</th>
<th>Department</th>
</tr>
{employees}
</tbody>
</table>
)
}
}
在src/components/employee.js裡,利用從EmployeeList透過props傳入的employee,將每個欄位放到對應的位置上
import React from 'react';
export default class Employee extends React.Component{
render() {
return (
<tr>
<td>{this.props.employee.id}</td>
<td>{this.props.employee.name}</td>
<td>{this.props.employee.department}</td>
</tr>
)
}
}
Proxy
執行時,會看不到結果,事實上是因為cross-origin requests (CORS)被擋了,因為spring是跑在8080 port而react跑在3000 port,spring因為資訊安全的關係,拒絕了react的要求。
所以,請打開react專案裡的package.json,增加proxy的設定,讓spring認為是在同樣的8080 port。
**不過,這樣的作法只適合在測試的情況之下使用 (詳參: Webapp with Create React App and Spring Boot)。
新版的react,proxy設定與上面文章說的,有點不一樣 (請參考:Proxying API Requests in Development 或 https://coursework.vschool.io/setting-up-a-full-stack-react-application/)
{
"name": "test-react",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^0.18.0",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-scripts": "2.1.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"proxy": "http://localhost:8080/",
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
這樣的話,就可以看到內容了。
基於上面的範例,我們來讓使用者可以新增資料。先在EmployeeController.java中(Spring Rest Controller)增加一個Post,@RequestBody就是讓rest controller將傳過來的內容,轉換成employee。
package com.example.demo;
import java.util.ArrayList;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class EmployeeController {
private List<Employee> employeeList = new ArrayList<>();
private int count=0;
public EmployeeController(){
employeeList.add(new Employee(1, "Arpit", "IT"));
employeeList.add(new Employee(2, "Sanjeev", "IT"));
employeeList.add(new Employee(3, "Ben", "IT"));
count = 3;
}
@GetMapping("/employee/")
public List<Employee> get() {
return employeeList;
}
@PostMapping("/employee/")
public void post(@RequestBody Employee employee) {
count++;
employee.setId(count);
employeeList.add(employee);
}
}
在src/components/react-app.jsx中,新增了一個元件。
import React from 'react';
import axios from 'axios';
import EmployeeList from './employee-list'
import EmployeeAdd from './employee-add'
export default class ReactApp extends React.Component {
constructor(props) {
super(props);
this.state = {employees: []};
this.Axios = axios.create({
baseURL: "/employee",
headers: {'content-type': 'application/json'}
});
}
componentDidMount() {
let _this = this;
this.Axios.get('/')
.then(function (response) {
console.log(response);
_this.setState({employees: response.data});
})
.catch(function (error) {
console.log(error);
});
}
render() {
return (
<div>
<EmployeeAdd/>
<EmployeeList employees={this.state.employees}/>
</div>
)
}
}
在src/components/employee-add.jsx中,包括兩個處理事件的方法(handleChange、handleSubmit),另外,加了一個form,在輸入之後,利用setState將資料存起來,一般而言,可以在這些方法中順便檢查一下資料的格式或內容。當按下submit時,再把內容利用axios送出。細節可參考: Handling Events 、Forms以及How to handle forms with just React。在ES7之後,可以利用arrow function來簡化寫法 (詳參: The best way to bind event handlers in React 、Submitting Form Data With React)。
要注意,每一個變數的名稱,要和後端Employee.java的變數名稱一致,否則rest controller會無法自動對應。
import React from 'react';
import axios from 'axios';
export default class EmployeeAdd extends React.Component{
handleChange=(event)=>{
this.setState({ [event.target.name]: event.target.value });
}
handleSubmit=(event)=> {
//event.preventDefault();
const employee = {
id:0,
name: this.state.name,
department: this.state.department
};
axios.post("/employee/", employee )
.then(res => {
console.log(res);
console.log(res.data);
})
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<label>
Person Name:
<input type="text" name="name" onChange={this.handleChange} />
</label>
<label>
Department:
<input type="text" name="department" onChange={this.handleChange} />
</label>
<button type="submit">Add</button>
</form>
</div>
)
}
}