likes
comments
collection
share

2023 年超全前端代码评审清单!!

作者站长头像
站长
· 阅读数 2

在项目开发中,代码评审是至关重要的一个环节,是整个项目健康运行的必要保障。笔者基于维护多年的 React 前端组件库 经验积累,总结了如下的代码评审清单,欢迎大家参考讨论。

另外欢迎大家访问:KDesign React 前端组件库 参与建设。

一. 代码风格和一致性

1. 是否遵循团队内部的代码风格指南

在项目开发过程中,编写符合团队编码规范的代码是尤为重要,否则阅读和维护代码需要花费很多的时间。在前端发展的前期阶段一般是团队统一制定规范约定代码格式,然后输出成文档,但团队成员无法在日常开发中去熟悉这些约定,无法真正的落地。后续出现了 eslintprettier, 我们一般结合两者去实现团队的代码规范。一般代码规范由代码格式和代码质量组成:

  • 代码格式:单行代码长度、tab 长度、空格、逗号表达式等问题
  • 代码质量: 未使用变量、三等号、全局变量声明等问题

这里是 eslintprettier的文档说明,从描述中我们可以归纳为:

  • eslint:主要处理代码质量校验,也能处理代码格式化处理
  • prettier:对代码进行格式化处理

由于 eslint 的规则不能完全包含 prettier 的规则,比如单行最大长度,对象大括号的前后空格等,以及 prettier 支持多语言,而 eslint 只支持类 js 文件(js, jsx, ts, tsx 等)。所以我们一般使用 eslint 来校验代码质量,prettier 来统一代码格式,并结合一些插件和其他第三方包来实现团队的统一的代码规范。以下是涉及的一些 npm 包:

npm 包说明
eslint校验js, jsx, ts, tsx代码质量
prettier统一代码格式
eslint-config-prettierprettier 格式化时与 eslint 的一些规则有冲突时,使用 prettier 的规则
eslint-plugin-prettier将 prettier 的格式化规则嵌入到 eslint 中,通过运行 eslint —fix 指令就能通过 prettier 的规则格式化代码
eslint-plugin-react支持对 react 语法校验的插件
eslint-plugin-import支持 ES2015+ (ES6+)导入/导出语法的检测,并防止文件路径和导入名称拼写错误的问题
eslint-plugin-jsx-a11y用于 JSX 元素的可访问规则的静态 AST 检查器
eslint-plugin-react-hooks验证 react hooks 的规则
stylelint校验css, less, sass等代码质量
lint-staged在git暂存文件上运行linters(eslint、prettier、stylelint)的工具,避免校验针对整个项目的所有文件
husky为 git 客户端增加 hook 的工具,可以在git的pre-commit阶段执行代码校验和格式化

提交代码前确认进行了eslint、prettier、stylelint的脚本校验,可以借助 lint-stagedhusky 工具实现自动化

2. 是否遵循团队内部的 git 提交规范

目前市面上使用较多的是 angular 团队的规范,它规定 commit message都包括三个部分:Header, Body 和 Footer,Header部分只有一行,包括三个字段:type(必需)、scope(可选)和subject(必需)。如果让我们自己去遵守这些规定有点不太现实,我们可以借助 commitizen 这个工具帮助我们按照上面的规范来生成commit信息

pnpm add -g commitizen

此时只需要在运行 git commit 的地方使用 git cz指令,按照指令进行操作就能生成符合规范的 commit message,但实际开发中根据项目不同我们可能需要有以下不同的需求

  • 针对 type 需要添加其他的类型,比如 cli ;然后我们还需要对新添加 type 添加说明;所有的说明必须是中文 对于以上情况我们使用comitizen 不能满足要求,此时我们需要借助 cz-customizable
pnpm add -D cz-customizable

安装完成后我们需要在 package.json 添加如下代码

"config": {
  "commitizen": {
    "path": "./node_modules/cz-customizable"
  }
}

虽然我们已经能通过 cz-customizable 完成commit message的规范化,但是我们依旧能通过 git commit指令来完成commit 信息的提交,commitlint能对我们最后生成的commit 信息进行验证

pnpm add -D commitlint @commitlint/config-conventional

@commitlint/config-conventional 是社区比较认可的校验commit message配置项,这里是具体的 校验规则

2.1 使用git cz 替换 git commit 来提交代码

2.2 Header的subject格式:动词(添加/修复/删除) + 具体改动 + 关联 issues

// 动词 + 具体改动
add a personal center page

// 动词 + 具体改动 + 关闭单个关联issues#333
delete unnecessary comments close #333

// 动词 + 具体改动 + 关闭多个关联issues#666、#888
fix an issue with dropdown menus rendering multiple times close #666, close #888

2.3 所有提交采用提 pr 模式提交代码到主分支

3. 是否遵循团队内部的命名规范

3.1 js 变量名使用小驼峰命名

// 推荐
const productName = 'book'

// 不推荐
const product_name = 'book'

3.2 css 变量使用 BEM 的命名规则

<!-- 推荐 -->
<div class="card">
  <h2 class="card__title">Welcome</h2>
  <p class="card__text">This is a sample card</p>
  <button class="card__button card__button--primary">Learn More</button>
</div>

<!-- 不推荐 -->
<div class="card">
  <h2 class="cardTitle">Welcome</h2>
  <p class="cardText">This is a sample card</p>
  <button class="button button-primary">Learn More</button>
</div>

3.3 常量是用全部大写、并用下划线连接

// 推荐
const MAX_VALUE = 100;

// 不推荐
const maxValue = 100

1.3.4 函数命名语义化,采用动词前缀,can/validate/has/find/get/set/is 等

// 推荐◊
function canEdit(user) {
  return user.isAdmin;
}

// 不推荐
function fn(user) {
  return user.isAdmin;
}

二. 代码结构和组织

清晰的文件和文件夹结构: 项目的文件和文件夹结构是否有意义,易于理解

规范的目录结构和文件命名可以让你更快的找到你需要的代码,并且可以让你更容易的维护和扩展。

推荐用法

  • 按功能划分文件夹
.
└── src
    ├── assets                        // 存放项目内部静态资源
    ├── components                    // 存放可复用的组件
    │   ├── Button                    // Button组件
    │   │   ├── Button.js
    │   │   └── button.css
    │   ├── Form                      // Form组件
    │   │   ├── Form.js
    │   │   └── form.css
    │   └── ...                       // 其他组件
    ├── pages                         // 存放页面
    ├── services                      // 存放与后台交互的服务
    ├── stores                        // 存放与状态管理相关的store
    ├── utils                         // 存放工具函数
    └──...
  • 按业务划分文件夹
.
└── src
    ├── authentication                 // 认证模块
    │   ├── components                 // 认证相关组件
    │   ├── styles                     // 认证模块样式
    │   └── ...                        // 其他认证模块文件
    ├── dashboard                      // 仪表板模块
    │   ├── components                 // 仪表板相关组件
    │   ├── styles                     // 仪表板模块样式
    │   └── ...                        // 其他仪表板模块文件
    └── ...                            // 其他模块

不推荐用法

  • 使用一个文件夹承载所有文件
.
└── src
    ├── file1.js
    ├── file1.css
    ├── file2.js
    ├── file2.css
    ├── file3.js
    ├── file3.css
    ├── file4.js
    ├── file4.css
    ├── file5.js
    ├── file5.css
    ├── ...
    └── longListofFiles.js
  • 层次结构过于复杂
