Vue 3 + Spring Boot 前后端数据交互
·
1. 架构概述
在现代 Web 应用开发中,前后端分离架构已成为主流。本文将详细介绍如何在 Vue 3 前端与 Spring Boot 后端之间实现高效、安全的数据交互。
1.1 技术栈
| 层级 | 技术 | 说明 |
|---|---|---|
| 前端 | Vue 3 + Axios | 响应式框架 + HTTP 客户端 |
| 后端 | Spring Boot 3.x | Java 企业级开发框架 |
| 数据层 | JPA + MySQL | 对象关系映射 + 关系型数据库 |
| 安全 | JWT | JSON Web Token 认证 |
1.2 数据流向
┌─────────────────────────────────────────────────────────────────┐
│ 前端 (Vue 3) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ 组件/页面 │ │ Service 层 │ │ Axios 请求封装 │ │
│ │ 用户交互 │──▶│ 业务逻辑 │──▶│ API 路径管理 │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
│ HTTP/REST + JWT
▼
┌─────────────────────────────────────────────────────────────────┐
│ 后端 (Spring Boot) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Controller │ │ Service │ │ Repository │ │
│ │ 接口定义 │──▶│ 业务逻辑 │──▶│ 数据访问层 │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 数据库 (MySQL) │
└─────────────────────────────────────────────────────────────────┘
2. 前端数据层设计
2.1 Axios 实例配置
前端使用 Axios 进行 HTTP 请求,通过拦截器实现统一的认证和错误处理。
import axios from 'axios'
// 创建 axios 实例
const api = axios.create({
baseURL: 'http://localhost:8080', // 后端 API 基础路径
timeout: 10000, // 请求超时时间
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器 - 自动添加认证 Token
api.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => Promise.reject(error)
)
// 响应拦截器 - 统一错误处理
api.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 401) {
// Token 过期,清除登录状态并跳转
localStorage.clear()
window.location.href = '/'
}
return Promise.reject(error)
}
)
export default api
关键点说明:
baseURL统一配置后端地址,便于环境切换- 请求拦截器自动从
localStorage获取 JWT Token 并添加到请求头 - 响应拦截器处理 401 未授权状态,自动跳转登录页
2.2 API 路径集中管理
将所有 API 路径统一管理,便于维护和修改。
const API_BASE = '/api/v1';
const apiPaths = {
// 用户相关
user: {
login: `${API_BASE}/users/login`,
register: `${API_BASE}/users/register`,
info: `${API_BASE}/users/info`,
update: `${API_BASE}/users/update`,
},
// 饮食记录
food: {
records: `${API_BASE}/food-records`,
summary: `${API_BASE}/food-record/summary`,
favorite: (id) => `${API_BASE}/food-records/${id}/favorite`
},
// 运动记录
exercise: {
records: `${API_BASE}/workouts`,
summary: `${API_BASE}/workout/summary`,
},
// 睡眠记录
sleep: {
records: `${API_BASE}/sleep-records`,
summary: `${API_BASE}/sleep-record/summary`,
},
}
export default apiPaths
2.3 Service 层封装
按业务模块封装 API 调用方法,实现代码复用。
import api from './api'
import apiPaths from '../utils/apiPaths'
const healthService = {
// 饮食记录模块
food: {
// 获取列表
getRecords: async (username, params) => {
return api.get(`${apiPaths.food.records}?username=${username}`, { params })
},
// 创建记录
createRecord: async (record) => {
return api.post(apiPaths.food.records, record)
},
// 更新记录
updateRecord: async (recordId, record) => {
return api.put(`${apiPaths.food.records}/${recordId}`, record)
},
// 删除记录
deleteRecord: async (recordId) => {
return api.delete(`${apiPaths.food.records}/${recordId}`)
},
// 切换收藏状态
toggleFavorite: async (recordId, favorited) => {
return api.post(apiPaths.food.favorite(recordId), { favorited })
},
// 获取统计数据
getSummary: async (username, startDate, endDate) => {
return api.get(apiPaths.food.summary, {
params: { username, startDate, endDate }
})
}
},
// 运动记录模块
exercise: {
getRecords: async (username, params) => {
return api.get(`${apiPaths.exercise.records}?username=${username}`, { params })
},
createRecord: async (record) => {
return api.post(apiPaths.exercise.records, record)
},
// ... 其他方法
}
}
export default healthService
3. 后端数据层设计
3.1 实体类定义 (Entity)
使用 JPA 注解定义数据库实体,建立对象关系映射。
@Entity
@Table(indexes = {
@Index(name = "idx_user_username", columnList = "username")
})
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String email;
// 一对一关系:用户头像
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore
private UserAvatar avatar;
// 一对多关系:用户相册
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore
private List<UserPhoto> photos;
// 用户基本信息字段
private String title;
private String gender;
private Double height;
private Double weight;
private String birthDate;
private String city;
private String bio;
// Getters and Setters
}
关键点说明:
@Entity标记为 JPA 实体类@Table(indexes = ...)创建数据库索引优化查询@OneToOne/@OneToMany定义实体关联关系@JsonIgnore防止 JSON 序列化时的循环引用
3.2 数据访问层 (Repository)
继承 JpaRepository,获得基础的 CRUD 能力,同时定义自定义查询方法。
public interface FoodRecordRepository extends JpaRepository<FoodRecord, Long> {
// 根据用户名查询记录
List<FoodRecord> findByUsername(String username);
// 自定义 JPQL 查询:按日期范围查询
@Query("SELECT f FROM FoodRecord f WHERE f.username = :username " +
"AND f.recordTime >= :startDate AND f.recordTime < :endDate")
List<FoodRecord> findByUsernameAndRecordDatePrefix(
@Param("username") String username,
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate
);
// 按时间范围查询
List<FoodRecord> findByUsernameAndRecordTimeBetween(
String username,
LocalDateTime startDate,
LocalDateTime endDate
);
// 级联删除
@Transactional
void deleteByUsername(String username);
}
3.3 控制器层 (Controller)
定义 RESTful API 接口,处理 HTTP 请求和响应。
@RestController
@RequestMapping("/api/v1/food-records")
public class FoodRecordController {
@Autowired
private FoodRecordService foodRecordService;
// 创建记录(支持文件上传)
@PostMapping
public Map<String, Object> addFoodRecord(
@RequestParam("mealType") String mealType,
@RequestParam("recordTime") String recordTime,
@RequestParam("calories") String calories,
@RequestParam("notes") String notes,
@RequestParam("username") String username,
@RequestParam("foodItems") String foodItemsJson,
@RequestParam(value = "photos", required = false) MultipartFile[] photos) {
FoodRecord foodRecord = new FoodRecord();
foodRecord.setMealType(mealType);
foodRecord.setRecordTime(LocalDateTime.parse(recordTime));
foodRecord.setCalories(calories);
foodRecord.setNotes(notes);
foodRecord.setUsername(username);
// 处理 JSON 格式的食物项数据
if (foodItemsJson != null && !foodItemsJson.isEmpty()) {
ObjectMapper objectMapper = new ObjectMapper();
List<FoodItem> foodItems = objectMapper.readValue(
foodItemsJson,
new TypeReference<List<FoodItem>>() {}
);
foodRecord.setFoodItems(foodItems);
}
// 处理图片上传
if (photos != null && photos.length > 0) {
List<FoodPhoto> foodPhotos = new ArrayList<>();
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
FoodPhoto foodPhoto = new FoodPhoto();
foodPhoto.setFoodRecord(foodRecord);
foodPhoto.setPhotoData(photo.getBytes());
foodPhoto.setFilename(photo.getOriginalFilename());
foodPhoto.setContentType(photo.getContentType());
foodPhotos.add(foodPhoto);
}
}
foodRecord.setPhotos(foodPhotos);
}
FoodRecord savedRecord = foodRecordService.addFoodRecord(foodRecord);
return convertFoodRecord(savedRecord);
}
// 查询记录列表
@GetMapping
public List<Map<String, Object>> getFoodRecords(
@RequestParam("username") String username,
@RequestParam(value = "date", required = false) String date) {
List<FoodRecord> records;
if (date != null) {
records = foodRecordService.getFoodRecordsByUsernameAndDate(username, date);
} else {
records = foodRecordService.getFoodRecordsByUsername(username);
}
return records.stream()
.map(this::convertFoodRecord)
.collect(Collectors.toList());
}
// 更新记录
@PutMapping
public Map<String, Object> updateFoodRecord(
@RequestParam("id") Long id,
@RequestParam("mealType") String mealType,
// ... 其他参数
) {
FoodRecord existingRecord = foodRecordService.getFoodRecordById(id);
// 更新逻辑...
FoodRecord updatedRecord = foodRecordService.updateFoodRecord(foodRecord);
return convertFoodRecord(updatedRecord);
}
// 删除记录
@DeleteMapping("/{id}")
public void deleteFoodRecord(@PathVariable("id") Long id) {
foodRecordService.deleteFoodRecord(id);
}
// 切换收藏状态
@PostMapping("/{id}/favorite")
public Map<String, Object> toggleFavorite(
@PathVariable("id") Long id,
@RequestBody FavoriteRequest request) {
FoodRecord foodRecord = foodRecordService.getFoodRecordById(id);
foodRecord.setFavorited(request.isFavorited());
FoodRecord updatedRecord = foodRecordService.updateFoodRecord(foodRecord);
return convertFoodRecord(updatedRecord);
}
// 批量删除
@DeleteMapping("/batch-delete")
public void batchDeleteFoodRecords(@RequestBody List<Long> ids) {
foodRecordService.batchDeleteFoodRecords(ids);
}
}
4. 数据交互流程
4.1 用户注册流程
1. 前端收集表单数据
{
"username": "zhangsan",
"password": "password123",
"email": "zhangsan@example.com",
"captcha": "a3f8"
}
2. 调用 healthService.user.register(data)
3. Axios 发送 POST 请求到 /api/v1/users/register
4. 后端 UserController 接收请求
- 验证验证码
- 调用 userService.register(user)
- 生成 JWT Token
- 返回用户信息和 Token
5. 前端存储 Token
localStorage.setItem('token', response.token)
4.2 数据查询流程
1. 前端组件加载时调用
healthService.food.getRecords('zhangsan', { date: '2024-01-15' })
2. Axios 发送 GET 请求
GET /api/v1/food-records?username=zhangsan&date=2024-01-15
Headers: Authorization: Bearer {token}
3. JwtFilter 拦截请求,验证 Token 有效性
4. FoodRecordController 处理请求
- 调用 foodRecordService.getFoodRecordsByUsernameAndDate()
- Repository 执行数据库查询
- 返回记录列表
5. 前端接收数据并渲染到页面
4.3 文件上传流程
1. 前端选择文件后创建 FormData
const formData = new FormData()
formData.append('mealType', '早餐')
formData.append('recordTime', '2024-01-15T08:30')
formData.append('photos', file)
2. Axios 发送 POST 请求(Content-Type: multipart/form-data)
3. 后端使用 @RequestParam 接收参数
- @RequestParam("photos") MultipartFile[] photos
4. 处理文件数据并保存到数据库
5. 数据格式转换
5.1 后端实体转前端 JSON
private Map<String, Object> convertFoodRecord(FoodRecord record) {
Map<String, Object> result = new HashMap<>();
result.put("id", record.getId());
result.put("username", record.getUsername());
result.put("mealType", record.getMealType());
result.put("recordTime", record.getRecordTime());
result.put("calories", record.getCalories());
result.put("notes", record.getNotes());
result.put("favorited", record.isFavorited());
result.put("foodItems", record.getFoodItems());
// 图片数据 Base64 编码
if (record.getPhotos() != null && !record.getPhotos().isEmpty()) {
List<String> photoUrls = record.getPhotos().stream()
.map(photo -> "data:image/jpeg;base64," +
Base64.getEncoder().encodeToString(photo.getPhotoData()))
.collect(Collectors.toList());
result.put("photos", photoUrls);
}
return result;
}
5.2 前端使用示例
<template>
<div class="food-records">
<div v-for="record in records" :key="record.id" class="record-card">
<h3>{{ record.mealType }} - {{ formatDate(record.recordTime) }}</h3>
<p>卡路里: {{ record.calories }}</p>
<p>备注: {{ record.notes }}</p>
<!-- 显示图片 -->
<div v-if="record.photos" class="photos">
<img v-for="(photo, index) in record.photos"
:key="index"
:src="photo"
alt="食物照片" />
</div>
<button @click="toggleFavorite(record)">
{{ record.favorited ? '取消收藏' : '收藏' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import healthService from '@/services/healthService'
const records = ref([])
const loadRecords = async () => {
const username = localStorage.getItem('username')
records.value = await healthService.food.getRecords(username)
}
const toggleFavorite = async (record) => {
await healthService.food.toggleFavorite(record.id, !record.favorited)
record.favorited = !record.favorited
}
onMounted(loadRecords)
</script>
6. 最佳实践
6.1 错误处理
// 统一错误处理
api.interceptors.response.use(
response => response.data,
error => {
const { response } = error
switch (response?.status) {
case 400:
// 请求参数错误
console.error('请求参数错误:', response.data.message)
break
case 401:
// 未授权,清除登录状态
localStorage.clear()
window.location.href = '/'
break
case 403:
// 禁止访问
console.error('没有权限执行此操作')
break
case 404:
// 资源不存在
console.error('请求的资源不存在')
break
case 500:
// 服务器错误
console.error('服务器内部错误')
break
default:
console.error('网络请求失败:', error.message)
}
return Promise.reject(error)
}
)
6.2 请求取消
// 用于长时间运行的请求(如 AI 分析)
import axios from 'axios'
const CancelToken = axios.CancelToken
let cancel
const generateAnalysis = async (params) => {
try {
const response = await api.post('/api/v1/ai-analysis/generate', null, {
params,
timeout: 120000, // 120秒超时
cancelToken: new CancelToken(c => { cancel = c })
})
return response
} catch (error) {
if (axios.isCancel(error)) {
console.log('请求已取消')
}
throw error
}
}
// 取消请求
cancel && cancel('用户取消操作')
6.3 数据校验
// 后端使用 Bean Validation
public class UserRegisterRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
private String username;
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^[a-zA-Z0-9@_]+$", message = "密码只能包含字母、数字、@和_")
private String password;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}
// Controller 中使用 @Valid 注解
@PostMapping("/register")
public UserResponse register(@Valid @RequestBody UserRegisterRequest request) {
// 如果校验失败,自动返回 400 错误
}
更多推荐



所有评论(0)