1.可读性
1.命名语义清晰
变量,函数,组件的命名是否表达清晰的意图
2.函数职责单一
函数是否只做一件事,是否容易理解
3.层次清晰
逻辑是否有良好的缩进,空行,段落感
4.注释合理
1.用重构取代注释
- 提炼函数:将一段需要注释解释的代码提炼成一个独立的函数,并用清晰的函数名代替注释
- 改变函数声明:函数的名称和参数都可以取一个更合适的名字,使其意图一目了然
- 引入解释性变量:
if(['apple','orange'].includes(val)) => if(isFruit)
2.避免冗余注释
比如 a++ 注释为a自增
注释只是重复了代码在做什么,而没有说明为什么这么做。这种注释毫无价值,反而会增加阅读和维护的负担
3.误导性注释
注释应该及时更新,比如注释说该方法返回“用户列表”,但代码实际返回的是“用户ID列表”,要及时同步
5.重复代码
是否存在复制粘贴的代码块,可否抽象。
抽象其实就是提炼函数
- 创建一个新函数:将重复的代码块放到一个全新的、命名良好的函数里。
- 替换重复代码:找到所有复制粘贴了这段代码的地方,将它们替换为对这个新函数的调用。
2.可维护性
1.模块划分合理
组件/函数划分是否清晰,是否存在“巨型文件”
2.层级深度控制
嵌套是否控制在2-3层
- 1.代码块嵌套if/else promise.then async/await
- 2.组件/模块嵌套(Vue中的多层标签)
优化方案
- 1.使用卫语句提前返回减少嵌套
function goodFunction(data, config, user) {
// 卫语句提前返回,减少一层嵌套
if (!data) return;
if (!config.isEnabled) return;
// 主体逻辑(现在只有1层嵌套起点)
data.items
.filter(item => item.isValid) // 用 filter 替代内层 if
.forEach(validItem => { // 第2层 (回调)
// 处理 validItem...
});
}
- 2.分解函数(提炼嵌套函数)ui嵌套同理提取子组件
function goodFunction(data, config, user) {
if (!data || !config.isEnabled) return;
// 将内层复杂的循环和判断提炼成一个函数
processValidItems(data.items);
}
// 新函数,自己的嵌套层级独立计算
function processValidItems(items) {
items
.filter(item => item.isValid)
.forEach(validItem => { // 第2层
// ... 逻辑
});
}
3.是否有硬编码
字符串,配置项是否写死
- 硬编码如何理解 “硬编码” 指的是将本应动态获取或可配置的值(如字符串、数字、配置项)直接以字面量的形式“写死”在代码逻辑中,将代码和配置耦合在一起。
- 魔法数字/字符串(Magic Numbers/Strings)
// 坏味道:这里的 86400 和 'active' 就是魔法数字和字符串
if (user.status === 'active') { // 这个状态码代表什么?还有其他状态吗?
sessionTimeout = 86400; // 86400 秒是多少?一天?为什么是一天?
}
- API URL / 环境配置
// 坏味道:环境相关的配置被写死
axios.get('http://123.456.78.90:8080/api/v1/production/users');
const apiKey = 'sk_live_abcd1234'; // 无法环境隔离,换环境需要更改代码
- 如何避免
- 定义常量 将魔法值提取为有清晰命名、全大写的常量。
// 好的做法
const SESSION_TIMEOUT_ONE_DAY = 24 * 60 * 60; // 或者直接 86400,但有了名字
const USER_STATUS_ACTIVE = 'active';
const USER_STATUS_INACTIVE = 'inactive';
if (user.status === USER_STATUS_ACTIVE) {
sessionTimeout = SESSION_TIMEOUT_ONE_DAY;
}
- 使用配置文件/环境变量
// config.js
export const API_BASE_URL = process.env.VUE_APP_API_BASE_URL; //通过不同的env文件管理
export const API_KEY = process.env.VUE_APP_API_KEY; // 通过构建工具注入
// api.js
import { API_BASE_URL } from '@/config';
axios.get(`${API_BASE_URL}/users`);
4.是否有技术债(TODO HACK)
是否存在明显的技术妥协或待修复点
1.技术债如何理解 “技术债” 是一个比喻,指为了快速实现短期目标(如赶工期、临时绕坑)而采用的一种并非最优(甚至粗糙)的解决方案,这会在未来带来额外的“利息”,即更高的维护成本。
代码注释中的 // TODO:, // HACK:, // FIXME: 就是技术债最明显的标记和欠条。
常见标记的含义:
// TODO::计划要做的功能或改进。通常表示功能不完整,但当前不影响主流程。// HACK::一种临时的、取巧的、不优雅的解决方案。通常是为了绕过某个棘手的问题(如第三方库的Bug、奇怪的业务逻辑)而写的代码,作者自己都知道这很糟糕。// FIXME::已知的Bug或问题,需要修复。比TODO更紧急,通常表示现有代码有问题,但可能暂时没时间修复或修复影响较大。 2.如何解决
创建工单(Issue) 定期偿还,并且在代码审查中查看hack有没有更好的方案能否提高优先级
3.代码结构/模式使用
1.状态/副作用合理
React中useEffect/useState使用得当
2.组件拆分合理
UI逻辑是否解耦,是否复用组件
3.模块组织规范
utils/services/constants 分层是否清晰
4.使用现代语法
是否使用Es6+ Ts 解构 箭头函数等
5.类型严谨
Ts项目是否写明所有类型
4.代码异味
Code Smell 需要避免代码坏味道
1.Macic Numbers(魔法数字)
写死的数字字符串,用常量代替,避免硬编码(#3.1)
2.Long Function (长函数)
函数超过50行且职责混乱
3.Nested Hell (嵌套地狱)
多层嵌套控制结构 / Promise回调地狱或滥用then
4.Copy-paste code(复制粘贴的代码)
同一逻辑出现多处
5.全局状态污染
直接使用全局变量或污染全局作用域
5.开发者思维成熟度
1.使用抽象/封装思想
逻辑提炼到utils、hooks、service层
核心思想:从“怎么做”到“做什么”
初级开发者的思维:专注于“怎么做”(How)。他们会将实现一个功能的所有步骤(逻辑)都直接写在UI组件或主函数里。
成熟开发者的思维:专注于“做什么”(What)。他们会将具体的“怎么做”隐藏起来(封装),只给调用者提供一个清晰的接口(函数名),告诉调用者这个函数“能做什么”。
“使用抽象/封装思想” 就是指这种将变化莫测的具体实现细节隐藏起来,暴露出稳定清晰的接口的能力。
1. utils (工具函数层)
-
是什么:
utils是 “Utilities” 的缩写,意为工具。这里存放的是与业务无关的纯函数。 -
封装什么:通用的、可复用的算法、数据格式转换、辅助计算等。
-
目的:消除重复代码(DRY原则) ,让任何地方需要相同功能时都能调用同一个函数。
-
例子:
- 格式化时间:
formatDate(timestamp, format) - 防抖/节流:
debounce(func, delay) - 深拷贝:
deepClone(obj) - 生成随机ID:
generateRandomId()
- 格式化时间:
2. 提炼到 hooks (React) 或 composables (Vue)
-
是什么:与UI和状态相关的可复用逻辑。这是对
utils的升级,它通常包含了组件生命周期、状态响应等。 -
封装什么:从组件中抽离出的状态逻辑(如监听窗口大小、管理表单状态、获取数据等),而不仅仅是纯计算。
-
目的:实现状态逻辑的复用,避免在多个组件中重复编写相同的逻辑代码(如
useState,useEffect)。 -
例子:
- 获取数据:
useFetch(url) - 监听窗口大小:
useWindowSize() - 管理本地存储:
useLocalStorage(key, initialValue)
- 获取数据:
3. 提炼到 service (服务层)
-
是什么:与后端API交互的集中管理层。它是对网络请求的抽象封装。
-
封装什么:API的URL、请求方法(GET/POST)、请求数据格式、响应数据格式转换、错误处理等。
-
目的:
- 隔离变化:当后端API地址或接口协议变化时,只需修改
service层,而不用到处去找哪个组件调用了这个API。 - 简化调用:为组件提供语义化的API,让组件不再关心网络请求的细节。
- 统一处理:可以在这里统一添加认证token、统一错误处理逻辑
- 隔离变化:当后端API地址或接口协议变化时,只需修改
2.模块边界清晰(避免跨层调用、耦合)
1.前端常见分层
- UI组件层:渲染视图、处理用户交互事件(React components Vue files)
- 业务逻辑/状态层:管理应用状态,处理复杂业务规则(Hooks Composables pinia vuex)
- 服务/数据层 封装所有外部数据获取和变更(service.js api.js)
- 工具层:提供通用的与业务无关的辅助函数(utils/ helpers/)
跨层调用指的是一个模块跳过了它本应直接依赖的中间层,去直接调用更底层的模块。这破坏了依赖关系的单向性。
2. 耦合是个啥
指的是模块之间相互依赖的程度。 “避免耦合” 特指要避免不必要的紧密依赖
例如父子组件,父组件不再需要处理子组件的逻辑,只是传递数据
3. 如何做到 模块边界清晰
- 单向数据流:依赖关系应该是单向的,通常是 UI层 -> 业务层 -> 服务层 -> 工具层。
- 依赖倒置:上层模块定义接口(需要什么数据),下层模块实现接口(提供数据),而不是上层直接依赖下层的具体实现。
- 明确职责:每个文件/模块只做一件事,并把它做好。
3.可访问性考虑(语义化标签、aria属性等 )
可访问性规范的核心是要求我们在开发网页或Web应用时,有意识地确保所有用户,包括残障人士,都能感知、理解、导航并与之交互。 它主要围绕两大技术体系:
1. 语义化标签
<button onclick="doSomething()">点击我</button> <!-- 这才是真正的按钮 -->
<header></header>
<nav></nav>
<main></main>
<section></section>
<article></article>
<aside></aside>
<footer></footer>
<ul><li></li></ul> <!-- 列表 -->
2. ARIA属性
是一组特殊的HTML属性,用于弥补HTML语义的不足,特别是在复杂的动态Web应用中
-
角色:
role="..."属性,用于明确告诉辅助技术某个元素是什么。role="button"(把这个div声明为一个按钮)role="navigation"(声明一个导航区域,类似于<nav>)role="alert"(声明一个紧急提示信息)
-
状态和属性:
aria-*系列属性,用于描述元素的当前状态和额外信息。aria-label="关闭弹窗"(为元素提供一个看不见的标签)aria-hidden="true"(告诉屏幕阅读器忽略此元素)aria-expanded="false"(告诉屏幕阅读器一个可折叠的菜单当前是收起状态)aria-describedby="tip-id"(指示另一个元素包含了对此元素的描述)
核心关系:优先使用语义化标签,当HTML标签无法充分表达含义时,再用ARIA进行补充和增强
4.使用设计模式/最佳实践
例如策略模式、职责链
5.体现领域建模思维
变量/函数反应业务模型 DDD 参考 juejin.cn/post/751791… 的设计思维。我个人认为小型的crud没必要用这种设计,不复杂的业务用简单的pinia和组件化设计就够了。 什么时候有DDD?
- 有明确的业务身份。一般有对应的id来取分是不是同一个,比如订单 购物车 商品
- 有明确的业务规则。是否有校验,计算,状态流转,比如购物车的商品增减比如订单的状态变更
- 有行为或者方法,不仅仅是存数据。 比如购物车可以做添加删除动作
- 生命周期比较长。 一般都有增删改查全流程业务动作。