Skip to content

项目 A:前端页面实现 ​

S5-004必学进阶约 90 分钟JDK 21项目实战 · 项目 A:测试管理平台

1. 本节定位 ​

《项目 A:后端分层与接口设计》做好了接口;本节把它们用起来 —— 一个能真正操作的页面。

★ 本项目刻意不引入任何前端框架与构建工具 ✓,原因有三:

text
① 要演示的是"前后端如何配合",不是前端工程化 ✗
② 一个静态 HTML 就能覆盖:列表渲染、表单提交、错误处理、分页 ✓✓
③ 少一层构建,就少一个"本地能跑、部署挂掉"的可能 ✓

★ 页面位置:labs/project-a/src/main/resources/static/index.html启动后访问:本机 18080 端口

2. 学习目标 ​

  • 知道什么时候"不需要"前端框架 ★★★;
  • 能说清 textContent 与 innerHTML 的区别(XSS) ★★★;
  • 能写出统一的请求封装(错误处理只写一次) ★★★;
  • 知道写操作要防连点 ★★★;
  • 能把后端错误码展示成用户能懂的话 ★★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看一个真实的前端安全问题 ✗✗:

javascript
// ✗ 危险:把接口返回的数据当成 HTML 拼进去
row.innerHTML = '<td>' + book.title + '</td>';
// 如果 book.title 是 "<img src=x onerror=alert(1)>"
// → 脚本被执行 = XSS ✗✗(呼应 S3-006 的"输出也要转义")

// ✓ 安全:当作纯文本
const td = document.createElement('td');
td.textContent = book.title;      // ★ 浏览器不会把它当 HTML 解析

5. 核心概念 ​

5.1 什么时候不需要前端框架 ★★★ ​

★ 三个判断 ✓:

情况建议
页面少(1~5 个)、交互简单 ★★★原生 HTML + fetch 就够 ✓✓
需要复杂的组件复用与状态管理上框架(Vue / React)✓
需要 SEO / 首屏优化上 SSR 方案 ✓

★ 本项目属于第一种 ✓ —— 于是:

text
零构建、零依赖、一个文件、改完刷新即可 ✓✓
部署时随 jar 一起发(static 目录)✓

5.2 渲染:textContent 而不是 innerHTML ★★★ ​

★ 本项目全部用 DOM API 构建 ✓✓:

javascript
data.items.forEach(book => {
  const tr = document.createElement('tr');
  [book.id, book.title, book.author, book.category || '-'].forEach(value => {
    const td = document.createElement('td');
    td.textContent = value;          // ★★ 关键:纯文本,不解析 HTML
    tr.appendChild(td);
  });
  tbody.appendChild(tr);
});

★★ 为什么这比"拼字符串"更重要 ✓✓:

接口返回的数据来自用户输入(书名、作者名都是管理员填的)✗

只要有一处用 innerHTML,就等于给了用户注入脚本的机会 ✗✗

而且这种漏洞很难被测试发现(要专门构造输入)✓

5.3 统一的请求封装 ★★★ ​

★ 本项目只有一个 api() 函数 ✓✓:

javascript
async function api(path, options) {
  const response = await fetch(path, Object.assign(
    { headers: { 'Content-Type': 'application/json' } }, options));
  const text = await response.text();
  const body = text ? JSON.parse(text) : null;
  if (!response.ok) {
    // ★ 后端统一返回 {code, message, timestamp} —— 这里把 message 抛出去给用户看
    throw new Error((body && body.message) ? body.message : ('请求失败:HTTP ' + response.status));
  }
  return body;
}

★★ 这样写的好处 ✓✓:

好处说明
错误处理只写一次 ★★★页面各处只关心成功分支 ✓✓
错误一定能被看到 ★★★抛出来的 Error 会被 withBusy 捕获并展示 ✓
改后端协议只改一处 ★★不用在每个调用点改 ✓

5.4 写操作要防连点 ★★★ ​