.
└── src
    ├── module1
    │   ├── sub-module1
    │   │   ├── unnecessarilyLongFileName.js
    │   │   ├── unnecessarilyLongFileName2.js
    │   │   └── ...
    │   ├── unnecessarilyLongFileName.js
    │   ├── unnecessarilyLongFileName2.js
    │   └── ...
    ├── module2
    │   ├── sub-module2
    │   │   ├── unnecessarilyLongFileName.js
    │   │   ├── unnecessarilyLongFileName2.js
    │   │   └── ...
    │   ├── unnecessarilyLongFileName.js
    │   ├── unnecessarilyLongFileName2.js
    │   └── ...
    └── unnecessarilyLongFileName.js
  • 命名无意义
.
└── src
    ├── misc1
    │   ├── 1.js
    │   ├── 1.css
    │   └── 1.js
    ├── misc2
    │   └── 2.js
    └── ...

推荐命名方式

文件夹名称其他可能的名称意义
src/source/ , app/ , code/源代码文件夹
public/static/ , assets_public/静态资源文件夹,存放不经过编译的资源
dist/build/ , output/ , deploy/打包输出目录
assets/resources/ , media/ , files/存放项目所需的静态资源,如图片、字体等
components/widgets/ , ui_components/可复用的组件
pages/views/ , screens/ , layouts/页面级组件,与路由一一对应
styles/css/ , scss/ , sass/全局样式文件
utils/helpers/ , tools/工具类文件夹,存放一些辅助函数和工具类
services/api/ , http/ , network/与后端通信的服务、API请求等
hooks/custom_hooks/ , react_hooks/React自定义Hook函数
constants/constants_and_enums/ , enums/存放常量文件
config/settings/ , configs/项目配置文件
tests/specs/ , unit_tests/ , tests/单元测试文件夹
docs/documents/ , guides/项目文档
node_modules/packages/ , deps/Node.js模块依赖
build/scripts/ , webpack_config/构建脚本及相关配置文件
public/static_files/ , public_assets/公共资源文件夹,存放不经过编译的资源
mocks/fake_data/ , test_data/模拟数据,用于前端开发中的接口模拟
translations/i18n/ , localization/国际化文件夹,存放多语言翻译文件
logs/log_files/ , log_reports/存放日志文件

三. 注释和文档

1. 是否提供了足够的注释

1.1 文件注释: 在每个组件文件的顶部添加文件注释,包括作者信息、创建时间、最后修改时间等。可以使用注释块或文档注释。

/
 * @file MyComponent.js
 * @description This is a custom component for ...
 * @created 2023-01-01
 * @last-modified 2023-02-01
 * @author John Doe
 */

1.2 组件注释: 在每个组件的定义上方添加注释,说明组件的作用、用法、参数等信息。

/
 * MyComponent - A custom component for ...
 *
 * @component
 * @example
 * <MyComponent prop1="value1" prop2="value2" />
 */

1.3 函数和方法注释: 在每个函数或方法的定义上方添加注释,包括函数的作用、参数说明、返回值等信息。

/
 * Calculate the sum of two numbers.
 *
 * @param {number} a - The first number.
 * @param {number} b - The second number.
 * @returns {number} The sum of a and b.
 */
function addNumbers(a, b) {
  return a + b;
}

1.4 TODO 注释: 在需要添加功能或修复问题的地方添加 TODO 注释,描述需要完成的任务,并在注释中留下相关的联系方式或参考信息。

// TODO: Implement error handling for edge case

1.5 BUG 注释: 在发现 bug 但尚未修复的情况下,可以添加 BUG 注释,描述问题,并留下相关信息。

// BUG: This function crashes when input is null

1.6 fix注释: 刚修复的bug,可以添加注释,注明修复了什么问题。

// fix: xxx has been fixed

1.7 重要提示: 对于特别重要或复杂的代码块,添加一些重要提示,帮助其他开发人员更好地理解代码。

// IMPORTANT: This algorithm has a time complexity of O(n^2), optimize if possible.

1.8 版本历史: 在代码的关键变更处(如修复 bug、添加功能)添加注释,记录版本历史。

// v1.1.0 - Added new feature XYZ

1.9 文档链接: 如果有相关的文档或在线资源,可以在注释中添加链接,方便开发人员查阅。

// See documentation for more details: https://example.com/docs

1.10 代码片段引用: 引用了其他开源项目的一段代码,在注释中声明代码来源。

/*
fork by https://github.com/ElemeFE/element/blob/dev/build/md-loader/index.js
*/

2. 是否有清晰的README文档,介绍项目的结构和用法?

2.1 项目名称和描述: 在文件的顶部,明确项目的名称和简短描述。

# 项目名称

一个简短的项目描述,概述项目的目标和用途。

2.2 徽章: 如果适用,可以添加一些徽章,如npm下载量,当前版本号等,以提供更多信息。

