软件工程第二次结对作业
202601 软件工程课程二次结对作业 —— 校园失物招领 Web 应用(程序实现)
| 这个作业属于哪个课程 | 202601 软件工程 |
|---|---|
| 这个作业要求在哪里 | 第二次结对作业之程序实现 |
| 这个作业的目标 | 基于上次的原型设计,结对完成"校园失物招领"核心功能的编码与单元测试 |
| 结对成员 | 102401129 林诗航 / 102401127 张炜鑫 |
| 本作业博客链接 | 102401129 林诗航 软件工程第二次结对作业 |
| 结对同学博客链接 | 102401127 张炜鑫 软件工程第二次结对作业 |
| GitHub 项目地址 | 102401129-102401127 |
一、具体分工
| 成员 | 主要分工 |
|---|---|
| 林诗航(102401129) | 数据层与核心模块(core.js / validate.js / search.js / util.js / seed.js)、发布页与我的发布页、单元测试编写、端到端验收脚本、PSP 统计与博客排版 |
| 张炜鑫(102401127) | 全站样式与公共组件(style.css / ui.js)、首页 / 搜索页 / 详情页、流程图绘制、测试用例设计与复审、博客走查 |
结对方式沿用上次的做法:两人共同讨论出数据结构和接口约定后,轮流担任"驾驶员"(写代码)和"领航员"(逐行审查、负责提问)。数据层与校验模块由一人主写、另一人逐条核对边界条件;页面部分按页面分工并行推进,每天固定时间同步联调。
二、PSP 表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 30 |
| Estimate | 估计这个任务需要多少时间 | 30 | 30 |
| Development | 开发 | 850 | 1120 |
| Analysis | 需求分析(包括学习新技术) | 60 | 90 |
| Design Spec | 生成设计文档 | 40 | 60 |
| Design Review | 设计复审 | 30 | 30 |
| Coding Standard | 代码规范(为目前的开发制定合适的规范) | 20 | 30 |
| Design | 具体设计 | 60 | 50 |
| Coding | 具体编码 | 480 | 600 |
| Code Review | 代码复审 | 60 | 60 |
| Test | 测试(自我测试,修改代码,提交修改) | 120 | 240 |
| Reporting | 报告 | 100 | 140 |
| Test Report | 测试报告 | 30 | 40 |
| Size Measurement | 计算工作量 | 20 | 20 |
| Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 50 | 80 |
| 合计 | 980 | 1290 |
差异原因:总耗时比预估多 310 分钟,主要在两处——
- Coding 低估(+120):预估时只算了"把功能写出来"的时间,没算依赖梳理。开发中期把转义、时间格式化从 ui.js 抽成独立的 util.js 时,五个页面漏引了新模块;又在一次 UMD 封装改造中把
global参数弄丢(详见第六节),这两处排查加上回归测试花了约两小时; - Test 低估(+120):原计划单元测试跑通即可,实际为了验证"助教下载后双击就能用",额外写了一套基于 puppeteer 的端到端走查脚本(24 项断言),还真抓到了三个单元测试抓不到的问题,脚本编写、问题定位与修复回归共花了一倍于预估的时间。
反过来看,Design 比预估省了 10 分钟——原型阶段已经把页面结构和字段定清楚了,这次直接照着实现,验证了上次作业"设计做扎实,编码才省力"的判断。
三、解题思路描述与设计实现说明
1. 代码实现思路
总体架构:两层分离,纯逻辑与页面解耦。
页面层(5 个 HTML + page-*.js) —— 只负责取值、渲染、绑定事件
│ 调用
纯逻辑层(5 个 JS 模块,不依赖 DOM)—— 校验、搜索、数据存取、格式化
│ 读写
localStorage(不可用时自动降级为内存)
五个核心模块全部写成纯逻辑模块(UMD 导出,浏览器挂全局命名空间,Node 下可直接 require),这样单元测试可以同时跑在浏览器和 Node 两端,也是本次工程上最值得的一个决定——后面端到端走查出问题需要快速回归时,秒级的 Node 测试发挥了重要作用。
关键技术决策:
-
数据模型:一条信息是一个扁平对象。
type区分寻物(lost)/ 招领(found);status只有 open(进行中)/ resolved(已完成)两态,"已找到 / 已归还"是同一状态在两种类型下的展示文案(statusLabel()),避免出现两个需要同步的布尔字段;createdAt时间戳用于排序与相对时间显示。{ id, type: 'lost'|'found', name, cat, place, date, // date 为丢失/拾获日期 nickname, contact, note, status: 'open'|'resolved', createdAt } -
发布者身份:作业明确不做账号和实名认证,"我的发布"采用设备归属——发布时把信息 id 记入本机
clf_my_ids,凡是在这个 id 集合里的信息才显示状态操作面板。这是作业范围内的简化,README 里已向测试人员说明边界。 -
状态闭环:这是上次讲评点名的问题,这次在产品层面给了完整答案——谁来改(仅发布者,本机判定)、从哪里改(详情页"发布者操作面板" + 我的发布页行内按钮两个入口)、改了别人在哪里看到(首页/搜索卡片灰显+徽章、详情页绿色横幅、我的发布统计),并支持误标记后一键恢复。
-
健壮性:localStorage 访问包在探测逻辑里,被禁用时降级为内存存储并提示"数据仅本次会话有效";所有用户输入渲染前经
escapeHTML转义,防止备注里写一段脚本把信息墙打挂;JSON 解析失败按空数据处理,不让页面白屏。 -
零依赖运行:不引入任何框架与 CDN,图标全部是手绘的内联 SVG,Mocha/Chai 下载后本地化到
test/lib/——保证助教断网、双击 HTML 也能得到完整体验。
2. 关键实现的流程图 / 数据流图
① 总体数据流图——页面层不直接碰存储,全部经 core.js;这也是单测能抽离纯逻辑的原因:

② 发布信息流程——表单提交先过 validatePost 逐字段校验,出错字段就地红框提示,通过才落库:

③ 搜索与筛选流程——关键词拆词后多词 AND 匹配,空结果有兜底引导,不会停在死胡同:

④ 信息状态流转图——呼应作业"更新状态"要求:两个状态、两个修改入口、三处结果展示,外加删除支线:

3. 重要代码片段与解释
(1)存储层的探测与降级(core.js)——所有读写都走这一个入口。探测失败不是简单报错,而是降级为内存存储,页面功能照常可用,只提示数据不持久化:
var memoryStore = {}; // 降级用内存存储
var storage = (function () {
try {
var probe = '__clf_probe__';
global.localStorage.setItem(probe, '1'); // 试写探测
global.localStorage.removeItem(probe);
return global.localStorage;
} catch (e) {
return null; // 部分环境禁用 file:// 存储
}
})();
var isPersistent = !!storage; // 首页据此显示"数据仅本次会话有效"提示
这段代码还藏着一个我们踩过的坑,放在第六节"遇到的困难"里细说。
(2)状态流转(core.js)——"已找到 / 已归还"不是两个字段,而是同一状态按类型映射的文案。恢复为进行中防止误标记无法撤回:
function resolveItem(id) {
var item = getItem(id);
if (!item) return null; // id 不存在不抛异常,返回 null 让页面提示
return updateItem(id, { status: 'resolved' });
}
function statusLabel(item) {
if (!item || item.status !== 'resolved') return '';
return item.type === 'lost' ? '已找到' : '已归还'; // 寻物/招领各自的完成文案
}
(3)多关键词 AND 搜索(search.js)——关键词按空白拆词、转小写后,一条信息要在"名称 + 地点 + 备注 + 昵称"里全部命中才算匹配:"蓝色 耳机"能精确到一条,而不会把所有蓝色东西都翻出来:
function splitKeywords(q) {
if (typeof q !== 'string') return [];
var parts = q.trim().toLowerCase().split(/\s+/);
return parts.filter(function (p) { return p; });
}
function matches(item, terms) {
var text = [item.name, item.place, item.note, item.nickname].join(' ').toLowerCase();
return terms.every(function (t) { return text.indexOf(t) >= 0; });
}
query() 组合函数把关键词、类型、分类、状态四个条件串起来,首页和搜索页调用的是同一份代码——单元测试覆盖的就是它。
(4)表单校验的日期边界(validate.js)——日期必须是真实存在的 YYYY-MM-DD(2 月 30 日直接拒绝),且不允许晚于今天;today 作为参数注入而不是在函数里取系统时间,测试用例因此可以固定日期、稳定复现:
function isValidDateStr(s) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return false;
var p = s.split('-'), y = +p[0], m = +p[1], d = +p[2];
if (m < 1 || m > 12 || d < 1 || d > 31) return false;
var dt = new Date(y, m - 1, d); // 2月30日会被 Date 自动进位到 3 月
return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d;
}
// 校验入口:validatePost(input, today),today 缺省才取系统当天
(5)渲染前统一转义(util.js + ui.js)——信息墙全部由用户输入构成,< > " 若不转义,一条"标题写 <script>"的信息就能打挂整个页面。转义抽成纯函数并配了专门用例:
function escapeHTML(s) {
if (s === null || s === undefined) return '';
return String(s)
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
// 卡片渲染:'<h3>' + escapeHTML(item.name) + '</h3>' —— 所有用户字段一律先转义再拼接
4. 程序运行成果展示
按作业主流程「发布 → 浏览/搜索 → 详情 → 联系 → 更新状态」,在 Chrome 中以 file:// 直接打开实测(与助教评分方式一致),另配 24 项断言的自动化走查全部通过。运行结果如下:
① 首页信息墙——8 条信息按时间倒序,类型 / 分类 / 状态筛选与计数实时联动:

② 发布招领——表单提交后弹出成功提示,可直达详情、返回首页或再发一条:

③ 查看详情——完整信息 + 联系方式卡片,支持一键复制与拨打电话:

④ 更新状态(发布者)——发布者标记「已归还」后,详情页出现绿色横幅,误标记可一键恢复为进行中:

⑤ 结果对所有人可见——首页与搜索结果中,已完成信息灰显并带状态徽章,避免其他同学继续无效联系:

⑥ 关键词搜索——搜索「雨伞」精确命中 1 条,无结果时另有兜底引导(见附加特点):

四、附加特点设计与展示
特点一:一键复制联系方式
意义:失物招领的"最后一公里"是联系。手机上看不清、抄错一位号码,线索就断了。详情页提供【一键复制联系方式】,把物品、类型、发布人、联系方式拼成一段话复制进剪贴板,贴到 QQ / 微信就能发起联系;纯数字手机号额外给【拨打电话】直达拨号盘。这一步把"查到信息"变成"联系上人",是对主流程的直接增强。
实现思路:优先用异步 Clipboard API;file:// 等环境可能权限受限,自动降级为隐藏 textarea + execCommand('copy') 的传统方案;再不行弹 toast 请用户手动复制——三级兜底,保证按钮永远有明确的反馈:
UI.copyText = function (text) {
function legacyCopy() {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
var ok = false;
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
document.body.removeChild(ta);
return ok;
}
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text).then(function () { return true; })
.catch(function () { return legacyCopy(); });
}
return Promise.resolve(legacyCopy());
};
成果(详情页联系方式卡片 + 复制成功的 toast):

