Web-Dev-For-Beginners 银行应用重构实战:常量抽取、统一请求函数与 JSDoc 代码文档化
导读
在 Web-Dev-For-Beginners 课程的银行应用(Banking App)系列项目中,经过"模板与路由""表单与注册""数据获取"三节课的叠加,app.js 已经膨胀为集登录、注册、仪表盘于一体的长文件。本文以课程作业 Code Refactoring and Documentation Assignment(3-data 课作业) 为骨架,结合本仓库的 API 服务端、课程讲义 与 参考实现 源码,系统讲解专业开发者在真实项目中每天都在做的三类重构技巧——配置常量抽取、统一请求函数封装(DRY)、JSDoc 文档化。读完你将获得一份可直接对照执行的"app.js 重构检查清单",并能用三档成功标准自评重构质量。
一、为什么 app.js 需要重构:问题诊断
重复代码的两处源头
在课程推进过程中,前端逻辑被要求以 fetch 直接访问 API。对比可见重复模式的成因:
注册场景(见 2-forms 讲义):
async function createAccount(account) {
try {
const response = await fetch('//localhost:5000/api/accounts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: account
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Account creation failed:', error);
return { error: error.message || 'Network error occurred' };
}
}
登录取数场景(见 3-data 讲义):
async function getAccount(user) {
try {
const response = await fetch('//localhost:5000/api/accounts/' + encodeURIComponent(user));
return await response.json();
} catch (error) {
return { error: error.message || 'Unknown error' };
}
}
肉眼可见的问题:
- URL 魔法字符串:
//localhost:5000/api基础地址在多处硬编码; - 样板代码重复:
try/catch + fetch + response.json()结构在两个函数中各写一遍; - 错误信息散落:错误文案直接内联在各处;
- 逻辑可读性差:登录成功后要"存储账户 + 跳转仪表盘",若直接平铺写会与取数逻辑耦合。
关于服务端接口,仓库中真实配套的 Express API 定义在 7-bank-project/api/server.js,其中
POST /api/accounts创建账户(L46-L77)、GET /api/accounts/:user返回指定账户数据(L82-L91),测试请求可参考 api.http。这是"重复 fetch 调用要打的唯一后端"。
重构的三大目标
作业归纳为三点,也正是专业软件开发中的核心实践:
- 提升可读性(Readability)——让代码像散文一样可以被"顺着读";
- 提升可维护性(Maintainability)——将来加功能、改接口只动一处;
- 减少重复(DRY)——公共逻辑收敛到单一抽象点。
二、重构技术一:抽取配置常量(Configuration Constants)
作业任务
在 app.js 文件顶部新建一个"配置区",集中存放可复用常量:
- 提取服务端 API 基础 URL(目前在多处硬编码);
- 为多个函数中重复出现的错误消息创建常量;
- 进一步考虑提取路由路径、被反复使用的元素 ID。
作业给出的结构示例
// Configuration constants
const API_BASE_URL = 'http://localhost:5000/api';
const ROUTES = {
LOGIN: '/login',
DASHBOARD: '/dashboard'
};
仓库参考实现中的"常量区"
仓库的 solution/app.js 正是一个演进后的重构范本,文件头 9 行就是完整的常量区(L1-L9):
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const serverUrl = 'http://localhost:5000/api'; // reserved for future server swap
const storageKey = 'savedAccount';
const accountsKey = 'accounts';
const schemaKey = 'schemaVersion';
const schemaVersion = 1;
参考实现给我们的额外启示:
- 命名约定:全大写下划线(
API_BASE_URL)或驼峰(serverUrl)皆可,但同一文件必须统一; - 注释要解释"为什么":
// reserved for future server swap点明该常量预留用于日后更换后端——这正是文档中"注释解释 why 而非 what"的落地; - 不止 URL:localStorage 的 key、schema 版本号同样属于"魔法值",应一并抽成常量;
- 对结构对象的使用:课程讲义中的
ROUTES常量对象可与 1-template-route 讲义 中介绍的navigate(path)、routes路由表配合,让跳转目标一目了然。
三、重构技术二:统一请求函数 sendRequest()(DRY 原则)
作业任务与函数签名
构建一个可复用函数 sendRequest(),用它消除 createAccount() 与 getAccount() 之间的重复逻辑。需求:
- 同时支持 GET 与 POST;
- 具备规范错误处理;
- 支持不同 URL 端点;
- 可选地接收请求体数据。
作业给定的推荐签名:
async function sendRequest(endpoint, method = 'GET', data = null) {
// Your implementation here
}
基于仓库代码推导出的完整实现
结合 2-forms 讲义 中 createAccount 的请求头/错误处理细节与 3-data 讲义 中 getAccount 的路径拼接技巧,可将上面的函数骨架补成如下可用版本(注意默认参数、encodeURIComponent 对 URL 中特殊字符的转义、以及对 HTTP 非 2xx 状态码的显式判定):
async function sendRequest(endpoint, method = 'GET', data = null) {
const options = { method };
if (data) {
options.headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' };
options.body = JSON.stringify(data);
}
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
return { error: error.message || 'Network error occurred' };
}
}
重构后,原来的两个函数被改写为薄封装:
// 创建账户:POST 请求,携带表单数据
const result = await sendRequest('/accounts', 'POST', data);
// 读取账户:GET 请求,用户名拼入路径并做编码
const data = await sendRequest('/accounts/' + encodeURIComponent(user));
这样重构的本质收获:所有"发请求 → 判断状态 → 解析 JSON → 兜底错误"的共性被收进一个函数;而"端点不同、方法不同、参数不同"的差异保留在调用点。后续若要对所有请求统一加鉴权头或超时逻辑,只需改 sendRequest() 一处——这就是 DRY 的直接收益。
GET 与 POST 的选择依据
讲义 3-data README 中明确对比了两者的语义,封装时据此决定方法即可:
| 维度 | GET | POST |
|---|---|---|
| 用途 | 取回已存在的数据 | 向服务器提交新数据 |
| 参数位置 | URL 路径 / 查询串 | 请求体(body) |
| 缓存 | 可被浏览器缓存 | 通常不缓存 |
| 安全性 | 会出现在 URL 与日志中 | 藏在请求体中 |
服务端行为可在 server.js 中核对:GET /api/accounts/:user 账户不存在返回 404 { error: 'User does not exist' }(L86-L88),而 POST /api/accounts 缺少参数返回 400、重名返回 409(L48-L55)。因此统一的 sendRequest() 必须基于 response.ok/response.status 处理这些非 200 响应,而不是只捕获网络异常。
关于仓库参考实现的数据层差异
需要说明一个"课程演进"背景:仓库中的 solution/app.js 是课程后期形态(数据落在 localStorage 并模拟异步延迟,见 getAccount/createAccount 的 setTimeout 实现,L95-L135),它已不再直接调用 fetch。但从源码结构可以清晰看到它忠实落实了本文的三种重构技术:文件顶部为 Constants 常量区,随后按 Intl helpers / Storage and state / DOM helpers / Router / Auth / Dashboard / Global listeners / Init 分节(每节都有 // ------ 分隔横幅),全部函数均有 JSDoc——这正是你重构自己带 fetch 版本 app.js 时应当对照的目标形态。
四、重构技术三:添加专业代码文档(JSDoc 与分节注释)
作业的文档标准
- 为函数书写说明用途、参数、返回值的文档注释;
- 对复杂逻辑或业务规则补充行内注释;
- 用分节标题把相关函数分组;
- 解释任何不直观的代码模式或浏览器特有兼容技巧。
JSDoc 风格示例(作业原文)
/**
* Authenticates user and redirects to dashboard
* @param {Event} event - Form submission event
* @returns {Promise<void>} - Resolves when login process completes
*/
async function login(event) {
// Prevent default form submission to handle with JavaScript
event.preventDefault();
// Your implementation...
}
从仓库参考实现中看到的"好注释长什么样"
翻阅 solution/app.js,可归纳出四类值得照抄的注释模式:
① 分节横幅,把 500 行文件切成可扫读的模块:
// ---------------------------------------------------------------------------
// Auth
// ---------------------------------------------------------------------------
② 解释"为什么"的行内注释,例如数据与 UI 的冗余处理(L330-L332):
// Some markups use a separate currency span; keep it empty when using formatted output
setTextByAnyId(['balance-currency', 'currency'], '');
③ 记录兼容性权衡(L104-L110):
function uuid() {
// Works on HTTPS and localhost; fallback otherwise
if (globalThis.crypto && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return 'tx-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
}
④ 用注释标注状态与存储策略(L172-L181):如 // Keep a frozen state object to avoid accidental mutations 解释为何使用 Object.freeze;// Persist active account only 说明持久化取舍。
写注释的核心纪律:注释负责"为什么",代码本身负责"是什么"。例如 updateRoute() 中 // Attach handlers after DOM is rendered(L241)解释了顺序依赖——这种"如果不这样会怎样"的信息才真正帮助未来的维护者(包括未来的你)。
五、成功标准:重构质量的三个档次自评
作业给出了可操作的分级标准,重构完成后可逐条打勾:
优秀实现(Exemplary)✅
- 所有魔法字符串与 URL 均抽取为命名清晰的常量;
- 公共请求逻辑收敛进可复用的
sendRequest(); - 函数带完整 JSDoc,说明用途与参数;
- 代码按分节标题逻辑分组,格式一致;
- 错误处理借助新请求函数得到加强。
合格实现(Adequate)✅
- 大部分重复值已抽取,残留少量硬编码;
- 已建基础
sendRequest()但可能未覆盖全部边界; - 关键函数有文档,个别解释尚可更完整;
- 整体有序,仍有优化空间。
需要改进(Needs Improvement)❌
- 大量魔法字符串与 URL 仍硬编码;
- 相似函数间仍存在明显重复;
- 注释缺失或无法解释代码目的;
- 缺乏清晰结构与逻辑分组。
六、重构后的验证清单:确保功能零回归
重构不改变行为——以下回归测试步骤来自作业原文:
- 覆盖全部用户流程:注册、登录、仪表盘展示、错误处理;
- 验证 API 调用:确认
sendRequest()对"创建账户"与"取回账户"两条路径都工作正常; - 测试错误场景:错误凭据、网络故障(可关掉 API 服务器模拟)都应显示友好错误而不是抛异常;
- 检查控制台输出:确认重构过程没有引入新错误。
配套的本地验证手段在 3-data 讲义 中也有说明:
# 启动 API 服务器(仓库根目录下)
cd api && npm start
# 验证 API 存活
curl http://localhost:5000/api
# 读取 test 预置账户数据
curl http://localhost:5000/api/accounts/test
讲义提到可用用户名 test 登录——它在 server.js 中预置了余额与三笔交易样例数据,便于快速验证仪表盘渲染。
七、提交规范与 Bonus 挑战
提交清单(作业原文要求)
重构后的 app.js 应包含:
- 清晰的分节标题,按功能组织;
- 一致的代码格式与缩进;
- 所有函数具备完整的 JSDoc 文档;
- 文件顶部有一段简短的注释,说明你的重构思路。
Bonus:编写 CODE_STRUCTURE.md
作业附加挑战:创建一个轻量文档文件(如 CODE_STRUCTURE.md),说明应用架构、各函数如何协同工作。可参考 solution 目录自带的说明文档 的写法,用"数据流"视角叙述:表单提交 → 校验 → 请求函数 → 状态更新 → navigate() 路由 → 仪表盘 init 回调刷新界面。
八、现实世界联结:为什么要做这件事
作业最后点明了这些练习在真实工程中的映射,这也是全文最有价值的一句话式总结:
- 代码评审(Code Reviews) 评审的正是本作业考察的可读性与可维护性;
- 技术债(Technical Debt) 在代码长期不重构、不文档化时持续累积;
- 团队协作(Team Collaboration) 依赖清晰文档化的代码,新成员才能快速上手;
- 缺陷修复(Bug Fixes) 在结构良好、抽象得当的代码库中会轻松得多。
从仓库成品 solution/app.js 反推可见:抽取常量、消灭重复、书写清晰文档这三项基本功,最终铺就的是一条从"能跑的 demo"通向"可维护的产品代码"的路——它不会让界面多一个像素,却决定了你的应用在未来三个月、三年里是否还有人敢改、愿意改。
进一步阅读
- 本课完整讲义:7-bank-project/3-data/README.md(Fetch 演进、DOM 安全更新、模板渲染)
- 前置表单课:7-bank-project/2-forms/README.md(FormData 与
createAccount原始实现) - 路由基础课:7-bank-project/1-template-route/README.md(
navigate()与路由表) - 重构目标形态:7-bank-project/solution/app.js
- 后端 API 源码:7-bank-project/api/server.js
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00