10 KiB
10 KiB
Week 7:前后端分离实战
目标:将 Vue 3 前端与 Spring Boot 后端完整对接,交付前后端分离的学生管理系统 v3。
前置:完成 Week 5(Spring Boot 后端)和 Week 6(Vue 3 前端基础)。
本周产出:前后端分离的完整 SPA 应用,含登录注册、JWT 认证、CRUD、分页搜索、角色权限。
启动方式:
# 1. 启动后端(IDEA 中运行 Week7Application.java)
# 2. 启动前端
cd week7/frontend
npm install # 首次运行
npm run dev # http://localhost:5173
Day 1:前后端分离架构设计
知识点:
- 前后端分离 vs 单体架构:各自优劣
- 前端:Vite 开发服务器(5173)→ 用户浏览器渲染
- 后端:Spring Boot(8080)→ 只返回 JSON,不渲染 HTML
- 通信方式:HTTP + JSON,JWT 无状态认证
- Vite proxy:开发环境将
/api请求代理到后端(解决跨域) - 生产环境:Nginx 反向代理,统一域名/端口
- CORS(Cross-Origin Resource Sharing):Spring Security 如何配置
动手 → 理解:
- 阅读
vite.config.js的server.proxy配置,理解代理原理 - 阅读
SecurityConfig.java的corsConfigurationSource()Bean - 在 DevTools Network 面板查看:
/api/students请求的远程地址是 5173 还是 8080? - 对比 Week 5 的单体架构和 Week 7 的分离架构图
- 把 Vite proxy 注释掉,观察跨域错误(CORS error)
核心文件:
frontend/vite.config.js→server.proxybackend/.../config/SecurityConfig.java→ CORS 配置frontend/src/api/http.js→ Axios 实例
思考题:前后端分离的核心优势是什么?有什么代价(首次加载速度、SEO)?
Day 2:登录注册页面 + JWT 对接
知识点:
- 登录流程:表单输入 → Axios POST → 后端验证 → 返回 JWT → Pinia store 保存 → 跳转首页
- 注册流程:表单输入 → POST → 后端创建用户 → 返回 JWT → 自动登录
- Pinia Auth Store:
token、username、role状态管理 localStorage:持久化 Token,页面刷新不丢失<router-link>vs 编程式导航router.push()- 导航守卫:
router.beforeEach()检查登录状态 - HTTP 拦截器:自动附加
Authorization: Bearer <token>
动手 → 理解:
- 打开 DevTools → Application → Local Storage,观察
wk7_token、wk7_username、wk7_role - 在登录页输入错误密码,观察 Network 面板的 401 响应
- 在 DevTools 中手动删除 Local Storage 的 token,刷新页面,观察跳转
- 阅读
router/index.js的beforeEach守卫逻辑 - 注册一个新账号后,检查数据库中 users 表是否新增记录
核心文件:
frontend/src/views/LoginView.vue— 登录页面frontend/src/views/RegisterView.vue— 注册页面frontend/src/stores/auth.js— 认证状态管理frontend/src/api/auth.js— 认证 APIfrontend/src/router/index.js— 导航守卫
思考题:为什么 Token 存在 localStorage 而不是 Pinia store 中?Pinia store 刷新后会丢失吗?
Day 3:CRUD 页面实现
知识点:
- 列表渲染:
v-for+ Axios GET 请求 - 新增/编辑弹窗:
v-if控制显示,表单双向绑定v-model - 表单校验:HTML5 原生校验 + 自定义 JS 校验
- 删除确认:
confirm()+ DELETE 请求 - 区分新增和编辑:根据是否传入 student 对象
- 角色权限:
v-if="auth.isAdmin"控制按钮显示 - 乐观更新 vs 悲观更新(操作后重新拉取数据)
动手 → 理解:
- 用 admin 登录,点击"新增",填写表单提交 → 观察 Network 面板 POST 请求
- 点击"编辑",修改成绩后保存 → 观察 PUT 请求的请求体
- 点击"删除" → 确认后观察 DELETE 请求,刷新页面确认数据消失
- 用 user 登录,观察"新增/编辑/删除"按钮是否隐藏
- 用 user 登录,在浏览器控制台执行
fetch('/api/students', {method:'POST'})看 403
核心文件:
frontend/src/views/StudentListView.vue— 列表 + 表格frontend/src/components/StudentForm.vue— 表单弹窗(props/emits)frontend/src/api/student.js— CRUD API 调用
思考题:为什么不直接在表格行内编辑,而是用弹窗?各自适用什么场景?
Day 4:分页、搜索、排序
知识点:
- 分页参数:
page(0-based)、size(每页数量) - 前端分页 vs 后端分页:数据量决定策略
- 分页组件:首页/上一页/页码/下一页/末页 + 省略号逻辑
- 搜索防抖:
@keyup.enter触发搜索(避免每次按键都请求) - 排序:后端
ORDER BY实现(MP LambdaQueryWrapper / JPA Sort) - URL 状态同步:搜索关键词和页码是否反映在 URL 参数中
动手 → 理解:
- 在搜索框输入"张",按回车 → 观察请求 URL 中的
keyword参数 - 点击分页组件的"下一页" → 观察 page 参数变化
- 打开
Pagination.vue,理解省略号(...)的生成逻辑 - 修改后端
StudentMpService.list()中的排序字段为age→ 观察列表顺序变化 - 在 Network 面板中对比:搜索前后 total 数量的变化
核心文件:
frontend/src/components/Pagination.vue— 分页组件frontend/src/views/StudentListView.vue→search()/goPage()方法backend/.../service/mp/StudentMpService.java→ LambdaQueryWrapper 排序
思考题:为什么页码从 0 开始(Spring Data Pageable),而不是从 1 开始?
Day 5:文件上传(头像)
知识点:
- 前端:
<input type="file">+FormData - 后端:
MultipartFile接收文件 - Spring Boot 文件上传配置:
spring.servlet.multipart.max-file-size - 文件存储:本地目录 vs OSS(对象存储)
- 文件命名:UUID 避免冲突
- 文件访问:静态资源映射
addResourceHandlers() - 预览:上传后在前端显示缩略图
动手 → 理解:
- 在表单中增加
<input type="file">字段 - 用
FormData封装文件 + JSON 字段,POST 到后端 - 检查后端
StudentController中的@RequestParam MultipartFile处理 - 上传一个图片后,浏览器直接访问上传后的 URL
- 尝试上传超大文件(>10MB),观察
MaxUploadSizeExceededException
核心文件:
frontend/src/components/StudentForm.vue→ 文件输入backend/.../controller/StudentController.java→ MultipartFile 处理backend/.../application.yml→ multipart 配置
思考题:为什么不在数据库中存储文件二进制数据(BLOB),而是存储文件路径?
Day 6:交互体验完善
知识点:
- Loading 状态:数据加载中显示 spinner/骨架屏
- 错误状态:网络异常、后端报错时的用户提示
- 空状态:无数据时的友好占位
- 按钮 Loading:提交时禁用按钮 + 显示"保存中..."
- Toast 消息:操作成功/失败的轻提示
- 乐观更新:先更新 UI 再发请求(失败时回滚)
- 防抖(debounce)和节流(throttle)
动手 → 理解:
- 强制停止后端,刷新前端页面 → 观察错误状态展示
- 删除所有学生数据 → 观察空状态提示
- 点击保存按钮时,快速双击 → 观察 loading 状态是否阻止了重复提交
- 修改
StudentListView.vue中的 loading 实现为骨架屏 - 给搜索框加 300ms 的防抖
核心文件:
frontend/src/views/StudentListView.vue→ loading / error / empty 三种状态frontend/src/components/StudentForm.vue→formLoading按钮状态
思考题:Loading、Empty、Error 三种状态分别应该在哪里处理(组件内 vs 全局)?
Day 7:前后端联调 & 部署概念
知识点:
- 前后端联调:同时启动两个项目,确认所有接口正常
- Vite build:生产环境构建 → 输出
dist/目录 - Nginx 部署:反向代理将前后端统一到 80 端口
- Nginx 配置示例:
location /api { proxy_pass http://localhost:8080; }+location / { root dist/; } - 环境变量:
.env.developmentvs.env.production - Docker 部署概念:前端容器 + 后端容器 + Nginx 容器
- 部署检查清单:数据库连接、Redis 连接、CORS 配置
动手 → 理解:
- 运行
npm run build,观察生成的dist/目录结构 - 用
npx serve dist/预览生产构建 - 用 Postman 测试后端所有 API 端点(GET/POST/PUT/DELETE + Token)
- 画出完整的请求流程图:浏览器 → Nginx → 前端静态文件 / 后端 API
- 阅读 Nginx 反向代理配置示例
核心概念:
┌─────────────┐
│ 浏览器 │
│ localhost:80 │
└──────┬───────┘
│
┌──────┴───────┐
│ Nginx │
│ 端口 80 │
└──┬────────┬───┘
│ │
/ │ │ /api
│ │
┌─────┴──┐ ┌─────┴─────┐
│ dist/ │ │ Spring │
│ 静态 │ │ Boot:8080 │
└────────┘ └───────────┘
思考题:Vite proxy 只用于开发环境,为什么生产环境要用 Nginx 而不是继续用 Vite proxy?
Week 7 总结
| 维度 | Week 5(单体) | Week 6(纯前端) | Week 7(分离) |
|---|---|---|---|
| 前端框架 | 原生 HTML/JS | Vue 3 独立 | Vue 3 + 后端对接 |
| 页面路由 | 无 | Vue Router | Vue Router + 导航守卫 |
| 状态管理 | localStorage | Pinia 独立 | Pinia + API 同步 |
| 认证 | Login 页面(display 切换) | 模拟登录 | JWT 真实认证 |
| 数据 | fetch 直连 | 无后端 | Axios + 拦截器 |
| 部署 | Spring Boot 单端口 | Vite dev | 前后端分端口 / Nginx |
下一阶段:Week 8 — 工程化能力
- JUnit 5 + Mockito 单元测试
- Testcontainers 集成测试
- Knife4j API 文档
- Docker 容器化
- Git 工作流
- CI/CD 入门