特点二:分类 × 状态组合筛选 + 空状态兜底
意义:信息多了以后,"浏览"不能只靠滚动。首页提供"类型(全部/寻物/招领)× 物品分类 × 状态(进行中/已完成)"三重筛选,丢同学可以只看"进行中"的招领、按"证件卡类"快速缩小范围;已完成的信息默认可见(灰显 + 徽章),让"已找到/已归还"的结果对所有人透明——这正是状态闭环里"别人在哪里看到"的答案。搜索空结果时不留白屏,给出"清空重搜 / 去发布寻物"两条出路,把可能的流失转化为新发布。
实现思路:筛选逻辑全部收敛在 search.js 的 query(items, { type, cat, status, q }),页面只传条件对象;空状态是渲染层的约定——列表为空时必须给标题、说明和行动按钮,写进了 ui.js 的渲染约定。
成果(寻物 × 已完成的组合筛选结果;卡片灰显并带「已找到」徽章):

(搜索无结果的兜底页面):

特点三:演示数据一键重置(为测试人员设计)
首次打开自动灌入 8 条演示数据(含已完成的样本,日期基于当前时间动态生成、永不过期);【我的】页底部提供"载入演示数据 / 清空全部数据",数据如果测试乱了可以一键还原。

五、目录说明和使用说明
完整版在项目 README.md,此处为摘要,测试人员请以 README 为准。
目录组织:页面(5 个 HTML)+ 样式(css/style.css)+ 逻辑(js/ 下 5 个纯逻辑模块、1 个 UI 组件模块、5 个页面入口脚本)+ 测试(test/ 含本地化的 Mocha/Chai 与 5 个测试文件)+ 截图素材(screenshots/)。模块依赖关系:页面 → UI 组件 → 纯逻辑模块 → localStorage。
第四次作业/
├── index.html # 首页:浏览 + 三重筛选(入口)
├── publish.html # 发布页
├── search.html # 搜索页
├── detail.html # 详情页(?id=xxx)
├── my-posts.html # 我的发布:状态管理 + 数据工具
├── css/style.css # 全站样式
├── js/ # core 数据层 / validate 校验 / search 搜索 / util 工具 / seed 演示数据 / ui 组件 / page-*.js 页面逻辑
├── test/ # test.html 双击即跑单元测试;lib/ 已本地化 Mocha+Chai
└── screenshots/ # 流程图与运行截图
如何运行:
- 下载仓库全部文件,保持目录结构;
- 用谷歌浏览器双击打开
index.html——无需联网、无需安装、无需构建; - 首次打开自动载入 8 条演示数据,按 README 第四节的走查说明即可体验完整流程;
- 单元测试:双击
test/test.html查看测试报告(44 个用例应全部绿色通过)。
若浏览器限制 file:// 本地存储(页面顶部会出现提示条),可用 python -m http.server 8080 等任意静态服务器访问,功能不受影响。
六、单元测试
1. 测试工具的选择与学习过程
我们选用 Mocha(测试框架)+ Chai(断言库),原因有三点:
- 博客园课程推荐资料里就有《测试框架 Mocha 实例教程》,入门资料充足;
describe / it / expect的组织方式和我们按模块划分的代码天然对应;- 两者都有浏览器 UMD 版本,可以本地化后双击 HTML 运行,符合"助教下载即可验证"的交付要求。
学习路径:先读阮一峰的《测试框架 Mocha 实例教程》过一遍概念(BDD 接口、钩子函数),再看廖雪峰的 JavaScript 教程中关于浏览器测试的部分,然后直接在我们的 validate.js 上写第一个用例试手。Chai 我们用的是 expect 风格(expect(x).to.equal(y)),可读性最好。
我们的三步简易教程:
# 第 1 步:准备。项目 test/lib/ 已放好 mocha.js、mocha.css、chai.js(本地化,无需 npm)。
# 第 2 步:写用例。新建 test/validate.test.js:
var expect = (typeof window !== 'undefined' ? window : require('./lib/chai.js')).expect;
var V = typeof window !== 'undefined' ? window.CLFValidate : require('../js/validate.js');
describe('发布校验(validate.js)', function () {
it('日期不能晚于今天(明天报错、今天通过)', function () {
var input = { type: 'lost', name: '校园卡', cat: '证件卡类', place: '食堂',
date: '2026-09-30', nickname: '小林', contact: '13800138821' };
expect(V.validatePost(input, '2026-09-29').errors).to.have.property('date');
input.date = '2026-09-29';
expect(V.validatePost(input, '2026-09-29').valid).to.equal(true);
});
});
<!-- 第 3 步:运行。方式 A(零环境):test.html 里按顺序引入被测模块和测试文件后双击打开;
方式 B(Node):npx mocha@10.8.2 test/validate.test.js -->
要点:被测模块写成 UMD(浏览器挂 window.XXX,Node 走 module.exports),同一份用例就能双端运行;beforeEach 里重置数据层,用例之间互不污染。
2. 测试代码展示与说明
共 44 个用例,覆盖五个纯逻辑模块 + 渲染安全。这里贴两组代表性的。
数据层状态流转(core.test.js)——验证"标记已完成"在两种类型下产出正确文案、误标记可恢复、删除不可逆:
it('resolveItem:寻物标记为「已找到」,招领标记为「已归还」', function () {
var lost = CLF.addItem(BASE);
var found = CLF.addItem(Object.assign({}, BASE, { type: 'found' }));
expect(CLF.statusLabel(CLF.resolveItem(lost.id))).to.equal('已找到');
expect(CLF.statusLabel(CLF.resolveItem(found.id))).to.equal('已归还');
expect(CLF.resolveItem('不存在')).to.equal(null); // 异常 id 不抛异常
});
it('reopenItem 可把已完成的恢复为进行中(防误标记)', function () {
var item = CLF.addItem(BASE);
CLF.resolveItem(item.id);
expect(CLF.reopenItem(item.id).status).to.equal('open');
});
表单校验边界值(validate.test.js)——名称 30/31 字、备注 100/101 字、日期今天/明天,边界两侧都测:
it('物品名称:为空、超长均报错;30 字为边界可通过', function () {
expect(V.validatePost(Object.assign(okInput(), { name: ' ' }), TODAY).errors).to.have.property('name');
expect(V.validatePost(Object.assign(okInput(), { name: '卡'.repeat(31) }), TODAY).errors).to.have.property('name');
expect(V.validatePost(Object.assign(okInput(), { name: '卡'.repeat(30) }), TODAY).valid).to.equal(true);
});
渲染安全(ui.test.js,浏览器环境)——把 <script>alert(1)</script> 塞进物品名称,断言渲染结果里只可能出现转义后的实体:
it('物品名称中的脚本标签被转义,不会注入 HTML', function () {
var item = { id: 'xss1', type: 'lost', name: '<script>alert(1)</script>', /* … */ };
var html = UI.cardHTML(item);
expect(html).to.not.contain('<script>');
expect(html).to.contain('<script>');
});
浏览器端运行结果(44 passing / 0 failing):

