13 KiB
第四阶段教案:前端基础 + 项目整合(第 4 周)
学习周期:7 天 每日用时:2-3 小时 里程碑:学生管理系统 v1 — Spring Boot + JPA/MP 双轨 + 原生 JS 全栈应用
前置准备
- 执行
sql/init.sql初始化数据库(在你的 MySQL 中) - IDEA 打开
week4/pom.xml - 确认
application.yml中数据库密码正确
第 1 天:JavaScript 基础 —— 让页面"动"起来
核心概念
| 概念 | 解释 | 类比 |
|---|---|---|
| 变量 | let name = "张三" — 存放数据的容器 |
贴了标签的盒子 |
| 函数 | function add(a, b) { return a + b; } — 可复用的代码块 |
微波炉:输入食材,输出热菜 |
| DOM | Document Object Model — 把 HTML 文档变成 JS 可操作的对象树 | 遥控器:通过 JS 操控页面元素 |
| 事件 | 用户的操作(点击、输入、提交) | 门铃:按一下触发响应 |
阅读 app.js(重点部分)
打开 static/js/app.js,找到以下关键模式:
1. 选择 DOM 元素并操作它
// 获取元素
const tbody = document.getElementById('table-body');
// 修改内容
tbody.innerHTML = '<tr><td>...</td></tr>';
2. 事件绑定
// HTML 中直接绑定(在按钮上写 onclick="doSomething()")
<button onclick="logout()">退出</button>
// JS 中绑定(更灵活,但在这里我们用上面那种方式入门)
document.addEventListener('DOMContentLoaded', () => { ... });
3. 字符串模板(ES6)
// 反引号 + ${} 插值,比 + 拼接直观很多
return `
<tr>
<td>${s.id}</td>
<td>${s.name}</td>
</tr>`;
动手
- 打开浏览器开发者工具(F12 → Console),输入:
观察页面标题是否变化
document.querySelector('h1').textContent = 'Hello JS!' - 在
app.js的renderRow函数中,把"年龄"列的显示改成${s.age} 岁(加上"岁"字) - 在 Console 中手动创建一个
<div>,设置颜色,插入到页面中
第 2 天:fetch API —— 前后端握手
fetch 是什么?
fetch 是浏览器内置的 HTTP 客户端,让 JS 可以调用后端 API。
// GET 请求
const response = await fetch('/api/students');
const result = await response.json(); // 解析 JSON
console.log(result.data); // 使用数据
// POST 请求
const response = await fetch('/api/students', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '新同学', age: 20, email: 'x@mail.com', score: 85 })
});
async / await 是什么?
fetch 是异步操作(需要时间等待服务器响应),await 让代码看起来像同步的一样,更好理解。
// ❌ 不用 async/await(回调地狱)
fetch(url).then(resp => resp.json()).then(data => console.log(data));
// ✅ 用 async/await(像写同步代码一样)
async function load() {
const resp = await fetch(url);
const data = await resp.json();
console.log(data);
}
本项目中的请求封装
打开 app.js,看 api() 函数:
async function api(url, options) {
options.headers['Authorization'] = 'Bearer ' + token; // 自动带 Token
const resp = await fetch(url, options);
const result = await resp.json();
if (resp.status === 401 || resp.status === 403) {
logout(); // Token 失效,自动退回登录页
}
return result;
}
为什么封装? 每个请求都需要带 Authorization 头,封装后调用方只需要关心业务数据。
动手
- 在 Console 中手动调用 fetch 测试 API:
fetch('/api/students/hello', { headers: { 'Authorization': 'Bearer week4-secret-token' } }) .then(r => r.json()) .then(console.log) - 观察 Network 面板(F12 → Network),看请求和响应的完整内容
- 故意发一个错误的 Token,观察 401 返回和处理逻辑
第 3 天:Spring MVC 拦截器 —— 请求的"门卫"
拦截器原理
HTTP 请求
│
▼
DispatcherServlet(前端控制器)
│
▼
[拦截器 preHandle] ← 在这里决定"放行"还是"拦截"
│ 通过
▼
Controller(处理业务)
│
▼
[拦截器 postHandle/afterCompletion]
│
▼
HTTP 响应
本项目拦截器做了什么?
打开 controller/AuthInterceptor.java:
@Override
public boolean preHandle(HttpServletRequest request, ...) {
// 1. 白名单直接放行
if (path.startsWith("/api/hello")) return true;
// 2. 校验 Authorization 头
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
response.setStatus(401); // 返回 401 Unauthorized
return false; // 拦截!
}
// 3. 验证 Token 是否匹配
String token = authHeader.substring(7);
if (!validToken.equals(token)) {
response.setStatus(403); // 返回 403 Forbidden
return false;
}
return true; // 放行
}
注册拦截器(WebConfig.java)
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**") // 拦截所有 /api/ 路径
.excludePathPatterns("/api/hello", "/api/login"); // 白名单
}
}
动手
- 用 Postman 不带 Authorization 头发一个
GET /api/students,观察 401 响应 - 带正确的 Token
Bearer week4-secret-token,观察正常返回 - 把
auth.token改成另一个值,重启后观察旧 Token 变为 403 - 思考:真正的用户系统会怎么存储和验证 Token?(预告:JWT,第 5 周)
第 4 天:统一异常处理 + 参数校验
GlobalExceptionHandler 做了什么?
@RestControllerAdvice // 拦截所有 Controller
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class) // 参数校验失败
public ResponseEntity<?> handleValidation(...) {
return ResponseEntity.badRequest()
.body(ApiResponse.badRequest("参数校验失败: ..."));
}
@ExceptionHandler(Exception.class) // 兜底:所有未捕获异常
public ResponseEntity<?> handleAll(...) {
return ResponseEntity.status(500)
.body(ApiResponse.error(500, "服务器内部错误"));
}
}
为什么 Controller 不需要 try-catch?
因为 @RestControllerAdvice 在所有 Controller 方法的外层织入了一层异常处理网。任何异常抛出后,都会落入对应的 @ExceptionHandler 方法。
// Controller 中
@PostMapping
public ApiResponse<Student> add(@Valid @RequestBody Student s) {
return ApiResponse.created(service.add(s));
// 如果 s 的 name 为空 → MethodArgumentNotValidException
// 如果 service.add 中抛异常 → Exception
// 都会被 GlobalExceptionHandler 捕获,Controller 不需要处理
}
统一响应格式(ApiResponse)
所有接口返回统一结构:
{
"code": 200,
"message": "success",
"data": { ... },
"extra": { "total": 10, "orm": "JPA" }
}
这样前端只需要判断 result.code === 200 来确定请求是否成功。
动手
- 故意提交空的姓名,观察返回的 400 错误详情
- 在 Student 实体类中新增一个校验规则:
@Min(0) @Max(100) private int score - 测试传入 score=999,观察校验是否生效
第 5 天:RESTful 设计规范 + ORM 切换
RESTful API 最佳实践
| 原则 | 规则 |
|---|---|
| 用名词,不用动词 | /api/students ✅ /api/getStudents ❌ |
| HTTP 方法表达操作 | GET=查 / POST=增 / PUT=改 / DELETE=删 |
| 资源用复数 | /api/students 而不是 /api/student |
| 层级表示关系 | /api/students/5/avatar = 学生 5 的头像 |
| 正确使用状态码 | 200=成功 / 201=创建成功 / 400=参数错误 / 404=未找到 / 500=服务器错误 |
ORM 模式切换
本项目用 orm.mode 配置实现了 JPA 和 MP 的无缝切换:
# application.yml
orm:
mode: jpa # 改成 mp 即可切换到 MyBatis-Plus
原理:StudentServiceSelector 读取 @Value("${orm.mode}"),根据值选择调用 StudentJpaService 还是 StudentMpService。Controller 层完全不感知底层用的是哪个 ORM。
动手
- 修改
orm.mode为mp,重启,观察前端页面显示的 ORM 标签 - 测试同一个接口的返回数据是否完全一致
- 对比控制台 SQL 输出,JPA 和 MP 生成的 SQL 有何不同
- 思考:如果要在运行时动态切换(不重启),需要怎么改?
第 6 天:文件上传 + CORS 跨域
Spring Boot 文件上传
Controller 中处理文件只需 @RequestParam("file") MultipartFile file:
@PostMapping("/{id}/avatar")
public ResponseEntity<?> uploadAvatar(
@PathVariable Long id,
@RequestParam("file") MultipartFile file) {
// 1. 文件校验
if (file.isEmpty()) return badRequest("文件为空");
String ext = originalName.substring(originalName.lastIndexOf("."));
// 2. 保存到磁盘
String filename = UUID.randomUUID() + ext; // 随机名防冲突
file.transferTo(new File("./uploads", filename));
// 3. 更新数据库
service.updateAvatar(id, filename);
}
前端如何上传文件
const formData = new FormData();
formData.append('file', fileInput.files[0]); // 从 <input type="file"> 取文件
fetch(`/api/students/${id}/avatar`, {
method: 'POST',
body: formData // 不设 Content-Type!浏览器自动设置 multipart/form-data + boundary
});
CORS 跨域
当前前后端同端口,不存在跨域。但项目已经预置了 CORS 配置(WebConfig.addCorsMappings),为后续前后端分离做准备。
静态资源映射
// 将本地 uploads/ 目录映射到 URL /uploads/**
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:./uploads/");
这样通过 http://localhost:8080/uploads/xxx.jpg 就能访问上传的头像。
动手
- 新增一个学生,编辑该学生 → 上传一张头像 → 观察表格中头像显示
- 打开
./uploads/目录,确认文件已保存 - 故意上传一个超过 2MB 的文件,观察错误提示
- 修改
application.yml中spring.servlet.multipart.max-file-size为 5MB
第 7 天:项目整合与总结
本周完整请求流程
浏览器
│
├── 1. GET / → 加载 index.html + style.css + app.js
│
├── 2. 用户输入 Token → JS 调用 /api/students/hello 验证
│
├── 3. Token 验证通过 → 进入主面板 → JS 调用 /api/students 加载数据
│ │
│ ▼
│ [AuthInterceptor] 校验 Authorization 头
│ │ 通过
│ ▼
│ [StudentController] 接收请求 → 参数校验(@Valid)
│ │
│ ▼
│ [StudentServiceSelector] 根据 orm.mode 选择 JPA 或 MP Service
│ │
│ ▼
│ [StudentJpaService / StudentMpService] 执行业务逻辑
│ │
│ ▼
│ [Repository / Mapper] 操作数据库
│ │
│ ▼
│ 返回 ApiResponse 格式 JSON → JS 渲染到页面
│
└── 异常 → [GlobalExceptionHandler] 拦截 → 返回统一错误 JSON
完整验收清单
后端(Postman 逐项测试):
GET /api/students/hello无需 Token,返回运行状态GET /api/students需 Token,返回全部学生GET /api/students?keyword=张模糊搜索POST /api/students新增一个学生GET /api/students/1查询 ID=1PUT /api/students/1更新学生信息DELETE /api/students/1删除学生POST /api/students/1/avatar上传头像- 不带 Token 发请求 → 401
- 带错误 Token 发请求 → 403
- 提交空 name → 400 校验错误
- 修改
orm.mode: mp后功能不变
前端(浏览器):
- 输入正确 Token → 进入主面板
- 输入错误 Token → 显示错误
- 表格正确展示全部学生(头像、成绩徽章颜色正确)
- 新增学生 → 表单提交 → 表格刷新
- 编辑学生 → 回填数据 → 更新成功
- 上传头像 → 头像显示在表格中
- 搜索 → 过滤数据
- 删除 → 确认弹窗 → 删除成功
- 退出登录 → 回到登录页
本周产出
✅ 原生 JS 前后端交互(fetch + DOM 操作)
✅ Token 认证与 401/403 处理
✅ Spring MVC 拦截器(登录权限校验)
✅ 全局异常处理(统一 JSON 格式错误返回)
✅ 参数校验(@Valid + Bean Validation + 自定义错误消息)
✅ RESTful API 设计规范
✅ ORM 配置切换(JPA ↔ MyBatis-Plus,一行配置切换)
✅ 文件上传(头像,前端 FormData + 后端 MultipartFile)
✅ CORS 跨域预配置
✅ 统一 API 响应格式(ApiResponse<T>)
本周核心:你已从 Java 基础走到了能独立交付一个全栈应用的水平。后端有三层架构和双 ORM,前端有原生 JS 交互和文件上传,系统有拦截器保护、统一异常处理、参数校验。这是你第一个真正的里程碑项目。