javascript
async function withBusy(button, action) {
  button.disabled = true;                  // ★ 禁用按钮
  try {
    await action();
  } catch (e) {
    showMessage(e.message, true);          // ★ 错误展示给用户,不只是 console
  } finally {
    button.disabled = false;               // ★ 无论如何都要恢复
  }
}

★★ 两条必须做到 ✓✓:

#要点不做会怎样
①禁用按钮用户连点 5 次 = 5 个请求 ✗(虽然后端有幂等,但体验差)
②finally 里恢复出错后按钮永久禁用 ✗✗

★ 注意分工 ✓✓:

幂等靠后端(status = 'BORROWED' 条件 —— 《消息可靠性、幂等与顺序》)✓✓

体验靠前端(禁用按钮 + 提示)✓

两者都要有 —— 前端拦不住 curl / 脚本的重复请求 ✗

5.5 错误展示:把错误码翻译成人话 ★★★ ​

★ 本项目的做法 ✓:

javascript
// 后端已经给了可读的 message(如"《设计模式》暂无可借副本(共 1 本)")
showMessage(e.message, true);

★ 更进一步(如果要按码分支)✓:

javascript
const MESSAGES = {
  NO_AVAILABLE_COPY: '这本书暂无库存,换一本试试?',
  READER_SUSPENDED:  '您的读者证已停用,请到前台激活',
  BORROW_LIMIT_REACHED: '已达借阅上限,请先归还',
};
showMessage(MESSAGES[e.code] || e.message, true);

★★ 这就是为什么错误码要稳定 ✓✓(《接口管理平台》)—— 前端按码分支,文案可以随时改 ✓

5.6 页面的五个功能块 ★★ ​

块用到的接口
图书列表(搜索 + 分页)GET /api/books
图书入库 / 加副本(管理员)POST /api/books、POST /api/books/{id}/copies
借书(填 ID + 卡号)POST /api/borrows
借阅记录(按卡号查)GET /api/readers/{cardNo}/borrows
超期清单GET /api/borrows/overdue

★ 列表行内还有"归还 / 续借"按钮 ✓ —— 直接调对应接口后刷新 ✓

★ 入库闭环(《项目 A:需求分析与范围定义》 验收项):入库 → 加副本 → 借出,页面上三个表单就能走完整个 管理员的日常操作 —— 无需 curl ✓(写操作同样走 withBusy 防连点 + 统一错误展示)

6. 递进示例 ​

6.1 最小示例:渲染一行(安全版) ​

javascript
const td = document.createElement('td');
td.textContent = book.title;

6.2 业务示例:借书 ​

javascript
document.getElementById('btnBorrow').addEventListener('click',
  () => withBusy(document.getElementById('btnBorrow'), async () => {
    const bookId = Number(document.getElementById('borrowBookId').value);
    const cardNo = document.getElementById('borrowCardNo').value.trim();
    if (!bookId || !cardNo) { throw new Error('请填写图书 ID 与读者卡号'); }
    await api('/api/borrows', { method: 'POST',
      body: JSON.stringify({ bookId: bookId, cardNo: cardNo }) });
    showMessage('借书成功', false);
    await refreshAll();                    // ★ 操作后立刻刷新,让用户看到结果
  }));

6.3 反例:用 innerHTML 拼列表 ​

javascript
// ✗ XSS 风险 + 拼错一个引号就整页崩 ✗✗
tbody.innerHTML = items.map(b => '<tr><td>' + b.title + '</td></tr>').join('');

7. 完整可运行代码与实测 ​

代码:labs/project-a/src/main/resources/static/index.html

运行:

powershell
cd labs
.\mvnw.cmd -pl project-a spring-boot:run
# http://localhost:18080  → 页面

★ 手测清单(打开页面依次做一遍)✓:

操作期望
打开页面看到 5 本图书,其中《Java并发编程实战》可借 1/2 ✓
搜索 "Java"只剩相关的两本 ✓
借书(书 ID=4,卡号 R001)提示"借书成功",库存 -1 ✓
借书(卡号 R003)提示"读者已被停用" ✓✓
借书(同一本再借一次)提示"还没有归还" ✓
归还库存 +1 ✓
续借两次第二次提示"最多续借 1 次" ✓✓

