锦书在线
80.52M · 2026-03-21
同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。
(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)
很多前端开发者都会遇到一个瓶颈:
代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。
想写出干净、优雅、可维护的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验。
这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。
帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。
你有没有遇到过这些场景:
半年前自己写的业务,今天改个小需求,打开文件之后第一反应:“这谁写的垃圾代码?”,再一看作者:是自己。
接手别人老项目,逻辑绕来绕去,偶尔看到一行注释:// TODO、// 这里有点问题,先这么写……然后就没有然后了。
为了“规范”,团队强行要求每个函数、每个变量都加注释,结果:注释和代码一起过期,甚至误导后来的人。
这篇文章就想解决一个现实问题:
日常写代码时,注释到底该怎么写?为什么这么写?坑会踩在哪?
目标是:让 3 年后的自己和队友,打开代码就能快速搞懂上下文,而不是骂人。
本文不是讲晦涩的底层原理,而是站在一线开发、项目规范的视角,用 Vue / 前端开发场景来聊聊“代码注释规范”。
能用清晰的命名和结构表达含义,就不要用注释补课。注释只做代码无法表达的“额外信息”。
很多团队会陷入两个极端:
极端 1:注释洁癖“好的代码不需要注释”,结果写一堆晦涩难懂的缩写变量,没人看得懂。
极端 2:注释狂魔几乎每一行都要注释:
// 声明一个变量 a
let a = 1;
// a 加 1
a++;
这种注释只会浪费时间、增加维护成本。
正确姿势:
优先改代码,让代码本身更清晰(变量名、函数名、拆分方法、抽象组件……)
其次用注释补充“代码表达不到的信息”,例如:
下面是我在项目里常用、非常推荐的四类注释场景。
What 代码自己能看出来,Why 只能靠你写出来。
// 获取用户列表
const users = await fetchUsers();
// 这里不能直接用缓存的用户列表:
// 1. 用户状态(在线/离线)是实时的
// 2. 后端会根据当前登录态过滤可见用户
// 所以每次都强制请求最新数据
const users = await fetchUsers({ forceRefresh: true });
这里的注释说明了为什么不能优化成缓存,以后有人想“优化性能”时,看到注释就会收手,避免踩坑。
在 Vue 组件、工具函数、API 调用中,最容易出问题的往往不是“实现细节”,而是使用前提:
// 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)附近最合适。
有些代码你也知道写得不优雅,但短期内又不能重构,比如:
与其未来被队友(或自己)怒喷:
“这谁写的?怎么这么鬼畜?”
不如提前写清楚原因。
/**
* 注意:后端这个接口是老系统保留的,字段命名非常诡异。
* - '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(),
};
}
以后谁要改这个接口时,看到注释就会明白:
有些模块就算代码写得再优雅,逻辑本身就是复杂的:
这种时候,不要指望“代码自解释”,加一段流程性注释是对所有人的救赎。
calculateOrderPrice.ts 里)
/**
* 订单价格计算规则(简化版):
*
* 1. 基础金额 = 所有商品单价 * 数量 之和
* 2. 商品级优惠:
* - 满减券:优先按商品分类应用,不能跨分类凑单
* - 折扣券:在满减之后应用,最多 2 张
* 3. 订单级优惠:
* - 平台券:在所有商品级优惠之后应用
* - 封顶逻辑:总优惠金额不能超过基础金额的 30%
* 4. 运费:
* - 满 99 元包邮
* - 其他情况按地区和重量计算
*
* 注意:
* - 所有金额都用「分」为单位在内部计算,避免浮点误差
* - 对外展示时再转换为「元」
*/
export function calculateOrderPrice(order: Order): OrderPriceDetail {
// 具体实现略
}
这里注释的价值在于:
知道“该写什么”之后,更重要的是:哪些注释写了只会拖团队后腿?
// 用户名称
const userName = getUserName();
/**
* 获取用户列表
*/
function getUserList() { ... }
这些注释的问题:
解决办法:
getList → getUserListdata → userList / formState
// TODO: 后续优化
// FIXME: 有 bug
半年后你自己也不知道:
推荐写法:
// TODO(v2.1): 表格数据量>1w时,滚动卡顿,需要引入虚拟列表
// 影响范围:订单列表、用户列表
// FIXME(2025-03-18 by 张三):
// 后端偶发返回重复的 orderId,导致 set 里丢数据
// 临时方案:前端用 (orderId + createdAt) 拼接作为 key,等后端修复后移除
规范建议:
TODO / FIXME 注释建议包含:
团队可以规定:重要 TODO / FIXME 必须对应 Jira/禅道/飞书任务号,比如:
// TODO(JIRA-1234 v2.2): 支持多语言,先写死为中文
注释一旦和代码不一致,就会变成误导信息。
/**
* 返回 true 表示用户未登录
*/
function isLoggedIn() {
return !!localStorage.getItem('token');
}
显然逻辑是“有 token 才是登录”,但注释写反了。
如果后来别人只看注释不看实现,很容易写出一堆反逻辑的代码。
经验结论:
写过时注释 = 欺骗未来的同事。
写了就要维护,维护不了就少写。
所以在团队规范里可以明确:
有同学特别喜欢在函数内部“边写边感想”,比如:
function fetchData() {
// 这里先判断一下是不是有缓存
// 如果有缓存的话就不用请求接口了
// 但是这里我们又觉得可能缓存会不准
// 所以又加了一个时间戳的判断
// 总之就是很复杂,先这么写吧……
}
这种注释的问题:
更好的做法:
重写示例:
/**
* 缓存策略说明:
* 1. 默认命中缓存,避免重复请求
* 2. 如果缓存时间超过 5 分钟,则强制请求最新数据
* 3. 切换用户时,必须清空缓存(用户隔离)
*/
function fetchData() {
// 实现略
}
下面从 Vue 项目常见几层结构出发,给一套可直接落地到项目里的注释建议。
props / emits / expose 是最值得写注释的地方因为它们构成了组件的“对外接口”。
示例:表单组件
<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>
这里的注释能让你在不看实现的情况下,就知道怎么用这个组件,这就是高价值注释。
当模板里出现大量条件判断 / 嵌套 v-if / v-for 时:
示例:
<template>
<!-- 展示可见的菜单项:
1. 已被后端标记为启用
2. 当前用户有权限
3. 如果是移动端,只显示前 5 个
-->
<MenuItem
v-for="item in visibleMenuItems"
:key="item.id"
:item="item"
/>
</template>
这里注释的作用:
visibleMenuItems 的过滤规则很多 Vue 3 项目会把复杂逻辑拆到:
useXXX.ts(逻辑复用)xxxService.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,
};
}
示例:Service 层
/**
* 获取订单详情:
* - 后端只在 status='PAID' 时返回 payInfo 字段
* - 如果订单已退款,refoundInfo 字段存在但可能为空对象
* - 接口有 500ms 左右的延迟,注意不要在输入框输入时频繁调用
*/
export async function fetchOrderDetail(orderId: string): Promise<OrderDetail> {
const { data } = await request.get(`/api/orders/${orderId}`);
return normalizeOrderDetail(data);
}
这些信息如果不写在这里,很难在代码中第一时间发现,却又对上层调用逻辑影响极大。
通用的小工具函数,命名清晰时可以不用注释:
export function formatPrice(amountInCent: number): string { ... }
如果函数有一些隐含约束或性能特征,就应该注释说明:
示例:
/**
* 深拷贝对象(仅用于小对象):
* - 基于 JSON 序列化,不支持函数 / Date / Map / Set
* - 遇到循环引用会抛错
* 适合用于「接口 mock 数据」等简单场景,不要在核心路径频繁使用。
*/
export function simpleClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
下面给一份可以直接落地的团队规范草稿,你可以根据实际情况微调。
props / emits / exposeTODO(JIRA-1234))下面用一个实际例子,演示如何从“混乱风格”改到“规范易读”。
<!-- 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)
<!-- 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 → loadOrdersfetchOrderList(便于复用与测试)这就是一个**“代码 + 注释配合良好”的例子**。
你可以按本文结构,稍作润色,就能产出一篇完整的博客。建议大致结构如下:
你可以直接把上文复制到 CSDN,稍微调整标题 / 小节顺序,并补充你自己项目中的真实故事和代码片段,会更有代入感和说服力。
注释不是给现在的你看的,是给“未来的你”和“曾经不认识你的同事”看的。
技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护。
哪怕每次只吃透一条规范,长期下来,差距会非常明显。
后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。
觉得有用欢迎 点攒 + 收藏 + 关注,不错过每一篇实战内容。
我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~