[![NPM version](https://img.shields.io/npm/v/@kdcloudjs/kdesign-icons.svg?style=flat)](https://www.npmjs.com/package/@kdcloudjs/kdesign-icons)
[![NPM downloads](https://img.shields.io/npm/dm/@kdcloudjs/kdesign-icons?style=flat)](https://www.npmjs.com/package/@kdcloudjs/kdesign-icons)

2023 年超全前端代码评审清单!! 徽章生成方法可以参考lpd-ios.github.io/2017/05/03/…

2.3 目录: 如果项目结构较为复杂,可以添加一个目录,列出主要文件和文件夹。

## 目录
- [docs/](docs/) - 项目文档
- [src/](src/) - 源代码
- [tests/](tests/) - 测试代码

2.4 安装说明: 提供项目的安装步骤,包括所需的依赖项和如何安装它们。

## 安装

### 使用 npm 或 yarn 安装

$ npm install @kdcloudjs/kdesign --save
# 或者
$ yarn add @kdcloudjs/kdesign

2.5 使用说明: 提供简单的使用示例或指导,包括必要的命令或配置信息。

## 使用

运行以下命令启动项目:

$ npm start
# 或者
$ yarn start

2.6 贡献指南: 如果你希望其他人贡献到项目中,提供一些关于如何贡献的指南。

## 贡献

如果你想为项目做出贡献,请查看[贡献指南](CONTRIBUTING.md)。

2.7 许可证: 包含适用的许可证信息,以明确项目的使用条款。

## 许可证

本项目基于 [MIT 许可证](LICENSE) 发布。

2.8 版本历史:记录项目的版本历史,包括每个版本的变更。


- 1.0.0 - 初始发布 (2023-01-01)

2.9 联系方式: 提供与项目相关的联系信息,如作者的电子邮件或项目的主页。

## 联系

有任何问题或建议,请发送电子邮件至 author@example.com。

2.10 附加资源: 如果有相关的文档、示例、演示或其他资源,提供链接。

## 更多资源

- [项目文档](docs/)
- [示例应用](https://example.com/demo)

2.11 贴心提示: 在文件底部,可以添加一些关于问题报告、建议和其他注意事项的贴心提示。

## 注意事项

如发现任何问题,请在 [问题页面](https://github.com/yourusername/yourproject/issues) 中报告。

3. 是否有良好的API文档,如果是一个可重用的库或组件?

3.1 对于暴露出去的api需要列清楚使用方法,对于需要迭代版本的需注明版本号

| 属性 | 说明 | 类型 | 默认值 | 可选值 | 版本 |
| --- | --- | --- | --- | --- | --- |
| block | 开启该属性按钮将撑满父元素 | boolean | `false` | `true` `false` | 1.0.0 |
| bordered | 是否带边框 | boolean | `true` | `true` `false` | 1.0.0 |
| disabled | 按钮禁用状态 | boolean | `false` | `true` `false` | 1.0.0 |
| ghost | 幽灵属性,使按钮背景透明 | boolean | `false` | `true` `false` | 1.0.0 |
| loading | 按钮加载状态(加载中的按钮将不能触发点击事件) | boolean | `false` | `true` `false` | 1.0.0 |
| shape | 按钮形状 | string | `''` | `''` `circle` `round` `none` | 1.0.0 |
| size | 按钮尺寸 | string | `middle` | `small` `middle` `large` | 1.0.0 |
| type | 按钮类型 | string | `second` | `second` `primary` `ghost` `text` | 1.0.0 |
| onClick | 点击按钮时的回调 | (event) => void | `-` | `-` | 1.0.0 |
| htmlType | 设置 button 原生的 type 值 | string | `-` | `submit` `button` `reset` | 1.0.0 |

3.2 暴露出去的api是否真实有效,尽可能都演示在demo中。

3.3 演示demo规范

  1. 演示demo需要对所使用的API及组件默认行为和样式等进行尽可能详细的说明。避免让用户自己去推测,降低用户学习和使用成本。
  2. 每个组件的第一个demo,应该是组件最基本的用法,即展示组件在不传参数或者传入最基本参数情况下的效果。后面的demo应该尽可能按照参数的使用频率来排序。
  3. 每个demo所展示的API应该尽量精简,尽量不要一个demo中展示多个API的用法。
  4. 当API是枚举值时,demo中应尽量展示每个枚举值的效果。
  5. 演示demo代码,组件的参数大于三个或单行过长时需要每个参数占用一行来展示,避免出现横向滚动条。
  6. 一个标题尽量展示一个demo,避免一个标题中展示多个demo。
  7. boolean类型的参数,在demo中展示,设置为true的时候,不需要显式的设置为true,直接写参数名字即可。

4. 是否更新了变更日志(Changelog)?

4.1 同步更新相关文档。代码更新后有无更新日志记录,api文档,注释,README等,保证代码与说明统一

4.2 明确发布周期

#### 发布周期

- 修订版本号:每周末会进行日常 bugfix 更新。(如果有紧急的 bugfix,则任何时候都可发布)
- 次版本号:每月发布一个带有新特性的向下兼容的版本。
- 主版本号:含有破坏性更新和新特性,不在发布周期内。

4.3 issue一一对应。每一个问题的修复,后面都贴有对应的issue

## [1.7.13](https://github.com/kdcloudone/kdesign/compare/v1.7.12...v1.7.13)
`2023-06-02`
* anchor
  * 纵向锚点增加背景色 fix [#360](https://github.com/kdcloudone/kdesign/issues/360)
* modal
  * 修复maskClassName不生效 fix [#413](https://github.com/kdcloudone/kdesign/issues/413)
* stepper
  * 修复大数字时自增或自减操作丢失精度 fix [#385](https://github.com/kdcloudone/kdesign/issues/385)
* select
  * 多选时onChange第二个参数返回nodes数组 fix [#388](https://github.com/kdcloudone/kdesign/issues/388)
  * 修复单选初始化时设置value无行选中效果问题 fix [#387](https://github.com/kdcloudone/kdesign/issues/387)
* tree
  * 多选时onChange第二个参数返回nodes数组 fix [#388](https://github.com/kdcloudone/kdesign/issues/388)
  * 修复单选初始化时设置value无行选中效果问题 fix [#387](https://github.com/kdcloudone/kdesign/issues/387)
  * 去除多选时行选中效果 fix [#401](https://github.com/kdcloudone/kdesign/issues/401)
* tree-select
  * 多选时onChange第二个参数返回nodes数组 fix [#388](https://github.com/kdcloudone/kdesign/issues/388)
  * 修复单选初始化时设置value无行选中效果问题 fix [#387](https://github.com/kdcloudone/kdesign/issues/387)
* color-picker
  * 新增颜色类型下拉面板选项背景色的design token [#386](https://github.com/kdcloudone/kdesign/issues/386)
* 修复在窗口尺寸变化时首页退出登录的问题 [#399](https://github.com/kdcloudone/kdesign/issues/399)

四. 可读性和维护性

1. 清晰的函数和组件: 函数和组件是否足够小,功能单一

单一职责原则(Single Responsibility Principle): 每个函数或方法应该只负责一个明确的任务或功能。这有助于确保函数的职责清晰,代码更易于理解和维护。

// 不好的例子
function handleUserDataAndRenderUI(userData) {
  // 处理用户数据
  // 渲染 UI
}

// 好的例子
function handleUserData(userData) {
  // 处理用户数据
}

function renderUI() {
  // 渲染 UI
}

2. 精简代码

2.1 利用ES6新增的语法

ES6引入了箭头函数、解构赋值、模板字符串等新语法,可以更加简洁地实现某些功能,减少代码的嵌套。例如使用箭头函数来代替常规函数的写法,使用解构赋值来快速提取需要的数据。

// bad
function add(a: number, b: number): number {
    return a + b;
}
// good
const add = (a: number, b: number): number => a + b;

// bad
const user = { name: 'John', age: 30 };
const name = user.name;
const age = user.age;
// good
const { name, age } = user;


const name = 'John';
const age = 30;
// bad
const user = { name: name, age: age };
// good
const user = { name, age };

2.2 利用函数

将嵌套的代码块封装在一个函数中,避免代码的深度嵌套。这不仅有利于代码的可读性,还可以方便地对代码进行管理和维护。

// bad
const fun = () => {
  const arr = [1, 2, 3, 4]
  if (arr.length = 4) {
    arr.forEach((a: number) => {
      // ...
      // ...
      // ...
    });
  }
}

// good
const xxx = (a: number) => {
  // ...
  // ...
  // ...
}

const fun = () => {
  const arr = [1, 2, 3, 4]
  
  if (arr.length = 4) {
    arr.forEach(xxx);
  }
}

2.3 利用条件语句

使用switch 替换 多层级if/else

// bad
const fun = (a: number) => {
  if (a === 1) {
    // ...
  } else if (a === 2) {
    // ...
  } else if (a === 3) {
    // ...
  } else {
    // ...
  }
}

// good
const fun = (a: number) => {
  switch (a) {
    case 1:
      // ...
      break
    case 2:
      // ...
      break
    case 3:
      // ...
      break
    default:
      // ...
      break
  }
}

2.4 使用多态替换条件语句

// bad
const calculate = (a: number, b: number, operator: string) => {
  let result = null

  if (operator === 'add') {
    result = a + b
  } else if (operator === 'multiply') {
    result = a * b
  } else if (operator === 'divide') {
    result = a / b
  } else if (operator === 'subtract') {
    result = a - b
  }
  return result
}


// good
class Add {
  apply(a: number, b: number) {
    return a + b
  }
}
class Multiply {
  apply(a: number, b: number) {
    return a * b
  }
}
class Divide {
  apply(a: number, b: number) {
    return a / b
  }
}
class Subtract {
  apply(a: number, b: number) {
    return a - b
  }
}

const map = new Map([
  ['add', new Add()],
  ['multiply', new Multiply()],
  ['divide', new Divide()],
  ['subtract', new Subtract()],
])

const calculate = (a: number, b: number, operator: string) => {
  let result = null
  result = map.get(operator)?.apply(a, b)
  return result
}

2.5 避免深度嵌套

// 不好的例子
if (condition1) {
  if (condition2) {
    if (condition3) {
      // ...
    }
  }
}

// 好的例子
if (condition1 && condition2 && condition3) {
  // ...
}

3. 避免长函数和方法: 函数和方法是否过长,难以理解

3.1 函数长度限制

设置一个合理的函数长度限制。一般而言,函数不应该超过一屏幕的大小。如果函数变得太长,考虑将其拆分为多个小函数,每个函数执行一个特定的子任务。

// 不好的例子
function complexFunction() {
  // 长长的代码块
}

// 好的例子
function helperFunction1() {
  // ...
}

function helperFunction2() {
  // ...
}

function complexFunction() {
  helperFunction1();
  helperFunction2();
  // ...
}

3.2 提取共享代码

如果发现函数中有一些通用的代码块被多次使用,考虑将这些代码块提取到单独的函数中,以便在不同的上下文中共享。

// bad
function processUser(data: string) {
    if (data === 'admin') {
      return true
    }
    return false
}

function processAdmin(data: string) {
  if (data === 'admin') {
    return true
  }
  return false
}

// good
function sharedCode(data: string) {
  if (data === 'admin') {
    return true
  }
  return false
}

function processUser(data: string) {
    sharedCode();
}

function processAdmin(data: string) {
    sharedCode();
}

4. 使用设计模式

4.1 单例模式

单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点让程序能够访问该实例。单例模式通常用于全局配置、日志记录、数据库连接等场景。

class Singleton {
  constructor(name) {
    this.name = name;
    this.instance = null;
  }

  getName() {
    console.log(this.name);
  }
  
  static getInstance(name) {
    if (!this.instance) {
      this.instance = new Singleton(name);
    }
    return this.instance;
  }
}

5. 避免全局变量和函数: 是否避免了在全局范围内声明变量和函数

使用全局变量和函数可能会导致全局污染、命名冲突和代码耦合,以下方式不推荐使用:

  • 直接在全局范围内声明变量

    // globalVariables.js
    const globalVariable1 = 'Global variable 1'
    const globalVariable2 = 'Global variable 2'
    
    function globalFunction1() {
      console.log('Global function 1')
    }
    
    function globalFunction2() {
      console.log('Global function 2')
    }
    
    // main.js
    console.log(globalVariable1) // 直接在全局范围内使用全局变量
    console.log(globalVariable2)
    
    globalFunction1() // 直接在全局范围内调用全局函数
    globalFunction2()
    
  • 直接在原型上添加方法

    Array.prototype.customMethod = function () {
      console.log('I am a custom method on the Array prototype')
    }
    
  • 全局函数和变量直接导致的命名冲突

    function utilityFunction() {
      console.log('Utility function')
    }
    
    // 在其他地方的代码
    function utilityFunction() {
      console.log('Another utility function with the same name')
    }
    

避免使用全局变量和函数,可以采用以下方式来解决:

  • 模块化设计模式

    // module.js
    const moduleVariable = 'Module variable'
    
    function moduleFunction() {
      console.log('Module function')
    }
    
    export { moduleVariable, moduleFunction }
    
    // main.js
    import { moduleVariable, moduleFunction } from './module'
    
    console.log(moduleVariable)
    moduleFunction()
    
  • 立即执行函数表达式(IIFE)

    (function () {
      const iifeVariable = 'IIFE variable'
    
      function iifeFunction() {
        console.log('IIFE function')
      }
    
      // 在这里可以使用 iifeVariable 和 iifeFunction
    })()
    
    // 这里无法访问 iifeVariable 和 iifeFunction
    
  • 闭包

    // closure.js
    const closureFunction = (function () {
      const closureVariable = 'Closure variable'
    
      function closureInnerFunction() {
        console.log('Closure inner function')
      }
    
      return {
        variable: closureVariable,
        innerFunction: closureInnerFunction,
      }
    })()
    
    console.log(closureFunction.variable) // 使用变量
    closureFunction.innerFunction() // 使用函数
    

6. 模块化设计: 代码是否被合理地模块化,减少耦合度

不推荐用法

  • 单一模块负责多个不相关的功能

    // module.js
    export function getUserInfo(userId) {
      // 获取用户信息的逻辑
    }
    
    export function updateUserProfile(userId, newProfile) {
      // 更新用户资料的逻辑
    }
    
    export function renderUserPage(userId) {
      // 渲染用户页面的逻辑
    }
    
    export function handlePageNavigation(pageId) {
      // 处理页面导航的逻辑
    }
    
  • 直接在模块内创建依赖

    // userModule.js
    import { getOrderDetails } from './orderModule'
    
    export function getUserInfo(userId) {
      // 获取用户信息的逻辑
      const orderDetails = getOrderDetails(userId)
    }
    
  • 循环依赖

    // moduleA.js
    import { funcB } from './moduleB'
    
    export function funcA() {
      // 使用 funcB
      funcB()
    }
    
    // moduleB.js
    import { funcA } from './moduleA'
    
    export function funcB() {
      // 使用 funcA
      funcA()
    }
    
  • 暴露不必要的全局变量

    const unnecessaryGlobal = 'Unnecessary global variable'
    
    export function funcA() {
      console.log(unnecessaryGlobal)
    }
    
  • 直接操作 DOM

    function manipulateDOM() {
      document.getElementById('example').innerText = 'Manipulated content'
    }
    

推荐用法

  • 按功能划分模块

    // userModule.js
    export function getUserInfo(userId) {
      // 获取用户信息的逻辑
    }
    
    // orderModule.js
    export function getOrderDetails(orderId) {
      // 获取订单详情的逻辑
    }
    
    // main.js
    import { getUserInfo } from './userModule'
    import { getOrderDetails } from './orderModule'
    
    const userInfo = getUserInfo(123)
    const orderDetails = getOrderDetails(456)
    
  • 使用依赖注入

    // userModule.js
    export function getUserInfo(userId, logger) {
      // 获取用户信息的逻辑
      logger.log('Fetching user info')
    }
    
    // loggerModule.js
    export function createLogger() {
      return {
        log: (message) => console.log(message),
      }
    }
    
    // main.js
    import { getUserInfo } from './userModule'
    import { createLogger } from './loggerModule'
    
    const logger = createLogger()
    const userInfo = getUserInfo(123, logger) // 依赖注入
    
  • 发布-订阅模式

    // eventBus.js
    const subscribers = {}
    
    export function subscribe(event, callback) {
      if (!subscribers[event]) {
        subscribers[event] = []
      }
      subscribers[event].push(callback)
    }
    
    export function publish(event, data) {
      if (subscribers[event]) {
        subscribers[event].forEach((callback) => callback(data))
      }
    }
    
    // userService.js
    import { publish } from './eventBus'
    
    export function updateUserProfile(userId, newProfile) {
      // 更新用户资料的逻辑
      publish('userProfileUpdated', { userId, newProfile })
    }
    
    // userModule.js
    import { subscribe } from './eventBus'
    
    subscribe('userProfileUpdated', (data) => {
      // 处理用户资料更新的逻辑
    })
    

五. 错误处理和边界情况

1. 是否有适当的错误处理机制,避免未处理的异常

统一接口请求报错处理、函数运行报错使用 try/catch 包装、框架自带的处理(react)

1.1 使用 Axios 进行网络请求,可以使用拦截器来处理请求和响应,以及捕获错误

import axios from "axios";

// 请求拦截器
axios.interceptors.request.use(
  (config) => {
    // 在请求发送前可以进行一些处理
    return config;
  },
  (error) => {
    // 处理请求错误
    return Promise.reject(error);
  }
);

// 响应拦截器
axios.interceptors.response.use(
  (response) => {
    // 在响应之前可以进行一些处理
    return response;
  },
  (error) => {
    // 处理响应错误
    if (error.response) {
      // 服务器返回错误状态码
      console.error("Request failed with status:", error.response.status);
    } else if (error.request) {
      // 请求未收到响应
      console.error("No response received for the request.");
    } else {
      // 其他错误
      console.error("Error in request:", error.message);
    }

    return Promise.reject(error);
  }
);

1.2 在函数内使用 try/catch 包装可以捕获函数执行过程中的错误,防止错误导致整个应用崩溃

try {
  // 可能出错的代码
} catch (error) {
  console.error("An error occurred:", error);
}

1.3 在 React 中,可以使用错误边界(Error Boundaries)来捕获组件树中任何位置的 JavaScript 错误,并记录这些错误,同时展示备用 UI

import React, { useEffect, useState } from "react";

// 错误边界组件
function ErrorBoundary({ children }) {
  const [hasError, setHasError] = useState(false);
  const [error, setError] = useState(null);
  const [errorInfo, setErrorInfo] = useState(null);

  // 捕获 JavaScript 错误
  useEffect(() => {
    const handleError = (error, errorInfo) => {
      setHasError(true);
      setError(error);
      setErrorInfo(errorInfo);
    };

    // 添加错误处理器
    window.addEventListener("error", handleError);

    // 移除错误处理器(清理工作)
    return () => {
      window.removeEventListener("error", handleError);
    };
  }, []); // 空依赖数组表示只在组件挂载和卸载时执行

  // 渲染备用 UI 或正常子组件
  if (hasError) {
    return (
      <div>
        <h2>发生错误:</h2>
        <p>{error && error.toString()}</p>
        <p>组件树位置:</p>
        <pre>{errorInfo && errorInfo.componentStack}</pre>
      </div>
    );
  }
  return children;
}

<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>;

2. 是否考虑了边界情况和异常输入

2.1 特殊字符输入渲染

默认属性值: 在组件定义中,为属性设置默认值,以防止在没有提供必要属性时出现错误

function MyComponent({ myProp = "Default Value" }) {
  ...
}

空值合并操作符:空值合并操作符(??)只在左侧的值为 null 或 undefined 时使用右侧的值。而逻辑或运算符(||)运算符在左侧值为假值时(如空字符串、0、false、NaN、null、undefined)也会选择右侧的值, 在一些场景需要针对某些假值做特殊处理时,可以采用空值合并操作符

(value ?? "") !== "" && doSomething; // 针对传入值不为null、undefined、空字符串的情况
// 补充案例
const val1 = 0 ?? 42; // 0
const val2 = "" ?? "bar"; // bar
const val3 = null ?? "bar"; // bar
const val4 = undefined ?? "bar"; // bar

当需要根据数组长度进行渲染时:避免直接使用数组的 length 做条件渲染,而是需要拿到 length 属性与 0 做比较

data.length && <p>No items</p>; // 当data.length为0时,页面会直接渲染0
data.length > 0 && <p>No items</p>;

2.2 点击区域边界处理

  • 模态框(弹窗)关闭: 当用户点击模态框外部区域时,通常希望关闭模态框。需要确保在模态框外的点击事件不会误操作。在处理多层模态框的情况下,需要确保在打开多个模态框时,点击模态框以外的区域时能够正确地关闭最上层的模态框,而不是关闭所有模态框。
  • 可选择框:当用户点击可选择框时通常希望点击选择框的任何位置都能选中,包括点击可选择框除文字外的空白部位
  • 弹出框关闭按钮:当用户点击弹出框关闭按钮时通常希望只点击关闭按钮能关闭,但有时按钮的点击区域样式设置会比较大/小,导致可点击范围变大/小

2.3 拖动的边界处理

  • 拖动元素: 当用户需要通过鼠标或触摸手势拖动一个元素时,需要考虑元素在屏幕上的拖动范围,以防止拖动超出指定的边界
  • 拖动排序: 在可拖动排序的列表或表格中,拖动一个元素到新位置时,需要确保拖动操作不会使元素超出容器的边界或影响其他元素的位置
  • 拖动调整大小: 对于可调整大小的元素,用户可能希望通过拖动边缘或角来改变元素的尺寸。在这种情况下,需要确保拖动不会使元素的尺寸超出预定的范围(包含自定义尺寸或视口大小)

2.4 滚动的边界处理

  • 无限滚动加载: 在无限滚动加载的场景中,需要根据用户滚动的位置来触发加载新内容。在这种情况下,需要考虑何时停止加载新内容,以防止加载过多或加载不足
  • 可变换位置弹出框: 在滚动时,需要考虑弹出框的边界,以防止弹出框超出可视区域

2.5 键盘操作的边界处理

  • 焦点管理: 确保用户可以使用键盘(例如 Tab 键)在页面上的交互元素之间进行导航。考虑设置合适的焦点顺序,并确保焦点不会丢失在界面之外。
  • 键盘快捷键: 如果你的应用程序支持键盘快捷键,确保这些快捷键是易于记忆和使用的。避免与浏览器或操作系统的默认快捷键冲突,同时需要考虑键盘操作的边界,以防止操作超出预定的范围
const handleResize = (e) => {
  if (isResizing) {
    const newWidth = width + (e.clientX - startX);
    const newHeight = height + (e.clientY - startY);

    // 边界处理(边界大小自行定义或不能超过窗口大小)
    if (newWidth > 100 && newHeight > 100) {
      setWidth(newWidth);
      setHeight(newHeight);
      setStartX(e.clientX);
      setStartY(e.clientY);
    }
  }
};

3. 是否进行了输入验证和过滤

3.1 避免重复提交处理

使用防抖节流函数处理重复提交

const debounce = (fn, delay) => {
  let timer;
  return function (...args) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
};
const throttle = (func, delay) => {
  let isThrottled = false;
  return function (...args) {
    if (!isThrottled) {
      func.apply(this, args);
      isThrottled = true;
      setTimeout(() => {
        isThrottled = false;
      }, delay);
    }
  };
};

在发起请求时,设置一个标志位,表示当前请求正在进行中,确保在请求完成前不再发起相同类型的请求

function createRequestHandler(requestFunction) {
  let isRequesting = false;

  return async function (...args) {
    // 如果正在请求中,不执行重复请求
    if (isRequesting) {
      console.log("A request is already in progress. Please wait.");
      return;
    }

    // 设置请求标志位为true,表示请求开始
    isRequesting = true;

    try {
      // 执行实际的请求操作
      return await requestFunction(...args);
    } catch (error) {
      console.error("Request failed:", error);
    } finally {
      // 请求完成后的处理
      console.log("Request completed.");

      // 重置请求标志位为false,表示请求结束
      isRequesting = false;
    }
  };
}

// 示例:使用createRequestHandler包装一个请求函数
const wrappedRequest = createRequestHandler(async function makeRequest() {
  // 模拟异步请求,实际情况中会使用fetch或其他Ajax库
  await new Promise((resolve) => setTimeout(resolve, 2000));
  console.log("Actual request logic here.");
});

// 示例:连续调用wrappedRequest
wrappedRequest(); // 请求开始
wrappedRequest(); // 输出提示,不执行重复请求

3.2 表单验证正确后提交

在表单的 onSubmit 事件处理函数中执行最终的验证(包含长度验证、格式验证、数字范围验证、一致性验证、特殊字符验证、正则表达式验证、唯一性验证等),检查是否有错误。如果没有错误,则执行提交表单的操作;否则,阻止表单的默认提交行为,并显示错误信息。

六. 性能优化

1. 避免不必要的重渲染: 是否避免了不必要的 DOM 重绘和重新计算

使用 React.useMemoReact.useCallbackReact.memo 等函数避免父组件重渲染时导致子组件不必要的重渲染

import React, { useState, useMemo, useCallback } from "react";

// 子组件
const ChildComponent = React.memo(({ value, onClick }) => {
  return <button onClick={onClick}>{value}</button>;
});

// 父组件
const ParentComponent = () => {
  const [count, setCount] = useState(0);

  // 使用 useMemo 缓存计算结果
  const doubledValue = useMemo(() => {
    return count * 2;
  }, [count]);

  // 使用 useCallback 缓存回调函数
  const handleClick = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <h2>Parent Component</h2>
      <ChildComponent value={doubledValue} onClick={handleClick} />
    </div>
  );
};

export default ParentComponent;

2. 懒加载和代码分割: 是否使用了懒加载和代码分割以提高性能

2.1 懒加载

使用 React.lazySuspense懒加载

const MyComponent = React.lazy(() => import("./MyComponent"));

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <MyComponent />
      </Suspense>
    </div>
  );
}

2.2 代码分割

配置 webpack 的 splitChunks 进行代码分割

module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
  },
  optimization: {
    splitChunks: {
      chunks: "all", // 将所有类型的 chunk 都进行代码分割
      minSize: 30000, // 设置模块最小大小,小于此值的模块不进行代码分割
      maxSize: 0, // 设置模块最大大小,超过此值的模块将被拆分成更小的块
      minChunks: 1, // 模块被引用的次数达到 minChunks 才进行代码分割
      maxAsyncRequests: 5, // 按需加载时,并行请求的最大数量
      maxInitialRequests: 3, // 入口点的最大并行请求数量
      automaticNameDelimiter: "~", // 文件名之间的连接符
      name: true, // 根据模块和缓存组键自动生成名称
      cacheGroups: {
        vendor: {
          test: /[\/]node_modules[\/]/, // 匹配 node_modules 中的模块
          name: "vendor", // 分割出的 chunk 名称
          chunks: "all", // 在所有类型的 chunk 中进行分割
        },
      },
    },
  },
};

3. 优化资源加载: 是否合理使用 CDN,压缩图片,减少 HTTP 请求

3.1 合理使用 CDN

尽量将静态资源(如图片、字体等)托管在 CDN 上,减少本地资源的请求次数,提高性能

<img src="https://cdn.example.com/image.png" />

3.2 压缩图片

使用图片压缩工具(如 imagemin-webpack-plugin)进行图片压缩

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.(png|jpe?g|gif|svg)$/i,
        use: [
          {
            loader: "file-loader",
            options: {
              name: "images/[name].[hash].[ext]", // 输出图片的文件名和路径
            },
          },
          {
            loader: "image-webpack-loader", // 使用 image-webpack-loader 进行图片压缩
            options: {
              mozjpeg: {
                progressive: true,
                quality: 65, // 设置 JPEG 图片的质量
              },
              optipng: {
                enabled: false, // 禁用 OptiPNG,因为 file-loader 已经处理了图片优化
              },
              pngquant: {
                quality: "65-90", // 设置 PNG 图片的质量范围
                speed: 4,
              },
              gifsicle: {
                interlaced: false,
              },
              webp: {
                quality: 75, // 设置 WebP 图片的质量
              },
            },
          },
        ],
      },
    ],
  },
  // ...
};

3.3 压缩和合并资源

通过工具如 Webpack 将多个 CSS 和 JavaScript 文件压缩和合并,可以减少 HTTP 请求和文件大小,从而提高网站加载速度

3.4 使用缓存

通过设置 HTTP 缓存头或使用浏览器缓存机制,可以缓存已加载的资源,从而减少重复的 HTTP 请求

3.5 使用预加载和预渲染

<!-- 预加载样式表 -->
<link rel="preload" href="styles.css" as="style" />
<!-- 预加载脚本 -->
<link rel="preload" href="app.js" as="script" />
<!-- 预渲染页面 -->
<link rel="prerender" href="/path-to-page" />

4. 避免不必要的循环和计算: 代码中是否有冗余的循环和计算

4.1 使用 React.useMemo 缓存昂贵的计算结果

const expensiveResult = useMemo(() => {
  // 昂贵的计算逻辑
  return someExpensiveCalculation();
}, [dependency]);

4.2 当 useState 的初始值为一个比较复杂计算时,通常情况下最好不要将这个计算结果单独拆分出来

import React, { useState } from "react";

function Counter() {
  const [count, setCount] = useState(calculateInitialCount());

  function calculateInitialCount() {
    // 一些复杂的计算逻辑
    return 0;
  }

  // ...
}

在这个例子中,calculateInitialCount 是一个用于计算初始 count 值的函数。虽然你可以将其拆分成两个步骤,但是这样可能会导致计算逻辑被不必要地执行多次

const initialCount = calculateInitialCount();
const [count, setCount] = useState(initialCount);

4.3 选择适当的数据结构以便更轻松地执行所需的操作

例如,如果需要频繁的查找和删除操作,使用 SetMap 可能比数组更高效

4.4 使用 Web Workers

对于大量计算密集型的任务,可以考虑使用 Web Workers。Web Workers 可以在后台线程中运行,不会阻塞主线程,从而提高应用的响应性

七. 测试和质量保证

1. 单元测试覆盖: 是否有足够的单元测试覆盖率?

使用Jest等测试框架编写单元测试,确保每个函数和组件的行为符合预期。

// 示例测试用例
test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
})

执行 jest --coverage 就能在当前目录的coverage文件夹中看到代码覆盖率相关的信息了.如果需要执行指定文件的单测可以使用 --testPatchPatter 参数

文件更改后,需更新单测用例,保证单测能正常跑通保证单测覆盖率

2. 端到端测试: 是否进行了端到端测试,模拟真实用户行为?

使用Cypress等工具编写端到端测试,确保整个应用的流程正常运行。

// 示例Cypress测试
it('should display login form', () => {
  cy.visit('/login');
  cy.get('form').should('exist');
})

重要业务逻辑,可以使用 Jest/Cypress 来做端到端测试

3 持续集成和持续交付: 是否有持续集成和持续交付流程?

# 不好的例子
# 缺乏持续集成和持续交付

# 好的例子
name: CI/CD
on:
  push:
    branches:
      - main
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Install dependencies
        run: npm install
      - name: Run tests
        run: npm test
      - name: Deploy
        run: npm run deploy

使用CI/CD工具(如Jenkins、Travis CI)确保每次提交都通过自动化测试并能够顺利部署

八. 兼容性和可访问性

1. 跨浏览器测试: 代码是否在主流浏览器上正常运行?

1.1 针对 ie 浏览器(ie11)进行兼容

  • 使用 Babel 兼容 ES6+ 代码:
# 安装 Babel 相关依赖
npm install @babel/core @babel/preset-env --save-dev
// .babelrc 或 babel.config.js 配置文件
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "ie": "11"
        }
      }
    ]
  ]
}
  • 使用 polyfill 兼容 Promise
# 安装 core-js
npm install core-js@3 --save
// 在项目的入口文件中引入 polyfill
import 'core-js/stable'
import 'regenerator-runtime/runtime'
  • 使用 Babel 兼容箭头函数
# 安装 Babel 相关依赖
npm install @babel/plugin-transform-arrow-functions --save-dev
// .babelrc 或 babel.config.js 配置文件
{
  "plugins": ["@babel/plugin-transform-arrow-functions"]
}
  • 使用 polyfill 兼容不支持的 DOM 方法和属性
// 使用 classList polyfill
if (!('classList' in document.createElement('div'))) {
  require('classlist-polyfill')
}

1.2 针对 safari 浏览器进行兼容

  • 对日期格式进行兼容
// 使用标准的日期格式
const currentDate = new Date() // 生成当前日期

// 使用库(如 moment.js)处理日期
const formattedDate = moment(currentDate).format('YYYY-MM-DD')
  • 对 LocalStorage 进行兼容性
// Safari在隐私模式下可能禁用了LocalStorage
// 检查LocalStorage是否可用
if (typeof Storage !== 'undefined') {
  // 可以使用LocalStorage
  localStorage.setItem('key', 'value')
} else {
  // 处理LocalStorage不可用的情况
}

更多兼容性问题可以参考以下网站:

2. 移动设备兼容性: 代码在移动设备上是否有良好的兼容性?

2.1 响应式设计

使用响应式设计,确保你的网站或应用在不同尺寸的移动设备上都有良好的显示效果

/* 方案一:使用媒体查询来适应不同屏幕尺寸 */
@media screen and (max-width: 768px) {
  /* 在小屏幕下的样式 */
}
/* 方案二:使用相对单位rem,根据根元素的字体大小调整布局 */
html {
  font-size: 16px; /* 基础字体大小 */
}

body {
  font-size: 1rem; /* 相对于根元素的字体大小,等于16px */
  margin: 1rem; /* 与根元素字体大小相关的边距 */
}
/* 方案三:视窗单位,通过使用vw(视口宽度的百分比)和vh(视口高度的百分比)等单位,可以根据设备屏幕的大小来设置元素的大小。 */

/* VM示例:根据视口宽度和高度调整元素大小 */
body {
  font-size: 4vw; /* 相对于视口宽度的字体大小 */
}

section {
  width: 80vw; /* 相对于视口宽度的宽度 */
  height: 50vh; /* 相对于视口高度的高度 */
}

2.2 触摸事件兼容性

// 使用触摸事件处理方式,而不仅仅是鼠标事件,以提供更好的移动设备体验

// 示例:点击事件处理
element.addEventListener('touchstart', function (event) {
  // 处理触摸开始事件的逻辑
})

2.3 Viewport 设置

<!-- 使用适当的Viewport设置,确保在移动设备上显示的内容不会缩放 -->

<!-- Viewport 设置 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

2.4 字体和图像优化

/* 使用适合移动设备的字体和图像优化策略,以减小加载时间 */

/* 使用 Web 字体 */
body {
  font-family: 'Roboto', sans-serif;
}

/* 使用响应式图像 */
img {
  max-width: 100%;
  height: auto;
}

3. 无障碍性: 是否遵循无障碍标准,确保应用对残障用户友好?

3.1 必要的 aria 属性

<!-- aria-labe:提供一个可读文本来描述不具备语义的元素的目的 -->
<button aria-label="搜索">
  <svg aria-hidden="true" class="icon-search">
    <!-- 搜索图标的 SVG 图形 -->
  </svg>
</button>
<!-- aria-describedby:将元素关联到另一个提供额外说明的元素 -->
<label for="username">用户名:</label>
<input type="text" id="username" aria-describedby="username-description" />
<div id="username-description">请输入您的用户名,最多20个字符。</div>

3.2 语义化 HTML 标签

<!-- <nav>: 定义页面导航链接的容器,增强页面结构 -->
<nav>
  <ul>
    <li><a href="/">首页</a></li>
    <li><a href="/about">关于我们</a></li>
    <li><a href="/contact">联系我们</a></li>
  </ul>
</nav>
<!-- <article>: 表示独立的、完整的、可操作的内容单元 -->
<article>
  <h2>新技术改变生活</h2>
  <p>一些关于新技术如何改变我们日常生活的文章内容。</p>
</article>

3.3 键盘辅助支持

<!-- 焦点管理:确保通过键盘可以轻松导航和操作页面元素 -->
<button tabindex="0">可通过键盘聚焦的按钮</button>
//  事件处理:提供通过键盘触发的事件来操作页面元素
document.getElementById('myButton').addEventListener('keypress', function (event) {
  if (event.key === 'Enter') {
    // 执行按钮点击操作
    this.click()
  }
})

更多兼容性问题可以参考以下网站:

九. 日志和监控

1. 代码中添加足够的日志: 日志是否足够详细,方便调试和追踪问题?

1.1 日志级别: 使用不同的日志级别来标识日志的重要性,例如 debug、info、warn、error。确保在生产环境中只输出必要级别的日志,以避免信息泄漏。

// 示例
console.debug('This is a debug message.');
console.info('This is an informational message.');
console.warn('This is a warning message.');
console.error('This is an error message.');

1.2 清晰的信息: 每条日志应该包含足够的信息,以便于理解问题。包括有关上下文、变量值等的相关信息。

// 示例
console.error('Failed to load resource:', { url, status });

1.3 异常堆栈: 在捕获和记录异常时,包含堆栈信息以便于追踪问题的根本原因。

// 示例
try {
  // 一些可能抛出异常的代码
} catch (error) {
  console.error('An error occurred:', error);
}

1.4 唯一标识符: 在日志中包含唯一的标识符,以便于将相关日志关联在一起,特别是在处理分布式系统或微服务时。

// 示例
console.info('User authentication successful', { userId, requestId });

1.5 日志上报: 配置日志上报系统,将日志信息发送到服务器或第三方服务,以便于集中存储和分析。

1.6 用户行为日志: 记录用户的关键行为,以便在用户报告问题时更好地理解他们的操作。

1.7 日志监控工具: 使用专业的日志监控工具,例如 Sentry、LogRocket 等,以便于实时监控和分析日志。

1.8 版本信息: 在日志中包含应用程序的版本信息,有助于区分问题是在哪个版本中引入的。

// 示例
console.info('App version: 1.2.3');

1.9 开发环境日志: 在开发环境中输出更详细的日志信息,以便于调试和开发过程。

1.10 安全性考虑: 避免在生产环境中输出敏感信息,确保日志中不包含用户隐私数据。

2 性能监控工具: 是否集成了性能监控工具,追踪关键指标如加载时间、资源使用等?

2.1 测量关键指标: 覆盖页面加载的关键性能指标,如首次内容渲染(FCP)、可交互时间(TTI)、页面加载时间(PLT)。

2.2 使用 Performance API: 利用浏览器提供的 Performance API 来收集性能数据。这包括使用 performance.timing 对象和 performance.now() 方法。

// 示例
const timing = window.performance.timing;
console.log('Page load time:', timing.loadEventEnd - timing.navigationStart);

2.3 资源加载监控: 监控页面中的各个资源(如图片、脚本、样式表)的加载性能。可使用 performance.getEntriesByType('resource') 方法。

// 示例
const resources = window.performance.getEntriesByType('resource');
resources.forEach(resource => {
  console.log('Resource load time:', resource.duration);
});

2.4 错误监控: 监控前端错误,包括 JavaScript 错误和资源加载错误。使用 window.onerror 事件捕获 JavaScript 错误。

// 示例
window.onerror = function (message, source, lineno, colno, error) {
  console.error('JavaScript error:', message, source, lineno, colno, error);
};

2.5 用户体验度量: 使用 User Timing API 进行自定义用户体验度量,以便更好地了解用户与页面的交互。

// 示例
performance.mark('start-of-process');
// 执行一些操作
performance.mark('end-of-process');
performance.measure('process-time', 'start-of-process', 'end-of-process');

2.6 网络请求监控: 监控网络请求的性能,包括请求时间、响应时间、状态码等。可以使用 window.fetch 或 XMLHttpRequest,并记录相关信息。

// 示例
fetch('https://api.example.com/data')
  .then(response => {
    console.log('Request time:', response.headers.get('date'));
    return response.json();
  })
  .then(data => {
    // 处理数据
  })
  .catch(error => {
    console.error('Fetch error:', error);
  });

2.7 使用现成工具: 考虑使用现成的前端性能监控工具,例如 Google Analytics、New Relic、SpeedCurve 等,它们提供了丰富的性能数据和分析工具。

2.8 实时监控: 配置实时监控,以及时发现和解决性能问题。这可以通过将性能数据发送到监控平台或使用实时监控工具实现。

2.9 设备和浏览器兼容性: 考虑不同设备和浏览器的性能差异,确保性能监控工具在各种环境中都能正常工作。

2.10 定期分析和优化: 定期分析性能数据,识别瓶颈,并进行相应的优化。不断迭代,确保应用程序性能得到持续改进。

十. React 相关

1. useEffect 依赖项检查

错误的依赖项导致Effect不更新

const [count, setCount] = useState(0);

useEffect(() => {
  console.log('Effect triggered');
}, [count])

const handleClick = () => {
  setCount(count + 1);
}

2. JSX中的逻辑与(&&)运算符的写法不支持 ie11

因为IE11对于JSX的转译支持有限,无法正确解析一些特定的语法

// 不推荐写法
const App = () => {
  const isLoggedIn = false;

  return (
    <div>
      {isLoggedIn && <p>Welcome, User!</p>}
    </div>
  )
}


// 推荐写法:利用三元表达式
const App = () => {
  const isLoggedIn = false;

  return (
    <div>
      {isLoggedIn ? <p>Welcome, User!</p> : null}
    </div>
  )
}

3. setState 的参数为引用类型时,记得需要保证不为统一引用类型

import React, { useState } from 'react';

const MyComponent = () => {
  const [items, setItems] = useState([]);

  const handleClick = () => {
    // 错误示例:直接修改原数组,不会触发重新渲染
    items.push('New Item');
    setItems(items);
  };

  const handleFixedClick = () => {
    // 修正:创建一个新的数组副本,并传递给setState
    const updatedItems = [...items, 'New Item'];
    setItems(updatedItems);
  };

  return (
    <div>
      <button onClick={handleClick}>Add Item (Incorrect)</button>
      <button onClick={handleFixedClick}>Add Item (Correct)</button>
      <ul>
        {items.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

export default MyComponent;

4. 列表元素不要使用 index 作为 key

4.1 性能问题

当列表中的元素发生重排(顺序变动)时,使用索引作为 key 可能导致 React 误认为相邻的元素是相同的,从而导致不必要的重新渲染。这可能会影响性能,特别是在大型列表中

4.2 不正确的渲染

如果列表中的元素可能会在其他地方被改变(例如,排序、筛选等),使用索引作为 key 可能导致 React 错误地认为元素没有变化,从而导致不正确的渲染。这可能导致 UI 不一致性和 bug

5. 对 setTimeout、setInterval、addEventListener 的清除操作

import React, { useEffect, useState } from "react";

function MyComponent() {
  useEffect(() => {
    const handleClick = () => {
      // ...
    };

    // 添加setInterval
    const intervalId = setInterval(() => {
      // ...
    }, 1000);

    // 添加settimeout
    const intervalId = settimeout(() => {
      // ...
    }, 1000);

    // 添加事件监听器
    document.addEventListener("click", handleClick);

    // 在组件卸载时清除
    return () => {
      clearInterval(intervalId);
      clearTimeout(intervalId);
      document.removeEventListener("click", handleClick);
    };
  }, []); // 空依赖数组表示只在组件挂载和卸载时执行

  return (
    <div>
      <p>Count: {count}</p>
    </div>
  );
}

6. 使用函数式组件,尽量不要使用类组件

6.1 Hooks 的引入

React 16.8 引入了 Hooks,使得函数式组件可以拥有类似于类组件的状态管理和生命周期等功能。使用 Hooks,函数式组件可以更方便地处理状态、副作用等,避免了类组件中使用 class 关键字和繁琐的生命周期方法

6.2 更易于测试

函数式组件更容易进行单元测试。由于函数式组件更纯粹,没有生命周期方法和状态的概念,测试库可以更容易地对其进行测试

6.3 更好的逻辑组织

使用 Hooks 可以更好地组织组件的逻辑,将相关的逻辑拆分成独立的 Hook 函数,使组件更易于理解和维护

7. 合理使用useMemouseCallback

// useMemo 将会返回上一次计算的结果,而不会重新执行计算函数。
const expensiveCalculation = React.useMemo(() => {
    // 执行昂贵的计算
    return data.reduce((acc, value) => acc + value, 0);
}, [data]);

// useCallback 用于记忆函数引用,只有在依赖项发生变化时才重新创建函数。它也接收一个函数和一个依赖数组,并返回一个记忆后的函数。
const handleClick = React.useCallback(() => {
  // 处理点击事件
  onClick();
}, [onClick]);

结合React.memo 避免子组件重复刷新

// 子组件
const ChildComponent = React.memo(({ onClick }) => {
  console.log('Rendering ChildComponent...');

  return (
          <button onClick={onClick}>
            Click me
          </button>
  );
});

// 父组件
const ParentComponent = () => {
  const data = [1, 2, 3, 4, 5];

  const expensiveCalculation = useMemo(() => {
    console.log('Calculating...');
    return data.reduce((acc, value) => acc + value, 0);
  }, [data]);

  // 使用 useCallback 缓存回调函数,以确保仅在依赖项 data 变化时重新创建
  const handleClick = useCallback(() => {
    console.log('Button clicked!');
    // 处理点击事件
  }, [data]);

  console.log('Rendering ParentComponent...');

  return (
    <div>
      {/* 使用 React.memo 包裹子组件 */}
      <ChildComponent onClick={handleClick} />

      {/* 使用 expensiveCalculation */}
      {expensiveCalculation}
    </div>
  );
};

8. 确保每一个组件都有displayName,方便调试

const MyComponent = () => {
  // 组件的代码
};

MyComponent.displayName = 'MyComponent';

9. 组件名使用大驼峰,属性名称与style样式属性使用小驼峰

// Good
function MyComponent({backgroundColor}) {
  return <div style={{ backgroundColor }}>my component</div>
}

// Not recommended (不推荐)
function my_component({background_color}) {
  return <div style={{ 'background-color': background_color }}>my component</div>
}

10. 组件需要开放实例方法时,使用useImperativeHandle,开放出去的dom元素使用dom字段标识

import React, { useImperativeHandle, forwardRef, useEffect, useRef } from 'react'

const ChildComponent = forwardRef((props, ref) => {
  const domRef = useRef(null)
  // 使用 useImperativeHandle 暴露特定的方法给父组件
  useImperativeHandle(ref, () => ({
    dom: domRef.current,
  }))

  return <div ref={domRef}>{/* 子组件的内容 */}</div>
})
// 在父组件中使用子组件
const ParentComponent = () => {
  const childRef = useRef(null)

  useEffect(() => {
    // 通过 ref 调用子组件的 dom
    if (childRef.current) {
      childRef.current.dom
    }
  }, [])

  return (
    <div>
      <ChildComponent ref={childRef} />
    </div>
  )
}