首页
/ Web-Dev-For-Beginners 银行应用重构实战:常量抽取、统一请求函数与 JSDoc 代码文档化

Web-Dev-For-Beginners 银行应用重构实战:常量抽取、统一请求函数与 JSDoc 代码文档化

2026-09-07 18:32:37作者:牧宁李

导读

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 调用要打的唯一后端"。

重构的三大目标

作业归纳为三点,也正是专业软件开发中的核心实践:

  1. 提升可读性(Readability)——让代码像散文一样可以被"顺着读";
  2. 提升可维护性(Maintainability)——将来加功能、改接口只动一处;
  3. 减少重复(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、重名返回 409L48-L55)。因此统一的 sendRequest() 必须基于 response.ok/response.status 处理这些非 200 响应,而不是只捕获网络异常。

关于仓库参考实现的数据层差异

需要说明一个"课程演进"背景:仓库中的 solution/app.js 是课程后期形态(数据落在 localStorage 并模拟异步延迟,见 getAccount/createAccountsetTimeout 实现,L95-L135),它已不再直接调用 fetch。但从源码结构可以清晰看到它忠实落实了本文的三种重构技术:文件顶部为 Constants 常量区,随后按 Intl helpers / Storage and state / DOM helpers / Router / Auth / Dashboard / Global listeners / Init 分节(每节都有 // ------ 分隔横幅),全部函数均有 JSDoc——这正是你重构自己带 fetch 版本 app.js 时应当对照的目标形态。


四、重构技术三:添加专业代码文档(JSDoc 与分节注释)

作业的文档标准

  1. 为函数书写说明用途、参数、返回值的文档注释;
  2. 对复杂逻辑或业务规则补充行内注释
  3. 分节标题把相关函数分组;
  4. 解释任何不直观的代码模式或浏览器特有兼容技巧

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 renderedL241)解释了顺序依赖——这种"如果不这样会怎样"的信息才真正帮助未来的维护者(包括未来的你)。


五、成功标准:重构质量的三个档次自评

作业给出了可操作的分级标准,重构完成后可逐条打勾:

优秀实现(Exemplary)✅

  • 所有魔法字符串与 URL 均抽取为命名清晰的常量;
  • 公共请求逻辑收敛进可复用的 sendRequest()
  • 函数带完整 JSDoc,说明用途与参数;
  • 代码按分节标题逻辑分组,格式一致;
  • 错误处理借助新请求函数得到加强。

合格实现(Adequate)✅

  • 大部分重复值已抽取,残留少量硬编码;
  • 已建基础 sendRequest() 但可能未覆盖全部边界;
  • 关键函数有文档,个别解释尚可更完整;
  • 整体有序,仍有优化空间。

需要改进(Needs Improvement)❌

  • 大量魔法字符串与 URL 仍硬编码;
  • 相似函数间仍存在明显重复;
  • 注释缺失或无法解释代码目的;
  • 缺乏清晰结构与逻辑分组。

六、重构后的验证清单:确保功能零回归

重构不改变行为——以下回归测试步骤来自作业原文:

  1. 覆盖全部用户流程:注册、登录、仪表盘展示、错误处理;
  2. 验证 API 调用:确认 sendRequest() 对"创建账户"与"取回账户"两条路径都工作正常;
  3. 测试错误场景:错误凭据、网络故障(可关掉 API 服务器模拟)都应显示友好错误而不是抛异常;
  4. 检查控制台输出:确认重构过程没有引入新错误。

配套的本地验证手段在 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"通向"可维护的产品代码"的路——它不会让界面多一个像素,却决定了你的应用在未来三个月、三年里是否还有人敢改、愿意改。

进一步阅读

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389