# 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,页面刷新不丢失 - `` vs 编程式导航 `router.push()` - 导航守卫:`router.beforeEach()` 检查登录状态 - HTTP 拦截器:自动附加 `Authorization: Bearer ` **动手 → 理解**: 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:文件上传(头像) **知识点**: - 前端:`` + `FormData` - 后端:`MultipartFile` 接收文件 - Spring Boot 文件上传配置:`spring.servlet.multipart.max-file-size` - 文件存储:本地目录 vs OSS(对象存储) - 文件命名:UUID 避免冲突 - 文件访问:静态资源映射 `addResourceHandlers()` - 预览:上传后在前端显示缩略图 **动手 → 理解**: 1. 在表单中增加 `` 字段 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 入门