242 lines
10 KiB
Markdown
242 lines
10 KiB
Markdown
# Week 7:前后端分离实战
|
||
|
||
**目标**:将 Vue 3 前端与 Spring Boot 后端完整对接,交付前后端分离的学生管理系统 v3。
|
||
|
||
**前置**:完成 Week 5(Spring Boot 后端)和 Week 6(Vue 3 前端基础)。
|
||
|
||
**本周产出**:前后端分离的完整 SPA 应用,含登录注册、JWT 认证、CRUD、分页搜索、角色权限。
|
||
|
||
**启动方式**:
|
||
```bash
|
||
# 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 如何配置
|
||
|
||
**动手 → 理解**:
|
||
1. 阅读 `vite.config.js` 的 `server.proxy` 配置,理解代理原理
|
||
2. 阅读 `SecurityConfig.java` 的 `corsConfigurationSource()` Bean
|
||
3. 在 DevTools Network 面板查看:`/api/students` 请求的远程地址是 5173 还是 8080?
|
||
4. 对比 Week 5 的单体架构和 Week 7 的分离架构图
|
||
5. 把 Vite proxy 注释掉,观察跨域错误(CORS error)
|
||
|
||
**核心文件**:
|
||
- `frontend/vite.config.js` → `server.proxy`
|
||
- `backend/.../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>`
|
||
|
||
**动手 → 理解**:
|
||
1. 打开 DevTools → Application → Local Storage,观察 `wk7_token`、`wk7_username`、`wk7_role`
|
||
2. 在登录页输入错误密码,观察 Network 面板的 401 响应
|
||
3. 在 DevTools 中手动删除 Local Storage 的 token,刷新页面,观察跳转
|
||
4. 阅读 `router/index.js` 的 `beforeEach` 守卫逻辑
|
||
5. 注册一个新账号后,检查数据库中 users 表是否新增记录
|
||
|
||
**核心文件**:
|
||
- `frontend/src/views/LoginView.vue` — 登录页面
|
||
- `frontend/src/views/RegisterView.vue` — 注册页面
|
||
- `frontend/src/stores/auth.js` — 认证状态管理
|
||
- `frontend/src/api/auth.js` — 认证 API
|
||
- `frontend/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 悲观更新(操作后重新拉取数据)
|
||
|
||
**动手 → 理解**:
|
||
1. 用 admin 登录,点击"新增",填写表单提交 → 观察 Network 面板 POST 请求
|
||
2. 点击"编辑",修改成绩后保存 → 观察 PUT 请求的请求体
|
||
3. 点击"删除" → 确认后观察 DELETE 请求,刷新页面确认数据消失
|
||
4. 用 user 登录,观察"新增/编辑/删除"按钮是否隐藏
|
||
5. 用 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 参数中
|
||
|
||
**动手 → 理解**:
|
||
1. 在搜索框输入"张",按回车 → 观察请求 URL 中的 `keyword` 参数
|
||
2. 点击分页组件的"下一页" → 观察 page 参数变化
|
||
3. 打开 `Pagination.vue`,理解省略号(...)的生成逻辑
|
||
4. 修改后端 `StudentMpService.list()` 中的排序字段为 `age` → 观察列表顺序变化
|
||
5. 在 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()`
|
||
- 预览:上传后在前端显示缩略图
|
||
|
||
**动手 → 理解**:
|
||
1. 在表单中增加 `<input type="file">` 字段
|
||
2. 用 `FormData` 封装文件 + JSON 字段,POST 到后端
|
||
3. 检查后端 `StudentController` 中的 `@RequestParam MultipartFile` 处理
|
||
4. 上传一个图片后,浏览器直接访问上传后的 URL
|
||
5. 尝试上传超大文件(>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)
|
||
|
||
**动手 → 理解**:
|
||
1. 强制停止后端,刷新前端页面 → 观察错误状态展示
|
||
2. 删除所有学生数据 → 观察空状态提示
|
||
3. 点击保存按钮时,快速双击 → 观察 loading 状态是否阻止了重复提交
|
||
4. 修改 `StudentListView.vue` 中的 loading 实现为骨架屏
|
||
5. 给搜索框加 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.development` vs `.env.production`
|
||
- Docker 部署概念:前端容器 + 后端容器 + Nginx 容器
|
||
- 部署检查清单:数据库连接、Redis 连接、CORS 配置
|
||
|
||
**动手 → 理解**:
|
||
1. 运行 `npm run build`,观察生成的 `dist/` 目录结构
|
||
2. 用 `npx serve dist/` 预览生产构建
|
||
3. 用 Postman 测试后端所有 API 端点(GET/POST/PUT/DELETE + Token)
|
||
4. 画出完整的请求流程图:浏览器 → Nginx → 前端静态文件 / 后端 API
|
||
5. 阅读 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 入门
|