文章目录
- 简单了解React和Node
- 搭建开发环境
- React框架
- JavaScript客户端
- ChallengeComponent
- 组件的主要结构
- 渲染
- 与应用程序集成
- 第一次运行前端
- 调试
- 将CORS配置添加到Spring Boot应用
- 使用应用程序
- 部署React应用程序
- 小结
前端代码可从这里下载: 前端示例
后端使用这里介绍的Spring Boot应用程序:一个测试驱动的Spring Boot应用程序开发
现实世界中,用户不会通过REST API使用应用程序,因此,这里为这个服务构造前端应用,便于用户与应用程序进行交互。
简单了解React和Node
React是一个构建用户界面的JavaScript库,由Facebook开发,很流行,已被广泛使用。React基于组件构建,编写一段代码即可在多处复用,这很有优势。可以创建像 Thumbnail、LikeButton 和 Video 这样的组件,然后将它们组合成整个应用程序。
React 组件是 JavaScript 函数,学习 React 就是学习编程。可以在React中使用 JSX,这是由 React 推广的 JavaScript 语法扩展,它允许将 JSX 标签与相关的渲染逻辑放在一起,使得创建、维护和删除 React 组件变得容易。
React 组件接收数据并返回应该出现在屏幕上的内容。可以通过响应交互(例如用户输入)向它们传递新数据。然后,React 将更新屏幕以匹配新数据。也可以不用 React 去构建整个页面,而只是将 React 添加到现有的 HTML 页面中,在任何地方呈现交互式的 React 组件。
React 允许将组件放在一起,而不关注路由和数据获取。要使用 React 构建整个应用程序,建议使用像 Next.js 或 Remix 这样的全栈 React 框架。
React 也是一种架构。实现它的框架可以在服务端甚至是构建阶段使用异步组件来获取数据,也可以从文件或数据库读取数据,并将其传递给交互式组件。
简单的说 Node.js 就是运行在服务端的 JavaScript。Node.js 是一个基于 Chrome JavaScript 运行时建立的一个平台。Node.js 是一个事件驱动 I/O 服务端 JavaScript 环境,基于 Google 的 V8 引擎,V8 引擎执行 Javascript 的速度非常快,性能非常好。
如果你是一个前端程序员,想创建自己的服务,那么 Node.js 是一个非常好的选择。如果你熟悉 Javascript,那么将会很容易的学会 Node.js。当然,如果你是后端程序员,想部署一些高性能的服务,那么学习 Node.js 也是一个非常好的选择。
搭建开发环境
首先,需要到nodejs.org站点上下载应用程序包来安装Node.js,可以下载免安装的zip版本,配置相应的环境变量即可。安装后可以在控制台输入下列命令,查看Node.js和npm的版本:
> node -v
v16.14.2
> npm -v
9.8.1
现在,最新的长期支持的Node版本是20.9.0。
有了Node,就可以使用npx来创建React项目了,命令如下:
> npx create-react-app multiplication-frontend
下载并安装依赖后,会看到如下输出:
> npx create-react-app multiplication-frontend
Creating a new React app in Z:\_Learn\multiplication-frontend.
Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...
added 1463 packages in 42s
Initialized a git repository.
Installing template dependencies using npm...
added 69 packages, and changed 1 package in 5s
Removing template package using npm...
removed 1 package in 2m
Created git commit.
Success! Created multiplication-frontend at Z:\_Learn\multiplication-frontend
Inside that directory, you can run several commands:
npm start
Starts the development server.
npm run build
Bundles the app into static files for production.
npm test
Starts the test runner.
npm run eject
Removes this tool and copies build dependencies, configuration files
and scripts into the app directory. If you do this, you can’t go back!
We suggest that you begin by typing:
cd multiplication-frontend
npm start
Happy hacking!
进入项目目录,执行npm start
命令,Node服务器就会启动并打开一个浏览器窗口,访问localhost:3000
地址,显示刚刚生成的应用程序。
> cd .\multiplication-frontend\
> npm start
默认页面如图所示:
React框架
在IDEA中,加载项目,create-react-app工具已经创建了许多文件,如图所示:
- package.json和package-lock.json是npm文件,包含关于项目的基本信息,列出相关依赖,依赖项存储在node_modules文件夹中。
- public存储所有创建后不再变动的静态文件。唯一的例外是index.html,应对其处理以包含生成的JavaScript代码。
- src存储所有的React源文件及其相关资源。这里可以找到入口点文件index.js和一个React组件App,该示例组件附带自己的样式表App.css和一个测试文件App.test.js。
这里,从index.html开始,删除注释后,内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
body很简单。index.js定义了渲染内容的入口,代码如下:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
reportWebVitals();
DOM(文档对象模型)是HTML元素的树结构表达,这段代码将元素React.StrictMode
及其子元素App组件渲染到HTML中。具体地说,将渲染到ID为root的元素中,即插入index.html中的div标签。由于App是一个组件,可能包含其他组件,因此,最终会处理并渲染整个React应用程序。
JavaScript客户端
在创建第一个组件前,要确保可以访问前面创建的REST API接口,这需要使用JavaScript类。
JavaScript中的类与Java类类似,如下所示:
class ApiClient {
static SERVER_URL = 'http://localhost:8080';
static GET_CHALLENGE = '/challenges/random';
static POST_RESULT = '/attempts';
static challenge(): Promise<Response> {
return fetch(ApiClient.SERVER_URL + ApiClient.GET_CHALLENGE);
}
static sendGuess(user: string,
a: number,
b: number,
guess: number): Promise<Response> {
return fetch(ApiClient.SERVER_URL + ApiClient.POST_RESULT, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
userAlias: user,
factorA: a,
factorB: b,
guess: guess
})
});
}
}
export default ApiClient;
两个方法都返回Promise,JavaScript中的Promise与Java中的Future类相似:表示异步操作的结果。fetch函数用来与HTTP服务进行交互。
challenge()方法调用fetch函数的基本形式,默认对URL执行GET操作,返回Response对象。
sendGuess()方法接收请求所需的参数,与第二个参数一起使用,定义了HTTP方法(POST)的对象、请求体的内容类型(JSON)和请求体。
最后,为使类可以公开访问,添加了export default ApiClient,就可以将完整的类引入其他组件和类中了。
ChallengeComponent
下面,创建第一个React组件,来处理Challenge域,包括:
- 将后端检索到的数据渲染到ChallengeComponent
- 显示表单供用户发送猜测
下面是ChallengeComponent的代码:
import ApiClient from "../services/ApiClient";
import React from "react";
// 类从React.Component继承,这就是React创建组件的方式。
// 唯一要实现的方法是render(),该方法必须返回DOM元素才能在浏览器中显示。
class ChallengeComponent extends React.Component {
// 构造函数,初始化属性及组件的state(如果需要的话),
// 这里创建一个state来保持检索到的挑战,以及用户为解决尝试而输入的数据。
constructor(props) {
super(props);
this.state = {
a: '',
b: '',
user: '',
message: '',
guess: '',
};
// 两个绑定方法。如果想要在事件处理程序中使用,这是必要的,需要实现这些方法来处理用户输入的数据。
this.handleSubmitResult = this.handleSubmitResult.bind(this);
this.handleChange = this.handleChange.bind(this);
}
// 这是一个生命周期方法,用于首次渲染组件后立即执行逻辑。
componentDidMount(): void {
// 使用ApiClient实用程序类来调用服务,检索挑战。
// 考虑到函数返回一个promise,使用then()来指定获取响应时的操作。
ApiClient.challenge().then(res => {
if (res.ok) {
// 使用then()解析promise,将REST API响应中预期的factorA和factorB传递给setState()。
res.json().then(
json => {
this.setState({
a: json.factorA,
b: json.factorB
});
}
);
} else {
this.updateMessage("Can't reach the server");
}
});
}
handleChange(event) {
const name = event.target.name;
this.setState({
[name]: event.target.value
});
}
handleSubmitResult(event) {
event.preventDefault();
ApiClient.sendGuess(this.state.user,
this.state.a,
this.state.b,
this.state.guess)
.then(res => {
if (res.ok) {
res.json().then(json => {
if (json.correct) {
this.updateMessage("Congratulations! Your guess is correct");
} else {
this.updateMessage("Oops! Your guess " + json.reaultAttempt + " is" +
" wrong, but keep playing!");
}
});
} else {
this.updateMessage("Error: server error or not available");
}
});
}
updateMessage(m: string) {
this.setState({
message: m
});
}
render() {
return (
<div>
<div>
<h3>Your new challenge is</h3>
<h1>
{this.state.a} x {this.state.b}
</h1>
</div>
<form onSubmit={this.handleSubmitResult}>
<label>
Your alias:
<input type="text" maxLength="12" name="user"
value={this.state.user} onChange={this.handleChange}/>
</label>
<br/>
<label>
Your guess:
<input type="number" min="0" name="guess"
value={this.state.guess} onChange={this.handleChange}/>
</label>
<br/>
<input type="submit" value="Submit"/>
</form>
<h4>{this.state.message}</h4>
</div>
);
}
}
export default ChallengeComponent;
组件的主要结构
import ApiClient from "../services/ApiClient";
import React from "react";
// 类从React.Component继承,这就是React创建组件的方式。
// 唯一要实现的方法是render(),该方法必须返回DOM元素才能在浏览器中显示。
class ChallengeComponent extends React.Component {
// 构造函数,初始化属性及组件的state(如果需要的话),
// 这里创建一个state来保持检索到的挑战,以及用户为解决尝试而输入的数据。
constructor(props) {
super(props);
this.state = {
a: '',
b: '',
user: '',
message: '',
guess: '',
};
// 两个绑定方法。如果想要在事件处理程序中使用,这是必要的,需要实现这些方法来处理用户输入的数据。
this.handleSubmitResult = this.handleSubmitResult.bind(this);
this.handleChange = this.handleChange.bind(this);
}
// 这是一个生命周期方法,用于首次渲染组件后立即执行逻辑。
componentDidMount(): void {
// ... Component initialization
}
render() {
return (
// ... HTML as JSX ...
)
}
}
在React中,setState函数重新加载部分DOM。这意味着浏览器将再次渲染HTML被更改的部分,因此收到服务器响应后,会在页面上看到乘法因子。
渲染
JSX可以混合使用HTML和JavaScript。
render() {
return (
<div>
<div>
<h3>Your new challenge is</h3>
<h1>
{this.state.a} x {this.state.b}
</h1>
</div>
<form onSubmit={this.handleSubmitResult}>
<label>
Your alias:
<input type="text" maxLength="12" name="user"
value={this.state.user} onChange={this.handleChange}/>
</label>
<br/>
<label>
Your guess:
<input type="number" min="0" name="guess"
value={this.state.guess} onChange={this.handleChange}/>
</label>
<br/>
<input type="submit" value="Submit"/>
</form>
<h4>{this.state.message}</h4>
</div>
);
}
ChallengeComponent有一个根div元素,包含3个主要代码块。第一个代码块通过state中的两个参数来显示挑战。最后一个代码块,展示message状态属性。第二个代码块创建了一个表单,可以让用户输入自己的猜测。在表单中,涉及相关的处理,用于处理用户输入。
handleChange(event) {
const name = event.target.name;
this.setState({
[name]: event.target.value
});
}
handleSubmitResult(event) {
event.preventDefault();
ApiClient.sendGuess(this.state.user,
this.state.a,
this.state.b,
this.state.guess)
.then(res => {
if (res.ok) {
res.json().then(json => {
if (json.correct) {
this.updateMessage("Congratulations! Your guess is correct");
} else {
this.updateMessage("Oops! Your guess " + json.reaultAttempt + " is" +
" wrong, but keep playing!");
}
});
} else {
this.updateMessage("Error: server error or not available");
}
});
}
表单提交时,调用服务器的API来发送猜测。当获取响应时,检查是否正常,解析JSON,然后,更新状态中的message属性,最后,相应部分的HTML DOM对象会被再次渲染。
与应用程序集成
现在,以及完成了组件的代码,就可以在应用程序中使用了。修改App.js,在其中添加创建的组件。
import React from "react";
import './App.css';
import ChallengeComponent from './components/ChallengeComponent';
function App() {
return (
<div className="App">
<header className="App-header">
<ChallengeComponent/>
</header>
</div>
);
}
export default App;
框架应用程序在index.js中使用此App组件,构建代码时,生成的脚本包含在index.html文件中。
还需要调整App.test.js中包含的测试代码或直接删除。这里不会探讨React测试的细节,现在可以将其删除。
第一次运行前端
确保运行了Spring Boot应用程序,启动控制台,进入前端应用程序的文件夹,执行npm start
命令来启动React前端。
成功编译后,会打开默认浏览器并显示位于localhost:3000的页面,显示如下:
这里出问题了,应该去后端获取参数的,但现在的参数区域空白,下面看看如何进行调试。
调试
大多数浏览器都为开发者提供了功能强大的工具,打开开发者模式,刷新浏览器,就可以查看前端是否与后端正确交互。
上面的网页刷新,在开发者模式下,单击“网络”选项卡,会看到对http://localhost:8080/challenges/random的HTTP请求失败,如图所示:
控制台还显示一条描述性消息:已拦截跨源请求:同源策略禁止读取位于 http://localhost:8080/challenges/random 的远程资源。(原因:CORS 头缺少 'Access-Control-Allow-Origin')。状态码:200。
默认情况下,浏览器会阻止那些尝试访问前端所在域以外的域中的资源的请求,以避免浏览器中的恶意页面访问其他页面中的数据,这称为“同源策略”。本例中,虽然在本地主机中同时运行前端和后端,但是不同的端口,因此被认为是不同源的。
有多种方法可以解决该问题。这里,将启用跨域资源共享(CORS),这是一种可在服务器端启用的安全策略,允许前端使用来自不同源的REST API。
将CORS配置添加到Spring Boot应用
回到后端代码库,添加一个Spring Boot @Configuration类,配置CORS,代码如下:
package cn.zhangjuli.multiplication.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author Juli Zhang, <a href="mailto:zhjl@lut.edu.cn">Contact me</a> <br>
*/
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("http://localhost:3000");
}
}
这里通过CorsRegistry 实例完成工作,添加一个映射,允许前端的源访问任何路径(用/**表示)。也可以省略allowedOrigins部分,这样,所有的源域都可以访问。
注意:当前端和后端部署到不同的主机时,应该提供有选择的CORS配置策略,避免为所有的源域添加完全访问权限。
使用应用程序
现在,前后端可以协同工作了,重新启动前后端应用程序,刷新浏览器,如图所示:
这是一个测试,可以完成猜测游戏了。
部署React应用程序
目前为止,前端一直使用的是开发模式,这不是生产环境的工作方式。
要想部署React应用程序,首先需要构建它,使用npm run build
命令来构建用于生产部署的React应用程序。执行如下:
> npm run build
> multiplication-frontend@0.1.0 build
> react-scripts build
Creating an optimized production build...
One of your dependencies, babel-preset-react-app, is importing the
"@babel/plugin-proposal-private-property-in-object" package without
declaring it in its dependencies. This is currently working because
"@babel/plugin-proposal-private-property-in-object" is already in your
node_modules folder for unrelated reasons, but it may break at any time.
babel-preset-react-app is part of the create-react-app project, which
is not maintianed anymore. It is thus unlikely that this bug will
ever be fixed. Add "@babel/plugin-proposal-private-property-in-object" to
your devDependencies to work around this error. This will make this message
go away.
Compiled successfully.
File sizes after gzip:
47.14 kB build\static\js\main.b3d22150.js
1.79 kB build\static\js\787.444a2e11.chunk.js
515 B build\static\css\main.f855e6bc.css
The project was built assuming it is hosted at /.
You can control this with the homepage field in your package.json.
The build folder is ready to be deployed.
You may serve it with a static server:
serve -s build
Find out more about deployment here:
https://cra.link/deployment
该命令在build文件夹下生成了所有脚本和文件,还可以在其中找到放在public文件夹中的文件的副本。这些日志还说明了如何使用npm安装静态Web服务器。
另外,Spring Boot应用程序已经嵌入了Web服务器Tomcat,也可以直接使用。最简单的方式是,将整个应用程序(前端和后端)打包在同一个可部署单元中:Spring Boot生成的胖JAR文件。需要做的就是,将前端build文件夹中的所有文件复制到Multiplication代码库的src/main/resources/static文件夹中。Spring Boot的默认服务器配置为静态Web文件添加了一些预定义的位置,static文件夹就是其中之一。如图所示:
可根据需要配置这些资源位置及其映射。其中一个需要微调的就是WebMvcConfigurer接口实现。这里不需要改动,因为前端和后端共享同一个源域http://localhost:8080,当然,也可以删除它。
小结
文章介绍了如何基于React创建一个前端Web应用程序的过程。首先,使用create-react-app工具创建React应用程序框架,然后创建一个ApiClient类来实现与后端API服务的访问,并创建一个使用该服务并显示结果的React组件。为了使前后端能够协同,在后端增加了CORS配置。最后,介绍了如何构建用于生产环境的React应用程序,以及如何在Spring Boot的嵌入式Tomcat中集成。