8. Debug 观察点 ​

  1. 把书名改成 <img src=x onerror=alert(1)>(用 H2 控制台改数据):刷新页面看是否被转义 ✓✓;
  2. 连点"借书" 5 次:观察按钮禁用是否生效 ✓;
  3. 断掉后端再点按钮:观察错误提示(而不是白屏) ✓✓;
  4. 打开浏览器控制台看网络:确认请求体与状态码 ✓;
  5. 故意写错接口路径:观察错误提示是否可读 ✓。

9. 常见错误 ​

#错误说明
1用 innerHTML 渲染接口数据 ✗✗XSS ✓✓
2错误只在 console 里,不给用户看 ✗✗用户以为没反应 ✓
3写操作不禁用按钮 ✗重复提交 ✓
4忘记 finally 恢复按钮 ✗✗出错后按钮永久禁用 ✓✓
5每个调用点都写一遍错误处理 ✗重复代码、容易漏 ✓
6页面用了构建工具但部署时忘了打包 ✗✗线上 404 ✓
7把错误码硬编码成中文判断 ✗后端改文案就失效 ✓

10. 动手实验 ​

目标:把页面真正用起来,并验证安全与错误处理。

步骤:

  1. 启动项目,按手测清单走一遍 ✓;
  2. 在数据库里把书名改成带 <script> 的字符串,刷新看是否被转义 ★★★;
  3. 连点借书按钮 5 次,观察请求次数(用浏览器网络面板) ✓✓;
  4. 停掉后端再操作,观察错误提示 ✓;
  5. 给页面加一个"按分类筛选"(调后端新接口)★★。

验收方式:能说出"你的页面在后端挂掉时会显示什么"。

扩展任务:给页面加乐观更新(点击后立刻改 UI,不等后端返回):

  • 好处:响应更快 ✓;
  • 代价:失败时要回滚 UI(否则界面和数据不一致)✗✗;
  • 说明:为什么本项目选择了"操作后刷新"而不是乐观更新(提示:简单可靠优先于极致体验)★★

11. 练习题 ​

  1. 判断:前端禁用按钮就能防止重复提交。(对 / 错)
  2. 填空:渲染接口返回的数据应该用 ______,而不是 ______。
  3. 简答:为什么"幂等"必须靠后端而不是前端?
  4. (判断)
javascript
td.innerHTML = book.title;

有什么风险? 5. (设计)给借阅记录加"超期提醒":超期的行要标红并显示超期天数。

参考答案(要点)
  1. 错。前端拦不住 curl / 脚本 / 重试 ✗ —— 幂等必须在后端(数据库条件更新)✓✓★
  2. textContent;innerHTML ✓
  3. 因为前端的一切控制都可以被绕过:禁用按钮、防连点、甚至整个页面 ✗ —— 直接 curl 打接口照样能重复提交 ✓✓
  4. XSS 风险 ✗✗ —— 如果 book.title 含 <script>,会被浏览器执行 ✓✓★
  5. 要点:后端在记录里带上 overdueDays()(领域对象已实现 ✓); 前端按它标红(td.className = 'warn')✓; 注意:超期天数必须由后端算(前后端各算一遍迟早不一致)✓✓

12. 掌握标准 ​

  • [ ] 知道什么时候不需要前端框架 ★★★;
  • [ ] 能用 textContent 安全渲染(防 XSS) ★★★;
  • [ ] 能写出统一的请求封装 ★★★;
  • [ ] 知道写操作要防连点 + finally 恢复 ★★★;
  • [ ] 能把后端的错误展示给用户 ★★★;
  • [ ] 知道幂等靠后端、体验靠前端 ★★★。

13. 扩展资源 ​

14. 下一步学习 ​

《项目 A:测试方案与分层测试》:怎么证明这些功能真的对。

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引