10 Frontend React
Node.js
- Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine.
- Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.
- Node.js package ecosystem, npm, is the largest ecosystem of open source libraries in the world.
https://nodejs.org
(Download LTS Version)
NPM
- npm is the package manager for JavaScript and the world's largest software registry.
- discover packages of reusable code -- and assemble them in powerful new ways.
https://docs.npmjs.com/cli/install
React
https://github.com/facebook/create-react-app
npx create-react-app frontend
cd frontend
npm start
- Install component library
npm add antd
at command line
On the top of src/index.css
file, add this line
@import '~antd/dist/antd.css';
- Setup proxy
In the package.json file (there should be only one file with this name under your project folder), add "proxy": "http://localhost:8080"
proxy
是代理的意思,这里的意思是将请求代理到 http://localhost:8080 上,这样就可以解决跨域的问题了。
Create a file at this path, /src/utils.js
utils.js
export const login = (credential) => {
// login 是一个函数,接受一个参数 credential
// credential 是一个对象,包含 username 和 password 两个属性
// 例如:{ username: 'Jerry', password: '123' }
const loginUrl = `/login?username=${credential.username}&password=${credential.password}`;
// loginUrl 是一个字符串,例如:'/login?username=Jerry&password=123'
// 请注意,这里的 username 和 password 都是从 credential 对象中取出的
return fetch(loginUrl, {
// fetch 是浏览器提供的一个 API,用于发起 HTTP 请求
method: 'POST', // HTTP 请求的方法是 POST
headers: { // HTTP 请求的头部信息
'Content-Type': 'application/json', // HTTP 请求的内容类型是 JSON
},
credentials: 'include', // fetch 默认不会发送 Cookie,需要设置 credentials 为 'include'
// 请注意,这里的 body 是一个对象,需要转换成 JSON 字符串
}).then((response) => {
// fetch 的返回值是一个 Promise 对象
if (response.status < 200 || response.status >= 300) {
// 如果 HTTP 响应的状态码不是 2xx,就表示请求失败
throw Error("Fail to log in");
}
});
};
export const signup = (data) => {
// signup 是一个函数,接受一个参数 data
// data 是一个对象,包含 username 和 password 两个属性
const signupUrl = '/signup';
// signupUrl 是一个字符串,表示注册的 API 接口
// 例如:'/signup'
return fetch(signupUrl, { // 使用 fetch 发起 HTTP 请求
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}).then((response) => {
if (response.status < 200 || response.status >= 300) {
throw Error("Fail to sign up");
}
});
};
export const getMenus = (restId) => {
return fetch(`/restaurant/${restId}/menu`).then((response) => {
if (response.status < 200 || response.status >= 300) {
throw Error("Fail to get menus");
}
return response.json();
});
};
export const getRestaurants = () => {
return fetch("/restaurants").then((response) => {
if (response.status < 200 || response.status >= 300) {
throw Error("Fail to get restaurants");
}
return response.json();
});
};
export const getCart = () => {
return fetch("/cart").then((response) => {
if (response.status < 200 || response.status >= 300) {
throw Error("Fail to get shopping cart data");
}
return response.json();
});
};
export const checkout = () => {
return fetch("/checkout").then((response) => {
if (response.status < 200 || response.status >= 300) {
throw Error("Fail to checkout");
}
});
};
export const addItemToCart = (itemId) => {
return fetch(`/cart/${itemId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
}).then((response) =>{
if (response.status < 200 || response.status >= 300) {
throw Error("Fail to add item to cart");
}
});
};
Build the skeleton/layout of the app
find what we need here https://ant.design/components/layout/#header
-
Delete the whole src/App.css file, won't need it
-
Remove everything but the first line in src/index.css file, so it only has
@import '~antd/dist/reset.css';
Use what we found on Ant Design's doc on src/App.js
import logo from './logo.svg';
import './App.css';
import {Content, Header} from "antd/es/layout/layout"
import {Layout} from "antd"
function App() {
return (
<Layout style={{ minHeight: '100vh' }}>
<Header>header</Header>
<Content
style={{
padding: '50px',
maxHeight: "calc(100vh - 64px)",
overflow: "auto",
}}
>
content
</Content>
</Layout>
);
}
export default App;
Build login form
need a form to collect user input and execute a login request
src/components/LoginForm.js
import {login} from "../utils";
import {Button, Form, Input, message} from "antd";
import {UserOutlined} from "@ant-design/icons";
import React from "react";
class LoginForm extends React.Component{
// React.Component 是 React 的基类,用于定义组件类
state = {
// state 是组件内部的状态管理机制,用于存储组件内部的状态
// 通过 this.state 访问
loading: false,
// loading 用于标识当前是否正在登录中
// 通过 this.state.loading 访问
// false 表示当前没有正在登录中的请求
};
onFinish = (data) => {
// onFinish 是 Form 组件的回调函数,当用户点击登录按钮时,会调用 onFinish 函数
// onFinish 函数的参数 data 是一个对象,包含了用户输入的用户名和密码
this.setState({
// setState 是 React 组件的内置方法,用于更新组件的 state
// 通过 this.setState 更新 state
loading: true, // 设置 loading 为 true,表示当前正在登录中
});
// 调用 login 函数,向服务器发送登录请求
login(data)
.then((res) => { // 登录成功
message.success(`Login Successful`);
this.props.onSuccess(); // 调用父组件传递过来的 onSuccess 函数
// onSuccess 函数用于更新 App 组件的 state,将 isLogin 设置为 true
// 从而触发 App 组件的 componentDidUpdate 函数
// props 是组件的属性,通过 this.props 访问
})
.catch((err) => { // 登录失败
message.error(`err.message`);
// message 是 antd 组件库的消息提示组件
})
.finally(() => {
// 无论登录成功还是失败,loading 都应该设置为 false
this.setState({
// setState 是 React 组件的内置方法,用于更新组件的 state
// 通过 this.setState 更新 state
loading: false, // 设置 loading 为 false,表示当前没有正在登录中
});
});
};
render() { // render 函数用于渲染组件的内容
return(
<Form // Form 是 antd 组件库的表单组件
name="normal_login" // 表单的名称
onFinish={this.onFinish} // 表单提交的回调函数
style={{ // 表单的样式
width: 300,
margin: "auto", // 居中显示
}}
>
<Form.Item
name="username"
rules={[{
required: true,
message: "Please input your Username!",
}]}// 表单验证规则 rules
// rules 是一个数组,数组中包含了多个表单验证规则
// required 表示该表单项是必填项
>
<Input prefix={<UserOutlined />} placeholder="Username" />
{/*// Input 是 antd 组件库的输入框组件*/}
{/*// prefix 表示输入框前面的图标*/}
{/*// placeholder 表示输入框的提示文字*/}
{/*// <UserOutlined /> 是 antd 组件库的图标组件*/}
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true,
message: "Please input your Password!"
}]}
>
<Input prefix={<UserOutlined />} type="password" placeholder="Password" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit"
loading={this.state.loading}
>
Login
</Button>
</Form.Item>
</Form>
);
};
}
export default LoginForm;
Build signup form
similarly we need a UI to support sign up
src/components/SignupForm.js
import {signup} from "../utils"
import {Button, Form, Input, message} from "antd"
class SignupForm extends React.Component{
state = {
displayModel: false,
};
handleCancle = () => {
this.setState({
displayModel: false,
});
};
signupOnClick = () => {
this.setState({
displayModel: true,
});
}
onFinish = (data) => {
signup(data)
.then(() => {
this.setState({
displayModel: false,
});
message.success(`Successfully signed up`);
})
.catch((err) => {
message.error(err.message);
});
};
render() { // render 函数用于渲染组件的内容
return(
<>
<Button shape = "round" type="primary" onClick={this.signupOnClick}>
Register
</Button>
<Model
title="Register"
visible={this.state.displayModel}
onCancel={this.handleCancle}
footer={null}
destroyOnClose={true}
>
<Form
name="normal_register"
initialValues={{remember: true}}
onFinish={this.onFinish}
preserve={false}
>
<Form.Item
name="email"
rules={[{ required: true, message: 'Please input your email!' }]}
>
<Input prefix={<UserOutlined />} placeholder="Email" />
</Form.Item>
<Form.Item
name="password"
rules={[{
required: true,
message: 'Please input your password!',
}]}
>
<Input prefix={<LockOutlined />} type="password" placeholder="Password" />
</Form.Item>
<Form.Item
name="firstName"
rules={[{
required: true,
message: 'Please input your first name!' ,
}]}
>
<Input placeholder="first Name" />
</Form.Item>
<Form.Item
name="lastName"
rules={[
{ required: true,
message: 'Please input your last name!' ,
}
]}
>
<Input placeholder="last Name" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Register
</Button>
</Form.Item>
</Form>
</Model>
</>
);
};
}
export default SignupForm;
src/App.js
import {Layout, Typography} from "antd"
import {useState} from "react"
import LoginForm from "./components/LoginForm"
const { Header, Content } = Layout;
// const 是 ES6 的语法,用于定义变量
// Header, Content 是 antd 组件库的组件
// Layout 是 antd 组件库的布局组件
// 通过 import 语句引入 antd 组件库的组件
const { Title } = Typography;
// Typography 是 antd 组件库的排版组件
// Title 是 antd 组件库的标题组件
function App() {
const [authed, setAuthed] = useState(false);
// useState 是 React 的内置 Hook 函数,用于定义组件的 state
// authed 是 state 的名称,用于存储用户的登录状态
// setAuthed 是用于更新 authed 的函数
// useState(false) 表示 authed 的初始值为 false
return (
<Layout style={{ minHeight: '100vh' }}>
<Header>
<div className="header">
<Title level={2}
style={{color: "white", lineHeight: "inherit", marginBottom: 0}}
>
Eve Food
</Title>
</div>
</Header>
<Content
style={{
padding: '50px',
maxHeight: "calc(100vh - 64px)",
overflow: "auto",
}}
>
{
authed ?
(<div>content placeholder</div>)
:
(<LoginForm onSuccess={() => setAuthed(true)}/> )
}
</Content>
</Layout>
);
}
export default App;
Build FoodList
src/components/FoodList.js
import {Button, Card, List, message, Select, Tooltip} from "antd"
import {addItemToCart, getMenus, getRestaurants} from "../utils"
import {useEffect, useState} from "react"
const { Option } = Select;
const AddToCartButton = ({ itemId }) => {
const [loading, setLoading] = useState(false);
const AddToCart = () => {
setLoading(true);
addItemToCart(itemId)
.then(() => message.success("Successfully add item"))
.catch((err) => message.error(err.message))
.finally(() => {
setLoading(false);
});
};
return (
<Tooltip title="Add to Cart">
<Button
loading={loading}
type="primary"
icon={<PlusOutlined />}
onClick={AddToCart}
/>
</Tooltip>
);
};
const FoodList = () => {
const [foodData, setFoodData] = useState([]);
const [curRest, setCurRest] = useState(null);
const [restaurants, setRestaurants] = useState([]);
const [loading, setLoadings] = useState(false);
const [loadingRest, setLoadingRest] = useState(false);
useEffect(() => {
setLoadingRest(true);
getRestaurants()
.then((data) => {
setRestaurants(data);
})
.catch((err) => {
message.error(err.message);
})
.finally(() => {
setLoadingRest(false);
});
}, []);
useEffect(() => {
if (curRest) {
setLoadings(true);
getMenus(curRest)
.then((data) => {
setFoodData(data);
})
.catch((err) => {
message.error(err.message);
})
.finally(() => {
setLoadings(false);
});
}
}, [curRest]);
return (
<>
<Select
value={curRest}
onSelect={(value) => setCurRest(value)}
placeholder="Select a restaurant"
loading={loadingRest}
style={{ width: 300 }}
onChange={() => {}}
>
{restaurants.map((item) => {
return (
<Option key={item.id} value={item.id}>
{item.name}
</Option>
);
})}
</Select>
{curRest && (
<List
style={{ marginTop: 20 }}
loading={loading}
grid={{
gutter: 16,
xs: 1,
sm: 2,
md: 4,
lg: 4,
xl: 3,
xxl: 3,
}}
dataSource={foodData}
renderItem={(item) => (
<List.Item>
<Card
title={item.name}
extra={<AddToCartButton itemId={item.id} />}
>
<img
src={item.imageUrl}
alt={item.name}
style={{ height: 340, width: "100%", display: "block" }}
/>
{`Price: ${item.price}`}
</Card>
</List.Item>
)}
/>
)}
</>
);
}
export default FoodList;
Build MyCart
src/components/MyCart.js
import {Button, Drawer, List, message, Typography} from "antd";
import React, {useEffect, useState} from "react";
import {checkout, getCart} from "../utils";
const { Text } = Typography;
const MyCart = () => {
const [cartVisible, setCartVisible] = useState(false);
const [cartData, setCartData] = useState();
const [loading, setLoading] = useState(false);
const [checking, setChecking] = useState(false);
useEffect(() => {
if (!cartVisible) {
return;
}
setLoading(true);
getCart()
.then((data) => {
setCartData(data);
})
.catch((err) => {
message.error(err.message);
})
.finally(() => {
setLoading(false);
});
}, [cartVisible]);
const onCheckout = () => {
setChecking(true);
checkout()
.then(() => {
message.success("Successfully checked out");
setCartVisible(false);
})
.catch((err) => {
message.error(err.message);
})
.finally(() => {
setChecking(false);
});
};
const onCloseDrawer = () => {
setCartVisible(false);
};
const onOpenDrawer = () => {
setCartVisible(true);
};
return (
<>
<Button type="primary" shape="round" onClick={onOpenDrawer}>
Cart
</Button>
<Drawer
title="My Cart"
onClose={onCloseDrawer}
visible={cartVisible}
width={520}
footer={
<div
style={{
display: "flex",
justifyContent: "space-between",
}}
>
<Text strong={true}>{`Total price:
$${cartData?.totalPrice}`}
</Text>
<div>
<Button onClick={onCloseDrawer} style={{ marginRight: 8 }}>
Cancel
</Button>
<Button
onClick={onCheckout}
type="primary"
loading={checking}
disabled={loading || cartData?.orderItemList.length === 0}
>
Checkout
</Button>
</div>
</div>
}
>
<List
loading={loading}
itemLayout="horizontal"
dataSource={cartData?.orderItemList}
renderItem={(item) => (
<List.Item>
<List.Item.Meta
title={item.menuItem.name}
description={`$${item.price}`}
/>
</List.Item>
)}
/>
</Drawer>
</>
);
};
export default MyCart;
Integrate everything
src/app.js
import {Layout, Typography} from "antd"
import {useState} from "react"
import LoginForm from "./components/LoginForm"
import FoodList from "./components/FoodList"
import MyCart from "./components/MyCart"
import SignupForm from "./components/SignupForm"
const { Header, Content } = Layout;
// const 是 ES6 的语法,用于定义变量
// Header, Content 是 antd 组件库的组件
// Layout 是 antd 组件库的布局组件
// 通过 import 语句引入 antd 组件库的组件
const { Title } = Typography;
// Typography 是 antd 组件库的排版组件
// Title 是 antd 组件库的标题组件
function App() {
const [authed, setAuthed] = useState(false);
// useState 是 React 的内置 Hook 函数,用于定义组件的 state
// authed 是 state 的名称,用于存储用户的登录状态
// setAuthed 是用于更新 authed 的函数
// useState(false) 表示 authed 的初始值为 false
return (
<Layout style={{ minHeight: '100vh', backgroundColor: 'black'}}>
<Header style={{ backgroundColor: 'black'}}>
<div className="header">
<Title level={2}
style={{color: "white", lineHeight: "inherit", marginBottom: 0}}
>
Eve Restaurant Order
</Title>
<div>{authed ? <MyCart /> : <SignupForm />}</div>
</div>
</Header>
<Content
style={{
padding: '50px',
maxHeight: "calc(100% - 64px)",
overflowY: "auto",
}}
>
{
authed ?
(<FoodList />)
:
(<LoginForm onSuccess={() => setAuthed(true)}/> )
}
</Content>
</Layout>
);
}
export default App;
Now need to build our front end code and move them to server project, then all code will be hosted on the same server.
Open the terminal, at fronted , run npm run build
Copy all the generated file (front end) into the webapp folder under you backend project.
And int the file onlineOrder-servlet.xml
, add this some thing.
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:resources mapping="/**" location="/" />
<context:component-scan base-package="com.eve.onlineOrder" />
</beans>
Run the project http://localhost:8080