3. 测试数据构造思路与自我评价
构造思路:以白盒方法为主——先看代码里每一个 if,让每个错误分支至少有一条用例踩到;再叠加三类系统性数据:
- 边界值:所有长度限制(30/31 字、100/101 字)、日期临界(今天/明天)、时间格式化的区间端点(59 秒/1 分钟/59 分钟/1 小时);
- 等价类与异常输入:合法等价类(手机号 / QQ / 微信号)、非法等价类(空、全空格、
2026/09/29这类格式错误、2026-02-30这类不存在的日期); - 攻击性输入:
<script>、<img onerror>、引号与&——验证转义,模拟将来测试人员最可能使用的"刁难"手段。此外多关键词 AND("蓝色 耳机"命中 / "蓝色 雨伞"不命中)、大小写混排、排序稳定性(排序后原数组不被修改)也各有用例。
自我评价:这套用例能较好地守住纯逻辑层——数据存取、状态流转、校验规则、搜索语义都有分支级覆盖,将来改动这些模块,回归只需数秒。但它有明确的边界:页面交互(点击、表单联动、跨页面数据流)不在其覆盖范围内。为补上这一层,我们额外写了基于 puppeteer 的端到端走查脚本(24 项断言,模拟"双击打开 file:// 页面"的真实使用),并且事实证明单测全绿不等于应用可用——E2E 抓到了三个单测永远抓不到的问题(见第七节)。测试人员将来若继续刁难,可能的突破口还有:跨浏览器差异(我们只保证 Chrome)、存储配额写满、双开页面同时修改数据——前两者已有降级或提示,最后一点是纯前端方案的固有限制,只能靠真实账号体系解决。
七、GitHub 代码签入记录
按"每完成一个功能模块提交一次"的约定签入,commit message 描述当次功能或修复的问题:
b540858 Merge pull request #1 from OooneCloud/main
0607e8e 清理仓库:移除发布前内部待办清单(非产品文档,README 未引用)
b8d7937 同步张炜鑫本地修订:精简开篇引言、软化两处措辞、测试工具原因改为列表
42532a9 精简结对过程为事实概述(评分规则未要求展示结对过程)
b615828 补充结对过程:四节点对话整理(定契约/结对编码/用例评审/E2E走查)
2720c61 个人总结与队友评价定稿(张炜鑫)
d5f34d7 博客图片全部托管博客园(16/16 回读可用),签入记录截图入博,清单同步勾选
972efd8 博客补充「程序运行成果展示」小节:主流程 6 张实测截图
0169104 博客与发布清单:GitHub 地址更新为结对仓库 102401129-102401127
1864db5 第四次作业:README、发布清单、结对作业博客(含四张流程图与全流程截图)
51e303f 详情页分类图标跟随物品分类(与卡片一致)
67c6097 修复 E2E 走查发现的三个问题:UMD 工厂未传 global 致浏览器端存储降级、sticky 底部导航遮挡底部按钮、我的发布页误用通用卡片模板
9976370 第四次作业:单元测试(Mocha + Chai,42 个用例覆盖数据层/校验/搜索/工具/渲染安全)
9acf87a 第四次作业:发布页、搜索页、详情页与我的发布页(表单校验、关键词搜索、状态流转入口)
5e5a605 第四次作业:公共样式与 UI 组件(内联 SVG 图标、卡片渲染、toast、一键复制)+ 首页浏览与筛选
418e969 第四次作业:数据层与核心模块(localStorage 封装、发布校验、搜索筛选、演示数据)
(仓库:github.com/lin634/102401129-102401127)

