前端注释规范:如何写"后人能看懂"的注释(附示例)

注释不是为当下的"我",而是为未来的"他(她/它)"。好的注释能让后来者快速建立上下文、理解设计意图、准确扩展与修复。本文给出一套面向真实工程的前端注释规范与可复制的写作模板,并配备 TypeScript、React、CSS 等多场景示例。
目标与原则
- 面向读者:假设读者对业务陌生但具备工程常识,避免"理所当然"的隐性知识。
- 解释意图,不复述实现:代码讲"怎么做",注释讲"为什么这么做"和"不能这么做的边界"。
- 贴边界、讲约束:记录输入输出的契约、边界条件、性能权衡、兼容策略与风险。
- 可维护:注释与代码保持同等"更新责任";随着逻辑变更同步修订。
- 可检索、可协作:统一标签与格式,便于搜索与团队协作。
注释分层与适用场景
- 文件/模块头:背景、职责、关键约束、外部依赖、维护者与变更记录。
- 类型/接口:字段含义、单位/取值范围、跨模块契约、兼容策略。
- 函数/方法:意图、参数/返回/异常、复杂度来源、边界与副作用。
- 复杂逻辑块:算法步骤、性能权衡、临时 Hack 的撤销条件。
- UI 组件:交互约束、无障碍(a11y)约定、状态来源与共享边界。
- 样式/CSS:命名约定(BEM/Utility)、层叠/覆盖策略、兼容与降级。
标签与格式约定
- NOTE:重要说明或上下文补充
- WARNING:风险提醒或易错点
- TODO:[Owner][Due][Link] 约定待办的责任与时限
- FIXME:已知缺陷的定位与修复思路
- HACK:临时性解决方案及撤销条件
- DEPRECATED:弃用说明与替代方案
统一形如:
TODO(@alice, 2025-01-10, JIRA-123): ...FIXME: ... Root cause ... Workaround ...
JSDoc/TSDoc 推荐字段
@param参数契约与单位@returns返回约定与可能的空值@throws异常场景与上游处理建议@example典型调用示例@remarks设计/业务背景补充@deprecated弃用提醒与替代
示例一:业务逻辑函数(含边界与约束)
ts
/**
* 计算订单最终价格(含税与优惠)
*
* 意图:
* - 将多来源优惠与税费规整为统一结算口径,保证后端账务一致性
*
* 契约与约束:
* - 所有金额单位为"分",避免浮点误差
* - 税费按地区配置的 VAT 率计算;部分品类免税
* - 折扣先于满减,且折扣后不得低于成本价
*
* 边界:
* - 负数输入直接抛错
* - 免税地区 taxRate=0
*
* @param amount 原价(分)
* @param rules 优惠与税费规则
* @returns 最终价格(分)
* @throws 当 amount < 0 或规则非法时抛出 Error
* @example
* calculateFinalPrice(10000, { discount: 0.9, vat: 0.13, minCost: 8000 }) // => 10170
*/
export function calculateFinalPrice(
amount: number,
rules: {
discount?: number; // 0-1,缺省视为不打折
vat?: number; // 0-1,地区增值税率
minCost: number; // 成本价(分)
category?: 'general' | 'food' | 'book';
}
): number {
if (amount < 0) throw new Error('amount must be non-negative');
const d = rules.discount ?? 1;
const discounted = Math.max(Math.round(amount * d), rules.minCost);
// 免税品类(示例:book)
const vatRate = rules.category === 'book' ? 0 : (rules.vat ?? 0);
const tax = Math.round(discounted * vatRate);
return discounted + tax;
}
要点:
- 明确单位与顺序(折扣→满减→税费)避免歧义。
- 把"为什么"和"不能怎么做"写清楚,如成本价约束。
示例二:React 组件(交互与 a11y)
tsx
/**
* DebouncedInput:带防抖的文本输入框
*
* 意图:
* - 在频繁输入场景降低后端压力,同时保证用户反馈流畅
*
* 交互约束:
* - 按 Enter 立即触发(绕过防抖),满足可用性期望
* - Esc 清空输入但不触发 onChange
*
* a11y:
* - 提供 aria-label 或显式 <label htmlFor> 绑定
* - 按 Enter 的即时提交行为需告知屏幕阅读器
*/
export function DebouncedInput({
value,
onChange,
delay = 300,
ariaLabel,
}: {
value: string;
onChange: (v: string) => void;
delay?: number;
ariaLabel?: string;
}) {
const [inner, setInner] = useState(value);
useEffect(() => setInner(value), [value]);
useEffect(() => {
const t = setTimeout(() => {
if (inner !== value) onChange(inner);
}, delay);
return () => clearTimeout(t);
}, [inner, delay]);
return (
<input
aria-label={ariaLabel}
value={inner}
onChange={(e) => setInner(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') onChange(inner); // 即时触发
if (e.key === 'Escape') setInner(''); // 清空但不触发
}}
/>
);
}
要点:
- 把"绕过防抖"的意图写明,避免误改。
- 交互与无障碍约束要可检索。
示例三:异步与重试(取消/退避)
ts
/**
* fetchWithRetry:带指数退避与可取消的请求
*
* 设计权衡:
* - 避免雪崩:指数退避 + 抖动(jitter)
* - 保持可响应:支持 AbortController 取消
*
* 风险与约束:
* - 仅对网络/5xx 重试,4xx 视为业务失败不重试
* - 最大重试次数可配置
*
* TODO(@owner, 2025-01-31, TICKET-456): 采集重试遥测并接入告警
*/
export async function fetchWithRetry(
input: RequestInfo,
init: RequestInit & { retries?: number; baseDelay?: number },
signal?: AbortSignal
): Promise<Response> {
const retries = init.retries ?? 3;
const base = init.baseDelay ?? 200;
for (let i = 0; i <= retries; i++) {
try {
const res = await fetch(input, { ...init, signal });
if (!res.ok && res.status < 500) return res; // 4xx 不重试
if (res.ok) return res;
// 5xx 将进入重试
} catch (e) {
// 网络错误继续重试
if (signal?.aborted) throw e;
}
const jitter = Math.random() * 0.2 + 0.9; // 0.9~1.1
const delay = Math.round(base * Math.pow(2, i) * jitter);
await new Promise((r) => setTimeout(r, delay));
}
throw new Error('Max retries reached');
}
要点:
- 写清"哪些错误重试",避免误重试导致业务雪崩。
- 将遥测/告警作为 TODO,明确责任与时限。
示例四:CSS 命名与兼容策略
css
/* Header 导航
* 命名:BEM(.header__nav, .header__item)
* 层叠策略:工具类优先(.u-hidden),避免 !important
* 兼容:老版 iOS 不支持 position: sticky,降级为 static
*/
.header {
position: sticky; /* NOTE: iOS < 12 降级 */
top: 0;
z-index: 100;
background: var(--color-bg);
}
.header__nav {
display: grid;
grid-template-columns: repeat(4, minmax(80px, 1fr)); /* WARNING: 列宽低于 80px 会影响可点击区域 */
gap: 8px;
}
.u-hidden {
display: none;
}
要点:
- 命名约定、层叠策略与兼容/降级清晰可见。
- 用 NOTE/WARNING 提醒关键风险。
示例五:模块头部模板(可复制)
ts
/**
* 模块:优惠结算引擎(Pricing Engine)
*
* 背景:
* - 多渠道优惠并行叠加导致口径不一,需统一规则避免账务对不上
*
* 职责:
* - 计算折扣、满减、税费,并保证不低于成本价
*
* 外部依赖:
* - config/vat.ts(地区 VAT 配置)
* - services/catalog.ts(品类属性)
*
* 关键约束:
* - 金额单位统一为"分"
* - 折扣先于满减;免税品类 VAT=0
*
* 维护者:@alice(主)、@bob(备)
* 变更记录:
* - 2025-11-12:引入免税策略并校正边界
*/
写作模板(三段式)
- 背景:这段代码存在的业务/技术上下文是什么?
- 意图:期望达成的目标、选择的策略与不选的理由。
- 要点:契约/边界、副作用、风险与后续工作。
示例片段:
ts
// 背景:移动端输入频繁导致后端压力高
// 意图:防抖 + Enter 直发,兼顾性能与体验
// 要点:Esc 清空不触发;a11y 需有 label
不该写的注释(反例)
- 重复代码含义:"i++ 自增 1"这类废话。
- 时间线式注释:"2023-01-01 修复 bug"请放到版本/变更日志或模块头。
- 永久 TODO:没有责任人/时限/链接的 TODO 会烂尾。
- 过时注释:与代码不一致的注释比没有更糟。
- 评注式注释:"这代码太丑"------没有行动信息的情绪表达。
改写示例:
- 坏注释:
// 这里做了折扣计算 - 好注释:
// 折扣先于满减;折后不得低于成本价(防负毛利)
团队协作与维护建议
- 代码评审包含"注释检查":是否解释了意图与边界?是否落了标签?
- 变更驱动更新:任何影响契约/边界的改动必须同步修订注释。
- 定期清理:搜索
TODO|FIXME|HACK|DEPRECATED,核对是否过期。 - 本地化策略:中文为主,保留关键英文术语(如 debounce、backoff、a11y)。
快速清单(可打印)
- 是否解释了"为什么"和"不能怎么做"?
- 是否明确了单位、范围、顺序与副作用?
- 是否标注风险、兼容策略与撤销条件?
- 是否使用统一标签并附责任人/时限/链接?
- 是否与最新代码一致,且可被搜索到?
按此规范写注释,后人不必"读心术",你也能在未来的某一天感谢过去的自己。