优化# Spring Boot 3.0 + React 18 + TypeScript 构建现代化全栈应用:从零到一完整指南
引言
在现代Web开发领域,构建高性能、可维护的全栈应用已成为开发者的核心技能。本文将带你从零开始,使用Spring Boot 3.0后端框架配合React 18前端技术栈,结合TypeScript实现现代化全栈应用开发。通过本教程,你将掌握从项目初始化到部署发布的完整开发流程,涵盖API设计、状态管理、安全认证等关键实践。
技术栈概述
Spring Boot 3.0
Spring Boot 3.0是Spring生态系统的重要升级版本,基于Java 17,引入了多项新特性和改进。主要特性包括:
- 基于Java 17的完全支持
- 改进的性能和内存使用
- 更好的容器化支持
- 与Spring Framework 6的深度集成
React 18
React 18带来了革命性的新特性,包括:
- 自动批处理更新
- 新的并发渲染API
- useId、useSyncExternalStore等新Hook
- 更好的错误边界和Suspense支持
TypeScript
TypeScript为JavaScript提供了静态类型检查,大大提升了代码质量和开发体验:
- 类型安全
- 更好的IDE支持
- 代码重构支持
- 降低运行时错误
项目初始化
后端项目创建
使用Spring Initializr创建Spring Boot 3.0项目:
# 使用Spring Boot CLI创建项目
spring init --dependencies=web,data-jpa,h2,security,validation \
--java-version=17 \
--package-name=com.example.demo \
--group-id=com.example \
--artifact-id=backend \
--name=backend
或者使用Spring Boot 3.0的官方起始模板:
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>backend</name>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
前端项目创建
使用Vite创建React 18 + TypeScript项目:
# 创建React项目
npm create vite@latest frontend -- --template react-ts
# 进入项目目录
cd frontend
# 安装依赖
npm install
# 安装额外的开发依赖
npm install -D @types/node @types/react @types/react-dom
后端API设计
数据模型定义
// src/main/java/com/example/demo/model/User.java
package com.example.demo.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")
@Column(unique = true, nullable = false)
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
@Column(unique = true, nullable = false)
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码长度不能少于6个字符")
@Column(nullable = false)
private String password;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// 构造函数
public User() {}
public User(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
// Getter和Setter方法
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}
数据访问层
// src/main/java/com/example/demo/repository/UserRepository.java
package com.example.demo.repository;
import com.example.demo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
@Query("SELECT u FROM User u WHERE u.username = :username OR u.email = :email")
Optional<User> findByUsernameOrEmail(@Param("username") String username, @Param("email") String email);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}
服务层实现
// src/main/java/com/example/demo/service/UserService.java
package com.example.demo.service;
import com.example.demo.exception.ResourceNotFoundException;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> getAllUsers() {
return userRepository.findAll();
}
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("用户不存在,ID: " + id));
}
public User createUser(User user) {
if (userRepository.existsByUsername(user.getUsername())) {
throw new RuntimeException("用户名已存在: " + user.getUsername());
}
if (userRepository.existsByEmail(user.getEmail())) {
throw new RuntimeException("邮箱已存在: " + user.getEmail());
}
return userRepository.save(user);
}
public User updateUser(Long id, User userDetails) {
User user = getUserById(id);
// 检查用户名是否被其他用户使用
if (userDetails.getUsername() != null &&
!userDetails.getUsername().equals(user.getUsername()) &&
userRepository.existsByUsername(userDetails.getUsername())) {
throw new RuntimeException("用户名已存在: " + userDetails.getUsername());
}
// 检查邮箱是否被其他用户使用
if (userDetails.getEmail() != null &&
!userDetails.getEmail().equals(user.getEmail()) &&
userRepository.existsByEmail(userDetails.getEmail())) {
throw new RuntimeException("邮箱已存在: " + userDetails.getEmail());
}
user.setUsername(userDetails.getUsername() != null ? userDetails.getUsername() : user.getUsername());
user.setEmail(userDetails.getEmail() != null ? userDetails.getEmail() : user.getEmail());
return userRepository.save(user);
}
public void deleteUser(Long id) {
User user = getUserById(id);
userRepository.delete(user);
}
public Optional<User> findByUsername(String username) {
return userRepository.findByUsername(username);
}
public Optional<User> findByEmail(String email) {
return userRepository.findByEmail(email);
}
}
控制器层
// src/main/java/com/example/demo/controller/UserController.java
package com.example.demo.controller;
import com.example.demo.exception.ResourceNotFoundException;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "*")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userService.getAllUsers();
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
try {
User createdUser = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
} catch (RuntimeException e) {
return ResponseEntity.badRequest().build();
}
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User userDetails) {
try {
User updatedUser = userService.updateUser(id, userDetails);
return ResponseEntity.ok(updatedUser);
} catch (ResourceNotFoundException e) {
return ResponseEntity.notFound().build();
} catch (RuntimeException e) {
return ResponseEntity.badRequest().build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
try {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
} catch (ResourceNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
@GetMapping("/username/{username}")
public ResponseEntity<User> getUserByUsername(@PathVariable String username) {
Optional<User> user = userService.findByUsername(username);
return user.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/email/{email}")
public ResponseEntity<User> getUserByEmail(@PathVariable String email) {
Optional<User> user = userService.findByEmail(email);
return user.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}
前端应用开发
项目结构
frontend/
├── src/
│ ├── components/
│ │ ├── UserList/
│ │ ├── UserForm/
│ │ └── Header/
│ ├── services/
│ │ └── userService.ts
│ ├── types/
│ │ └── user.ts
│ ├── App.tsx
│ ├── main.tsx
│ └── index.css
├── public/
└── vite.config.ts
类型定义
// src/types/user.ts
export interface User {
id?: number;
username: string;
email: string;
password?: string;
createdAt?: string;
updatedAt?: string;
}
export interface UserFormProps {
user?: User;
onSubmit: (user: User) => void;
onCancel: () => void;
}
API服务层
// src/services/userService.ts
import { User } from '../types/user';
const API_BASE_URL = 'http://localhost:8080/api/users';
export const userService = {
// 获取所有用户
getAllUsers: async (): Promise<User[]> => {
try {
const response = await fetch(API_BASE_URL);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
} catch (error) {
console.error('获取用户列表失败:', error);
throw error;
}
},
// 根据ID获取用户
getUserById: async (id: number): Promise<User> => {
try {
const response = await fetch(`${API_BASE_URL}/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
} catch (error) {
console.error('获取用户失败:', error);
throw error;
}
},
// 创建用户
createUser: async (user: Omit<User, 'id'>): Promise<User> => {
try {
const response = await fetch(API_BASE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(user),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || '创建用户失败');
}
return response.json();
} catch (error) {
console.error('创建用户失败:', error);
throw error;
}
},
// 更新用户
updateUser: async (id: number, user: Partial<User>): Promise<User> => {
try {
const response = await fetch(`${API_BASE_URL}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(user),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || '更新用户失败');
}
return response.json();
} catch (error) {
console.error('更新用户失败:', error);
throw error;
}
},
// 删除用户
deleteUser: async (id: number): Promise<void> => {
try {
const response = await fetch(`${API_BASE_URL}/${id}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
console.error('删除用户失败:', error);
throw error;
}
},
};
用户列表组件
// src/components/UserList/UserList.tsx
import React, { useState, useEffect } from 'react';
import { User } from '../../types/user';
import { userService } from '../../services/userService';
const UserList: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
try {
setLoading(true);
const fetchedUsers = await userService.getAllUsers();
setUsers(fetchedUsers);
setError(null);
} catch (err) {
setError('获取用户列表失败');
console.error('获取用户列表失败:', err);
} finally {
setLoading(false);
}
};
const handleDelete = async (id: number) => {
if (window.confirm('确定要删除这个用户吗?')) {
try {
await userService.deleteUser(id);
// 从列表中移除已删除的用户
setUsers(users.filter(user => user.id !== id));
} catch (err) {
setError('删除用户失败');
console.error('删除用户失败:', err);
}
}
};
if (loading) {
return <div className="loading">加载中...</div>;
}
if (error) {
return <div className="error">{error}</div>;
}
return (
<div className="user-list">
<h2>用户列表</h2>
<table className="users-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.username}</td>
<td>{user.email}</td>
<td>{new Date(user.createdAt || '').toLocaleString()}</td>
<td>
<button
onClick={() => handleDelete(user.id!)}
className="delete-btn"
>
删除
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default UserList;
用户表单组件
// src/components/UserForm/UserForm.tsx
import React, { useState, useEffect } from 'react';
import { User, UserFormProps } from '../../types/user';
import { userService } from '../../services/userService';
const UserForm: React.FC<UserFormProps> = ({ user, onSubmit, onCancel }) => {
const [formData, setFormData] = useState<User>({
username: '',
email: '',
password: '',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
useEffect(() => {
if (user) {
setFormData(user);
}
}, [user]);
const validate = (): boolean => {
const newErrors: Record<string, string> = {};
if (!formData.username.trim()) {
newErrors.username = '用户名不能为空';
} else if (formData.username.length < 3) {
newErrors.username = '用户名至少需要3个字符';
}
if (!formData.email.trim()) {
newErrors.email = '邮箱不能为空';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = '邮箱格式不正确';
}
if (!formData.password && !user) {
newErrors.password = '密码不能为空';
} else if (formData.password && formData.password.length < 6) {
newErrors.password = '密码至少需要6个字符';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// 清除对应字段的错误
if (errors[name]) {
setErrors(prev => {
const newErrors = { ...prev };
delete newErrors[name];
return newErrors;
});
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validate()) {
return;
}
setIsSubmitting(true);
try {
if (user && user.id) {
// 更新用户
const updatedUser = await userService.updateUser(user.id, formData);
onSubmit(updatedUser);
} else {
// 创建用户
const createdUser = await userService.createUser(formData);
onSubmit(createdUser);
}
} catch (error) {
console.error('提交表单失败:', error);
alert('操作失败,请重试');
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit} className="user-form">
<div className="form-group">
<label htmlFor="username">用户名:</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
className={errors.username ? 'error' : ''}
/>
{errors.username && <span className="error-message">{errors.username}</span>}
</div>
<div className="form-group">
<label htmlFor="email">邮箱:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>
<div className="form-group">
<label htmlFor="password">密码:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
className={errors.password ? 'error' : ''}
/>
{errors.password && <span className="error-message">{errors.password}</span>}
</div>
<div className="form-actions">
<button type="submit" disabled={isSubmitting} className="submit-btn">
{isSubmitting ? '提交中...' : user ? '更新用户' : '创建用户'}
</button>
<button type="button" onClick={onCancel} className="cancel-btn">
取消
</button>
</div>
</form>
);
};
export default UserForm;
主应用组件
// src/App.tsx
import React, { useState, useEffect } from 'react';
import UserList from './components/UserList/UserList';
import UserForm from './components/UserForm/UserForm';
import { User } from './types/user';
import { userService } from './services/userService';
import './App.css';
function App() {
const [showForm, setShowForm] = useState<boolean>(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
try {
const fetchedUsers = await userService.getAllUsers();
setUsers(fetchedUsers);
} catch (error) {
console.error('获取用户列表失败:', error);
}
};
const handleAddUser = () => {
setEditingUser(null);
setShowForm(true);
};
const handleEditUser = (user: User) => {
setEditingUser(user);
setShowForm(true);
};
const handleSaveUser = (savedUser: User) => {
if (editingUser) {
// 更新用户
setUsers(users.map(user => user.id === savedUser.id ? savedUser : user));
} else {
// 添加新用户
setUsers([...users, savedUser]);
}
setShowForm(false);
setEditingUser(null);
};
const handleCancel = () => {
setShowForm(false);
setEditingUser(null);
};
return (
<div className="App">
<header className="app-header">
<h1>现代化全栈应用</h1>
<button onClick={handleAddUser} className="add-user-btn">
添加用户
</button>
</header>
<main className="app-main">
{showForm ? (
<UserForm
user={editingUser || undefined}
onSubmit={handleSaveUser}
onCancel={handleCancel}
/>
) : (
<UserList />
)}
</main>
</div>
);
}
export default App;
状态管理
使用Context API进行状态管理
// src/context/UserContext.tsx
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
import { User } from '../types/user';
interface UserState {
users: User[];
loading: boolean;
error: string | null;
}
interface UserAction {
type: 'FETCH_USERS_START' | 'FETCH_USERS_SUCCESS' | 'FETCH_USERS_ERROR' |
'CREATE_USER_SUCCESS' | 'UPDATE_USER_SUCCESS' | 'DELETE_USER_SUCCESS';
payload?: any;
}
const initialState: UserState = {
users: [],
loading: false,
error: null,
};
const userReducer = (state: UserState, action: UserAction): UserState => {
switch (action.type) {
case 'FETCH_USERS_START':
return { ...state, loading: true, error: null };
case 'FETCH_USERS_SUCCESS':
return { ...state, loading: false, users: action.payload, error: null };
case 'FETCH_USERS_ERROR':
return { ...state, loading: false, error: action.payload };
case 'CREATE_USER_SUCCESS':
return { ...state, users: [...state.users, action.payload] };
case 'UPDATE_USER_SUCCESS':
return {
...state,
users: state.users.map(user =>
user.id === action.payload.id ? action.payload : user
)
};
case 'DELETE_USER_SUCCESS':
return {
...state,
users: state.users.filter(user => user.id !== action.payload)
};
default:
return state;
}
};
interface UserContextType {
state: UserState;
dispatch: React.Dispatch<UserAction>;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
export const UserProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(userReducer, initialState);
return (
<UserContext.Provider value={{ state, dispatch }}>
{children}
</UserContext.Provider>
);
};
export const useUserContext = () => {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUserContext must be used within a UserProvider');
}
return context;
};
安全认证
JWT认证实现
// src/main/java/com/example/demo/config/JwtAuthenticationFilter.java
package com.example.demo.config;
import com.example.demo.service.JwtService;
import com.example.demo.service.UserService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
评论 (0)