一套真正能落地的前端代码注释规范,从 Vue 项目实战出发,告诉你注释该写什么、不该写什么,避开常见坑点,写出让 3 年后的自己还能看懂的可维护代码。

同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。
(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)
很多前端开发者都会遇到一个瓶颈:
代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。
想写出干净、优雅、可维护 的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验。
这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。
帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。
前言:为什么要认真对待"写注释"这件小事?
你有没有遇到过这些场景:
-
半年前自己写的业务,今天改个小需求,打开文件之后第一反应:"这谁写的垃圾代码?",再一看作者:是自己。
-
接手别人老项目,逻辑绕来绕去,偶尔看到一行注释:
// TODO、// 这里有点问题,先这么写......然后就没有然后了。 -
为了"规范",团队强行要求每个函数、每个变量都加注释,结果:注释和代码一起过期,甚至误导后来的人。
这篇文章就想解决一个现实问题:
日常写代码时,注释到底该怎么写?为什么这么写?坑会踩在哪?
目标是:让 3 年后的自己和队友,打开代码就能快速搞懂上下文,而不是骂人。
本文不是讲晦涩的底层原理,而是站在一线开发、项目规范的视角,用 Vue / 前端开发场景来聊聊"代码注释规范"。
一、第一原则:好代码胜过好注释,但没有注释也不一定是好代码
1.1 一句话核心原则
能用清晰的命名和结构表达含义,就不要用注释补课。注释只做代码无法表达的"额外信息"。
很多团队会陷入两个极端:
-
极端 1:注释洁癖"好的代码不需要注释",结果写一堆晦涩难懂的缩写变量,没人看得懂。
-
极端 2:注释狂魔几乎每一行都要注释:
js// 声明一个变量 a let a = 1; // a 加 1 a++;这种注释只会浪费时间、增加维护成本。
正确姿势:
-
优先改代码,让代码本身更清晰(变量名、函数名、拆分方法、抽象组件......)
-
其次用注释补充"代码表达不到的信息",例如:
- 为什么要这么写(业务背景 / 历史原因 / 兼容性)
- 注意事项(性能、边界条件、已知坑)
- 和其他模块的约定(接口协议、调用顺序)
二、注释的四大黄金场景:该写什么?
下面是我在项目里常用、非常推荐的四类注释场景。
2.1 解释"为什么这么写"(Why),而不是"代码在干嘛"(What)
What 代码自己能看出来,Why 只能靠你写出来。
❌ 错误示例:只是重复代码
ts
// 获取用户列表
const users = await fetchUsers();
- 这行注释几乎就是在重复变量名,没有信息增量。
✅ 推荐示例:解释设计/业务原因
ts
// 这里不能直接用缓存的用户列表:
// 1. 用户状态(在线/离线)是实时的
// 2. 后端会根据当前登录态过滤可见用户
// 所以每次都强制请求最新数据
const users = await fetchUsers({ forceRefresh: true });
这里的注释说明了为什么不能优化成缓存,以后有人想"优化性能"时,看到注释就会收手,避免踩坑。
2.2 标记"约定"和"前置条件":别人需要遵守什么?
在 Vue 组件、工具函数、API 调用中,最容易出问题的往往不是"实现细节",而是使用前提:
- 参数有没有默认值?
- 有哪些边界情况?
- 调用顺序有没有依赖?
✅ Vue 组件示例:在 props / emits 上写注释
ts
// UserForm.vue <script setup lang="ts">
interface Props {
/**
* 表单模式:
* - 'create':新建用户,所有字段可编辑
* - 'edit':编辑用户,用户名不可修改
* - 'readonly':只读模式,所有字段禁用
*/
mode: 'create' | 'edit' | 'readonly';
/**
* 编辑/只读模式下必传:
* 后端返回的完整用户信息。
* create 模式下可以不传(内部会使用默认值)
*/
user?: User;
}
const props = defineProps<Props>();
/**
* 表单提交事件:
* - create: 提交的 user.id 由后端生成
* - edit: 必须包含原有的 user.id
*/
const emit = defineEmits<{
(e: 'submit', payload: User): void;
}>();
这里注释的作用非常明确:
- 告诉你
mode不同模式的差别 - 告诉你
user在什么模式下是必传的 - 告诉你
submit的 payload 长什么样
重点:这类注释是"契约"的一部分,写在类型(interface / props / emits)附近最合适。
2.3 记录"历史遗留"和"坑点说明":这块代码为什么这么丑?
有些代码你也知道写得不优雅,但短期内又不能重构,比如:
- 老接口的奇怪字段命名
- 历史版本遗留的时间格式
- 奇怪的兼容写法(低版本浏览器 / 特定设备)
与其未来被队友(或自己)怒喷:
"这谁写的?怎么这么鬼畜?"
不如提前写清楚原因。
✅ 示例:兼容老接口
ts
/**
* 注意:后端这个接口是老系统保留的,字段命名非常诡异。
* - 'usr_nm' 对应用户姓名
* - 'crt_tm' 是创建时间字符串,格式为 'YYYY/MM/DD HH:mm:ss'
* 暂时不能动这个接口,只在这里统一做一次映射。
*/
function normalizeLegacyUser(raw: any): User {
return {
id: raw.id,
name: raw.usr_nm,
createdAt: dayjs(raw.crt_tm, 'YYYY/MM/DD HH:mm:ss').toDate(),
};
}
以后谁要改这个接口时,看到注释就会明白:
- 这是历史债务,不是你写代码水。
- 如果要改,要 连后端 / 老系统一并考虑。
2.4 对复杂算法 / 业务流程做"概览说明":给后人一张思维导图
有些模块就算代码写得再优雅,逻辑本身就是复杂的:
- 多步骤审批流
- 复杂的优惠券 / 价格计算规则
- 权限控制(菜单 + 按钮 + 数据权限)
这种时候,不要指望"代码自解释",加一段流程性注释是对所有人的救赎。
✅ 示例:订单价格计算(假设你在 calculateOrderPrice.ts 里)
ts
/**
* 订单价格计算规则(简化版):
*
* 1. 基础金额 = 所有商品单价 * 数量 之和
* 2. 商品级优惠:
* - 满减券:优先按商品分类应用,不能跨分类凑单
* - 折扣券:在满减之后应用,最多 2 张
* 3. 订单级优惠:
* - 平台券:在所有商品级优惠之后应用
* - 封顶逻辑:总优惠金额不能超过基础金额的 30%
* 4. 运费:
* - 满 99 元包邮
* - 其他情况按地区和重量计算
*
* 注意:
* - 所有金额都用「分」为单位在内部计算,避免浮点误差
* - 对外展示时再转换为「元」
*/
export function calculateOrderPrice(order: Order): OrderPriceDetail {
// 具体实现略
}
这里注释的价值在于:
- 给出了整体流程(按步骤)
- 标明了关键约束(封顶 30%、单位是"分")
- 以后别人改逻辑时,有一个可以"对齐口径"的地方
三、哪些注释是坚决不要写的?
知道"该写什么"之后,更重要的是:哪些注释写了只会拖团队后腿?
3.1 重复代码的注释:浪费时间 + 增加维护成本
❌ 示例 1:重复变量名
ts
// 用户名称
const userName = getUserName();
❌ 示例 2:重复函数名 / 类型名
ts
/**
* 获取用户列表
*/
function getUserList() { ... }
这些注释的问题:
- 没有额外信息
- 只要一改函数名/变量名,注释就有可能不一致
- 时间久了变成"看着像对的,其实是错的"
解决办法:
- 优先把命名改清晰:
getList→getUserListdata→userList/formState - 确实没啥要补充的,就不要写注释,空着反而更安全。
3.2 "心情日志"注释:TODO / FIXME 不写清楚内容
❌ 典型反面教材:
ts
// TODO: 后续优化
// FIXME: 有 bug
半年后你自己也不知道:
- 要优化什么?
- 有什么 bug?复现步骤是什么?
- 是否已经修了?是否还有影响?
✅ 推荐写法:
ts
// TODO(v2.1): 表格数据量>1w时,滚动卡顿,需要引入虚拟列表
// 影响范围:订单列表、用户列表
ts
// FIXME(2025-03-18 by 张三):
// 后端偶发返回重复的 orderId,导致 set 里丢数据
// 临时方案:前端用 (orderId + createdAt) 拼接作为 key,等后端修复后移除
规范建议:
-
TODO / FIXME 注释建议包含:
- 触发条件 / 复现方式
- 影响范围
- (可选)目标版本/时间 & 责任人缩写
-
团队可以规定:重要 TODO / FIXME 必须对应 Jira/禅道/飞书任务号,比如:
ts
// TODO(JIRA-1234 v2.2): 支持多语言,先写死为中文
3.3 和真实逻辑不一致的注释:比没有注释更可怕
注释一旦和代码不一致,就会变成误导信息。
❌ 示例:注释没更新
ts
/**
* 返回 true 表示用户未登录
*/
function isLoggedIn() {
return !!localStorage.getItem('token');
}
显然逻辑是"有 token 才是登录",但注释写反了。
如果后来别人只看注释不看实现,很容易写出一堆反逻辑的代码。
经验结论:
写过时注释 = 欺骗未来的同事。
写了就要维护,维护不了就少写。
所以在团队规范里可以明确:
- 改动逻辑时,必须同步检查相关注释是否仍然正确
- Code Review 时,把**"注释是否仍然成立"**当成一个检查点
3.4 写在实现细节里的"小说故事":越写越乱
有同学特别喜欢在函数内部"边写边感想",比如:
ts
function fetchData() {
// 这里先判断一下是不是有缓存
// 如果有缓存的话就不用请求接口了
// 但是这里我们又觉得可能缓存会不准
// 所以又加了一个时间戳的判断
// 总之就是很复杂,先这么写吧......
}
这种注释的问题:
- 没有结构,像碎碎念日记
- 讲了一堆感受,没有讲清楚最终规则
- 以后别人看的时候,只会更迷惑
更好的做法:
- 把真正关键的规则整理成条目
- 其他的犹豫、不确定、吐槽,写到需求文档 / 评审记录里,而不是代码里
✅ 重写示例:
ts
/**
* 缓存策略说明:
* 1. 默认命中缓存,避免重复请求
* 2. 如果缓存时间超过 5 分钟,则强制请求最新数据
* 3. 切换用户时,必须清空缓存(用户隔离)
*/
function fetchData() {
// 实现略
}
四、不同层级怎么写?以 Vue 项目为例的一套落地规范
下面从 Vue 项目常见几层结构出发,给一套可直接落地到项目里的注释建议。
4.1 组件层(Vue SFC):注释重点放在哪里?
4.1.1 props / emits / expose 是最值得写注释的地方
因为它们构成了组件的"对外接口"。
✅ 示例:表单组件
ts
<script setup lang="ts">
interface Props {
/**
* 表单初始值:
* - 不传则使用内部默认值
* - 传入时会完全覆盖默认值(不要只传部分字段)
*/
modelValue?: UserFormModel;
/**
* 是否立即在 mounted 后拉取远程选项数据
* 默认 true;如果父组件要控制时机,可以传 false 后手动调用 `reloadOptions`
*/
autoLoadOptions?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
autoLoadOptions: true,
});
const emit = defineEmits<{
/**
* 表单提交成功时触发
* payload 包含表单内的所有字段
*/
(e: 'submit', payload: UserFormModel): void;
/**
* 任意字段变化时触发(用于实时保存草稿)
*/
(e: 'update:modelValue', value: UserFormModel): void;
}>();
defineExpose({
/**
* 重新拉取远程下拉选项
*/
reloadOptions,
});
</script>
这里的注释能让你在不看实现的情况下,就知道怎么用这个组件,这就是高价值注释。
4.1.2 复杂模板逻辑,优先拆组件,其次写块级注释
当模板里出现大量条件判断 / 嵌套 v-if / v-for 时:
- 优先选择"拆小组件 / 抽函数"
- 仍然复杂时,可以在逻辑块上方加一段块级注释,说明大体意图
✅ 示例:
html
<template>
<!-- 展示可见的菜单项:
1. 已被后端标记为启用
2. 当前用户有权限
3. 如果是移动端,只显示前 5 个
-->
<MenuItem
v-for="item in visibleMenuItems"
:key="item.id"
:item="item"
/>
</template>
这里注释的作用:
- 总结了
visibleMenuItems的过滤规则 - 方便别人查找时快速定位逻辑(比如"为什么这个菜单在移动端消失了?")
4.2 业务逻辑层(hooks / composables / services)
很多 Vue 3 项目会把复杂逻辑拆到:
useXXX.ts(逻辑复用)xxxService.ts(调用后端接口 + 业务规则)
这部分逻辑往往最需要注释,但注释也最容易乱写。
4.2.1 统一写在函数/方法签名上方,说明职责和返回值
✅ 示例:组合式函数
ts
/**
* 订单列表的分页 + 筛选逻辑:
* - 对外暴露响应式数据:list、loading、pagination
* - 支持关键字搜索、状态筛选
* - 初始化时自动加载一次数据
*/
export function useOrderList() {
const list = ref<Order[]>([]);
const loading = ref(false);
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0,
});
// ...
return {
list,
loading,
pagination,
reload,
resetFilters,
};
}
4.2.2 和后端接口交互的地方,注释协议差异/约束
✅ 示例:Service 层
ts
/**
* 获取订单详情:
* - 后端只在 status='PAID' 时返回 payInfo 字段
* - 如果订单已退款,refoundInfo 字段存在但可能为空对象
* - 接口有 500ms 左右的延迟,注意不要在输入框输入时频繁调用
*/
export async function fetchOrderDetail(orderId: string): Promise<OrderDetail> {
const { data } = await request.get(`/api/orders/${orderId}`);
return normalizeOrderDetail(data);
}
这些信息如果不写在这里,很难在代码中第一时间发现,却又对上层调用逻辑影响极大。
4.3 工具层(utils / helpers):何时需要注释?
-
通用的小工具函数,命名清晰时可以不用注释:
tsexport function formatPrice(amountInCent: number): string { ... } -
如果函数有一些隐含约束或性能特征,就应该注释说明:
✅ 示例:
ts
/**
* 深拷贝对象(仅用于小对象):
* - 基于 JSON 序列化,不支持函数 / Date / Map / Set
* - 遇到循环引用会抛错
* 适合用于「接口 mock 数据」等简单场景,不要在核心路径频繁使用。
*/
export function simpleClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
五、团队层面的"注释规范建议":可以直接抄到你们 RULE.md 里
下面给一份可以直接落地的团队规范草稿,你可以根据实际情况微调。
5.1 总体原则
- P1:注释是代码的一部分,写了就要维护。
- P2:注释说明"为什么 / 有什么坑 / 有什么约定",不要"翻译代码"。
- P3:宁可少写,也不要写错;宁可写在"合适位置",也不要乱丢。
5.2 "必须注释"的场景
- 对外接口:
- 组件的
props/emits/expose - 公共工具函数 / Service 层函数的入参、返回值说明(特别是有约束时)
- 组件的
- 复杂业务逻辑 / 算法:
- 在函数 / 模块顶部写整体流程说明或规则列表
- 历史遗留 / 兼容代码:
- 必须说明历史背景 / 兼容对象 / 计划替换方案
- TODO / FIXME:
- 必须写明触发条件 / 影响范围 / 预期目标
- 建议关联任务号(如:
TODO(JIRA-1234))
5.3 "禁止/不鼓励"的注释
- 重复代码内容的注释(变量名 / 函数名已经表达清楚)
- 空泛的 TODO / FIXME(未说明问题和上下文)
- 纯吐槽 / 情绪化注释
- 长篇大论但没有结构的"感想式注释"
六、一个完整的小案例:从"糟糕注释"到"可维护代码"
下面用一个实际例子,演示如何从"混乱风格"改到"规范易读"。
6.1 初版(很多人项目里真实存在的写法)
html
<!-- OrderList.vue -->
<script setup lang="ts">
// 订单列表组件
const data = ref([]);
const loading = ref(false);
const page = ref(1);
const pageSize = ref(10);
const total = ref(0);
// 获取列表
async function getList() {
loading.value = true;
// 调接口
const res = await request.get('/api/list', {
params: {
p: page.value,
ps: pageSize.value,
},
});
// 处理数据
data.value = res.data.list;
total.value = res.data.total;
loading.value = false;
}
// TODO: 后面要加筛选
</script>
<template>
<!-- 列表 -->
<Table :data="data" />
</template>
问题:
- 命名不清晰(
data/getList//api/list) - 注释几乎都是废话,没有说明任何约束
- TODO 没有说明到底怎么"要加筛选"
6.2 改进版:结合命名 + 注释一起升级
html
<!-- OrderList.vue -->
<script setup lang="ts">
/**
* 订单列表页:
* - 支持分页
* - 计划后续增加:状态筛选、关键字搜索(见 TODO)
*/
import { fetchOrderList } from '@/services/order';
const orders = ref<Order[]>([]);
const loading = ref(false);
const pagination = reactive({
page: 1,
pageSize: 10,
total: 0,
});
/**
* 拉取订单列表:
* - 后端的页码从 1 开始(不要传 0)
* - pageSize 最大不超过 100,否则后端会报错
*/
async function loadOrders() {
loading.value = true;
const res = await fetchOrderList({
page: pagination.page,
pageSize: pagination.pageSize,
});
orders.value = res.list;
pagination.total = res.total;
loading.value = false;
}
// TODO(v2.1): 增加筛选条件(状态 / 下单时间区间)
// - 与后端对齐接口 GET /api/orders:新增 status / startAt / endAt 参数
// - UI 上用折叠面板隐藏高级筛选
</script>
<template>
<OrderTable
:data="orders"
:loading="loading"
:pagination="pagination"
@change="loadOrders"
/>
</template>
这里我们做了几件事:
- 改变量名:
data→orders、getList→loadOrders - 提取 Service 层:
fetchOrderList(便于复用与测试) - 用注释补充约束和未来计划,而不是重复代码
这就是一个**"代码 + 注释配合良好"的例子**。
七、如何把"注释规范"写成一篇能发 CSDN 的文章?
你可以按本文结构,稍作润色,就能产出一篇完整的博客。建议大致结构如下:
- 引子(痛点故事)
- 自嘲+团队真实场景,引出"注释到底该不该写"的问题
- 第一原则:好代码优先,注释补充 Why & 限制
- 四大高价值注释场景
- Why / 约定 / 历史坑点 / 复杂流程概览
- 四类反面注释示例
- 重复代码、空 TODO/FIXME、过期注释、碎碎念
- 结合 Vue 项目结构的一套实践
- 组件层、业务层、工具层分别给建议和示例
- 前后对比小案例
- "糟糕版" vs "改进版"
- 总结 + 个人习惯分享
- 比如:写完函数先写注释再实现、Review 时检查注释等
你可以直接把上文复制到 CSDN,稍微调整标题 / 小节顺序,并补充你自己项目中的真实故事和代码片段,会更有代入感和说服力。
八、结语:写给 3 年后的自己
注释不是给现在的你看的,是给"未来的你"和"曾经不认识你的同事"看的。
- 多写一点"为什么这么写",少写一点"这行在干嘛"
- 多写一点"有什么坑 / 有什么约束",少写一点"将来再说"
- 写得少,但每一行都值钱,比写一堆废话强太多
技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护。
哪怕每次只吃透一条规范,长期下来,差距会非常明显。
后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。
觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。
我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~