八、结对过程
本次按"先定契约 → 结对编码 → 交叉走查"推进:开工前共同评审数据结构与公共接口(status 两态建模、ui.js 函数签名);编码阶段按模块轮换驾驶员 / 领航员——数据层与校验由林诗航主写、张炜鑫逐条核对边界,页面与样式由张炜鑫主写、林诗航审查;测试阶段先由张炜鑫按分支设计用例(列用例过程中即发现两处漏判),再用 Puppeteer 端到端走查兜底,连抓三个单元测试覆盖不到的问题(详见第九节)。全部讨论结论均以功能粒度签入仓库,记录见第七节。
九、遇到的代码模块异常或结对困难及解决方法
问题一:单元测试全绿,浏览器里数据却"活不过一个页面"
- 问题描述:端到端走查时发现,发布的信息跳到详情页就"查无此信息",所有页面的数据都只在单页内有效——等于状态闭环整个断了。诡异的是,44 个单元测试全部通过。
- 做过哪些尝试:先怀疑无头浏览器的
file://限制,做了最小实验页证明解析期、加载后访问 localStorage 都正常、中文路径也不影响;又在 core.js 的探测失败分支里把被吞掉的异常打印出来,拿到了关键线索:TypeError: Cannot read properties of undefined (reading 'localStorage')。 - 是否解决:已解决。根因是 core.js 的 UMD 封装——外层 IIFE 调用
factory()时忘了把global传给工厂函数,工厂里global.localStorage因此抛了 ReferenceError;而这个错误恰好被我们自己写的"探测失败就降级为内存存储"的 try/catch 吞掉了,于是应用在浏览器里一直安静地跑在"内存模式"。修复只需一行:factory()改为factory(global)。 - 有何收获:这是本次作业印象最深的一课——防御式编程会把 bug 藏进降级路径里。try/catch 兜底方案必须有"兜底是否生效"的可观测信号(我们后来在首页加了"数据仅本次会话有效"的提示条,它其实也是降级发生时的报警器);同时"单测全绿"只覆盖你想到的路径,必须有一层模拟真实使用的端到端走查。
问题二:真实点击"立即发布",表单被清空且页面刷新
- 问题描述:E2E 走查中,模拟真实坐标点击提交按钮时页面整体刷新、填好的内容全部丢失;改用 DOM 层
click()却一切正常。 - 做过哪些尝试:先后怀疑了设备像素比导致的坐标偏移、Windows 显示缩放、表单默认提交行为,都排除后,在页面里注册导航监听发现:点击确实触发了一次导航——按钮被滚动到视口底部时,恰好压在悬浮的 sticky 底部导航栏下面,点击落在了底部导航的"发布"标签上,等于点了链接跳转回自己。
- 是否解决:已解决,且这不是自动化工具的假问题——真实用户把页面滚到同样位置一样会误触。修复:三个滚动内容区增加 88px 底部留白,并给
html加scroll-padding-bottom,保证任何按钮都能滚动到底部导航之上。 - 有何收获:老师上次说"界面画出来和用户能够用起来是两回事",这个 bug 是最生动的注脚——原型阶段我们给底部导航和内容区的遮挡问题画过重点,实现时还是栽在了同样的地方。悬浮元素必须有对应的布局补偿,这类约束应该进代码规范。
问题三:模块重构后的"silent miss"
- 问题描述:把转义、时间格式化从 ui.js 抽成 util.js 后,五个页面忘了引入新文件,ui.js 找不到
CLFUtil直接报错,页面数据区空白。因为测试页 test.html 引入了全部模块,单测依然全绿。 - 做过哪些尝试:E2E 报错后直接读浏览器控制台,一眼定位到缺失的模块引用。
- 是否解决:已解决(五个页面补引 util.js),并顺带把"页面脚本清单"固定为 HTML 模板注释。收获:重构的破坏面要用自动化走查兜住,"测试能跑"和"页面能跑"的依赖集合并不相同。
结对协作上的困难:两人页面分工并行时,公共组件(卡片渲染、toast)的接口一度各自为政,合并时出现两套样式。解决办法是把"公共层先定契约、页面只消费"立为规矩:ui.js 的函数签名由两人共同评审后才动工,之后再没出现样式打架。另外调试 E2E 时时区、日期等因素让复现不稳定,我们约定"能注入的依赖一律注入"(如 validate 的 today 参数),测试稳定性明显提升。
十、评价队友
林诗航评价张炜鑫(102401127)
值得学习的地方:炜鑫在页面样式和交互细节上把控得很不错,审美在线。首页信息墙卡片、状态徽章还有已完成条目灰显这些效果,初稿完成度就很高,基本不需要反复修改。在测试这块他很擅长挑问题,算是我的“把关人”,我写的校验逻辑,他会主动去试各种边界场景,像文本长度上限、非法日期这类情况,不少我遗漏的分支都是他先发现的。流程图也画得很清晰,把状态修改权限、操作入口、展示位置这些关键点都完整表达出来。
需要改进的地方:遇到底层代码问题时,有时会满足于功能跑通,不太愿意深挖背后的原理。就像localStorage降级那个bug,一开始他觉得只要页面能用就可以,后面我们一起排查才找到被try-catch隐藏的报错。另外写页面代码的时候注释可以再多补充一点,后续我接手调试他负责的页面会更容易看懂。
十一、个人总结
林诗航(102401129)
做完这次结对作业,我真切感受到“测试左移”不是书本上的空话。编码阶段就同步编写单元测试,原本以为这样就能保证程序质量,没想到端到端走查还是找出了三处严重bug。特别是UMD封装的问题,我们自己写的降级逻辑直接掩盖了报错,单元测试完全发现不了。这也让我读懂《构建之法》里的观点:开发自测和完整的用户场景测试是两回事,单元测试用来保证底层逻辑正确,端到端走查验证真实用户操作流程,二者缺一不可。
这次也锻炼了我的排错思路:碰到“单测全部通过,但网页实际运行异常”这类问题,优先构造最小复现案例、把被捕获隐藏的异常打印出来,远比靠经验猜测高效。后续开发项目,我会把两条规则加到个人代码规范里:降级处理逻辑必须带上提示告警;悬浮导航这类UI组件,一定要预留布局补偿防止遮挡交互按钮。

浙公网安备 33010602011771号