Code Review规范

266 阅读17分钟

一、Code Review 概述

Code Review (简称 CR ,字面意思就是对开发编写的源代码进行评审,找出并修正软件开发过程中出现的错误,提升系统白盒测试能力保障软件的质量。

二、CR目的

现在随着互联网流量的剧增,必须要保障代码质量才能保障业务高稳定高可用,另外从编码者的角度来看,编码的时候全面地去考虑更好的实现,提交的不一定是一份高质量的代码,在运行中可能出问题。

CR追求的是质量而不是数量,是为了团队成员之间相互了解学习,加深成员对系统的理解,使团队成员的代码更加健壮,提早发现代码缺陷。

2.1 For代码提交者角度看CR

  • 在Review过程中,同事之间可以相互学习、启发,最后促进个人和团队的成长。找出核心业务代码的漏洞,提前避免一些BUG甚至故障的发生。
  • 代码被 CR 后,评审者也相当于参与了这次开发,在团队中相当于做了一次人员“备份”。

2.2 For代码评审者角度看CR

  • 代码编写规范方面:遵循公司编码规范避免带来可读性低的问题。
  • 业务逻辑和边界问题:减少业务逻辑和边界问题是否有遗漏,对现有业务是否有影响。
  • 错误处理:参数是否进行了校验,远程调用超时或服务超时或不可用时,是否有兜底解决方案。

三、CR前期准备工作与介入时机

3.1 做好前置工作

  • 必须要让“被review者”心态开放,只有心态的开放,才能让你听到更多的声音,否则毫无意义。
  • “反馈者”表述的真实性,千万不要客套话,不要虚假的繁荣,简单直接、对事不对人。
  • 主持人可以在开场时做好铺垫,告诉大家这些原则,让大家心里有数,支持在进行的过程中也可以暗自观察,然后适时的提醒。

3.2 抓住关键点

review的现场都是一个能够对话的现场,对话的背后是心智模式,要抓到关键问题深入探讨。主持人的几点提醒:

  • 请“被review者”聆听为主,不清楚的可以进行澄清,但千万不要解释,过于防备可不行。
  • “反馈者”在提问的同时,也能表达问题背后的意图和担心。
  • 若感到“反馈者”提出的建议或问题是关键的,主持人可以就这个问题发表一下自己的看法,或者请“被review者”谈谈听到后的体会,促进这个问题能有机会被谈的更深入。

3.3 介入时机

image.png

SIT测试阶段前由测试发起 CR ,最晚要在 UAT 前1天。

四、CR核心内容与细则事项

4.1 核心内容

  • 目标共识: 一定要确认或是回顾对于结果的共识,这点是非常重要的,这点就象是review的指南针,方向错了,后面所有的都会偏离轨道。
  • 抽丝剥茧: 我们看事情不能只注重表面,必须根据下属提供的数据进行深入沟通,了解数字背后的理解,比如说你对于现在的目标完成进度情况是如何思考的?
  • 核心提升: 对于目标的达成,我们必须要在一个时间段内,我们都需要抓少数的核心关键数据,甚至只用抓住一个数据,比如说在休养生息期,我们只抓业务拜访量,在高歌猛进期,我们只抓业绩达成率,在业务转型期,我们只抓新客户数量,在核心提升的数据上,上级应该和下属达成共识,并形成有利的监督机制,反复强调,死抓核心需要提升的点,但最终也要与公司的大战略保持一致才行。

五、CR流程落实

5.1 选择合适的工具、合理的介入时机、选取合适的形式

  • CR 工具: 使用代码编写工具,Eclipse、Visual Studio Code、XCode。

  • CR 时机: SIT测试阶段前,最晚UAT测试前1天。

  • CR 形式:三人为一组,并和故障捆绑。

    • 小功能模块的开发、不超过500行的代码的轻量级CR:邀请同组的人进行CR,结合反馈意见进行修改。
    • 大功能模块的开发或是涉及架构变动的CR:组织团队人员进行评审,开发者讲自己的设计逻辑,评审人给出意见。

5.2 For代码提交者

  • Review时机:对于普通bugfix或优化,CR最迟要在发布前一天或发布当日上午;对于迭代项目,CR最迟应在提交测试前一天。
  • 持续增强:每次提交变更都是正确和完整的,合并到代码库中持续增强系统。
  • 去除无用改动:去掉无用的注释和没有逻辑改动的文件。
  • 合并提交:如果仅有一个分支,且全部是自己的改动,合并成一次提交,避免漏掉某些重要Review。
  • 分支名:使用具体服务_迭代_年月日作为分支名。
  • 相关性:组内业务的相关改动放在单独分支里方便Review ,避免在一个大而全的项目分支里Review。

5.3 For代码审查者

  • 影响评估:变更代码是否覆盖了需求的范围,没有遗漏。
  • 代码组织:变更代码是否放置在合适的分层或模块。
  • 接口设计:接口设计的参数与返回值是否正交,足够灵活。
  • 变化分离:变更代码是否将特定业务逻辑耦合写在通用逻辑里面。
  • 公共逻辑:变更代码是否更改了公共逻辑,影响了其他的业务或更大的业务范围。
  • 引用变更:修改了类引用的地方尤其要注意。
  • 事实单一:变更代码是否重复了多段相似代码,是否同一个业务点采用了多个不同的实现方式。
  • 基类修改:变更代码是否直接修改基类而影响所有的子类,是否使用适配器、委托模式来扩展基类的功能。
  • 检查业务逻辑:一定要向代码提交者请教清楚相关的业务才可以。
  • API调用:是否是关键API,需要对返回数据添加日志,方便问题排查。
  • 重复创建:在循环里重复创建新的可复用对象或开销很大的对象。
  • 超时设置:API 调用、数据库访问等是否设置了超时机制。
  • 边界:边界情况是BUG事故多发地,检查是否使用了单元测试覆盖。
  • 日志:业务的关键状态和关键路径是否有日志覆盖,关键业务是否有日志埋点。
  • 性能:是否在循环里对每一个实例调用接口而不使用批量接口,是否使用了多重循环影响算法性能。
  • 并发:Controller, Service类中的实例变量只允许 Service, DAO 的单例,不允许业务变量实例;是否使用了线程池或连接池而不是单个的线程连接。
  • 事务:多个 SQL 语句提交时是否需要使用事务。
  • SQL:添加SQL语句时候尤其表量很大的时候一定看下表的DDL ,是否使用索引避免慢查询。
  • 安全:敏感信息是否使用了加密,是否可能存在 SQL 注入攻击的可能。

HTML 篇

启用标准模式

使用 HTML5 的 doctype 来启用标准模式

<!DOCTYPE html>

统一使用 UTF-8 编码

<meta charset="utf-8" />

优先使用 IE 最新版本和 Chrome

<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />

自闭合标签无需闭合

例如: imginputbrhr

<img src="https://xxx.png" alt="Google" />
<br />
<input type="text" name="title" />

使用语义化标签

html 的标签能使用语义化的,尽量使用语义化标签,避免一个页面都是 div 或者 p 标签

<!-- bad -->
<div>
  <p></p>
</div>

<!-- good -->
<header></header>
<footer></footer>

属性顺序要求

HTML 属性应该按照特定的顺序出现以保证易读性。

id
class
name
data-xxx
src, for, type, href
title, alt
aria-xxx, role

CSS 篇

BEM 命名原则

  • block:模块,名字单词间用 - 连接
  • element:元素,模块的子元素,以 __ 与 block 连接
  • modifier:修饰,模块的变体,定义特殊模块,以 -- 与 block 连接
/* 举个例子 */
.block__element {
}
.block--modifier {
}

有效使用 css 选择器

选择器嵌套应少于 3 级

/* bad */
.page .header .login #username input {
}

/* good */
#username input {
}

有效使用 css 选择器,因遵循以下原则

  • 保持简单,不要使用嵌套过多过于复杂的选择器。
  • 通配符和属性选择器效率最低,需要匹配的元素最多,尽量避免使用。
  • 不要使用类选择器和 ID 选择器修饰元素标签。
  • 不要为了追求速度而放弃可读性与可维护性
  • 避免使用 CSS 表达式

慎重选择高消耗的样式

高消耗属性在绘制前需要浏览器进行大量计算:

box-shadows
border-radius
transparency
transforms
CSS filters(性能杀手)

避免重绘重排

当发生重排的时候,浏览器需要重新计算布局位置与大小,不利于性能优化。

常见引起重绘重排属性和方法

  • 添加或者删除可见的 DOM 元素;
  • 元素尺寸改变——边距、填充、边框、宽度和高度
  • 内容变化,比如用户在 input 框中输入文字
  • 浏览器窗口尺寸改变——resize 事件发生时
  • 计算 offsetWidthoffsetHeight 属性
  • 设置 style 属性的值

减少重绘重排的方法

  • 使用 transform 替代 top
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  • 不要把节点的属性值放在一个循环里当成循环里的变量。
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找,避免节点层级过多

Javascript 篇

关于命名

普通命名采用小驼峰式命名

let userName = 'jack'

命名是复数的时候需要加 s,比如说我想声明一个数组,表示很多人的名字

let names = new Array()

每个常量都需命名,这样更利于别人读懂含义

// good
const COL_NUM = 10
let row = Math.ceil(num / COL_NUM)

// bad
let row = Math.ceil(num / 10)

命名需要符合语义化,如果函数命名,可以采用加上动词前缀:

  • can 判断是否可执行某个动作
  • has 判断是否含有某个值
  • is 判断是否为某个值
  • get 获取某个值
  • set 设置某个值
//是否可阅读
function canRead(){
   return true;
}
//获取姓名
function getName{
   return this.name
}

关于字符串

统一使用单引号而不是双引号

// bad
const name = 'jack'

// good
const name = 'jack'

用字符串模板而不是 '+' 来拼接字符串

function sayHi(name) {
  return 'How are you, ' + name + '?'
}

// good
function sayHi(name) {
  return `How are you, ${name}?`
}

关于数组

用字面量赋值

// bad
const items = new Array()

// good
const items = []

用扩展运算符做数组浅拷贝

// bad
let arr = [1, 2, 3]
const len = arr.length
const copyArr = []

for (let i = 0; i < len; i += 1) {
  copyArr[i] = arr[i]
}

// good
const copyArr = [...arr]

用 Array.from 去将一个类数组对象转成一个数组。

const arrLike = { 0: 'foo', 1: 'bar', 2: 'baz', length: 3 }

// bad
const arr = Array.prototype.slice.call(arrLike)

// good
const arr = Array.from(arrLike)

使用数组解构

const arr = [1, 2, 3, 4]

// bad
const first = arr[0]
const second = arr[1]

// good
const [first, second] = arr

关于对象

创建对象和数组推荐使用字面量,因为这不仅是性能最优也有助于节省代码量。

// good
let obj = {
  name: 'Tom',
  age: 15,
  sex: '男',
}

// bad
let obj = {}
obj.name = 'Tom'
obj.age = 15
obj.sex = '男'

ES6 使用属性值缩写

const lukeSkywalker = 'Luke Skywalker'

// bad
const obj = {
  lukeSkywalker: lukeSkywalker,
}

// good
const obj = {
  lukeSkywalker,
}

将属性的缩写放在对象声明的开头

const anakinSkywalker = 'Anakin Skywalker'
const lukeSkywalker = 'Luke Skywalker'

// bad
const obj = {
  episodeOne: 1,
  twoJediWalkIntoACantina: 2,
  lukeSkywalker,
  episodeThree: 3,
  mayTheFourth: 4,
  anakinSkywalker,
}

// good
const obj = {
  lukeSkywalker,
  anakinSkywalker,
  episodeOne: 1,
  twoJediWalkIntoACantina: 2,
  episodeThree: 3,
  mayTheFourth: 4,
}

对象浅拷贝时,更推荐使用扩展运算符 ...,而不是 Object.assign。解构赋值获取对象指定的几个属性时,推荐用 rest 运算符,也是 ...。

// very bad
const original = { a: 1, b: 2 }
const copy = Object.assign(original, { c: 3 })
delete copy.a //  改变了 original

// bad
const original = { a: 1, b: 2 }
const copy = Object.assign({}, original, { c: 3 }) // copy => { a: 1, b: 2, c: 3 }

// good
const original = { a: 1, b: 2 }
const copy = { ...original, c: 3 } // copy => { a: 1, b: 2, c: 3 }

const { a, ...noA } = copy // noA => { b: 2, c: 3 }

关于函数

函数参数使用默认值替代使用条件语句进行赋值。

// good
function createMicrobrewery(name = 'Jack') {
   ...
}

// bad
function createMicrobrewery(name) {
  const userNameName = name || 'Jack'
   ...
}

函数参数使用结构语法,函数参数越少越好,如果参数超过两个,要使用 ES6 的解构语法,不用考虑参数的顺序。

// good
function createMenu({ title, body, buttonText, cancellable }) {
   ...
}

createMenu({
  title: 'Foo',
  body: 'Bar',
  buttonText: 'Baz',
  cancellable: true,
})

// bad
function createMenu(title, body, buttonText, cancellable) {
  // ...
}

优先使用 rest 语法...,而不是 arguments

// bad
function concatenateAll() {
  const args = Array.prototype.slice.call(arguments)
  return args.join('')
}

// good
function concatenateAll(...args) {
  return args.join('')
}

把默认参数赋值放在最后

// bad
function handleThings(opts = {}, name) {
  // ...
}

// good
function handleThings(name, opts = {}) {
  // ...
}

尽量使用箭头函数

// bad
[1, 2, 3].map(function (x) {
    const y = x + 1
    return x * y
  })

// good
 [1, 2, 3].map((x) => {
    const y = x + 1
    return x * y
  })

关于模块

在非标准模块系统上使用(import/export)

// bad
const AirbnbStyleGuide = require('./AirbnbStyleGuide')
module.exports = AirbnbStyleGuide.es6

// ok
import AirbnbStyleGuide from './AirbnbStyleGuide'
export default AirbnbStyleGuide.es6

// best
import { es6 } from './AirbnbStyleGuide'
export default es6

一个入口只 import 一次

// bad
import foo from 'foo'
// … some other imports … //
import { named1, named2 } from 'foo'

// good
import foo, { named1, named2 } from 'foo'

在只有一个导出的模块里,用 export default 更好

// bad
export function foo() {}

// good
export default function foo() {

for 循环

使用 for 循环过程中,数组的长度,使用一个变量来接收,这样有利于代码执行效率得到提高,而不是每走一次循环,都得重新计算数组长度

// bad
for(var i = 0; i < arr.length; i++){

}

// good
for(var i = 0; len = arr.length; i < len; i++){

}

React 篇

组件名为多个单词

我们开发过程中自定义的组件的名称需要为多个单词,这样做可以避免跟现有的以及未来的 HTML 元素相冲突。并且使组件标签更加遵循语义化。

多个属性进行分行

在 JavaScript 中,用多行分隔对象的多个属性是很常见的最佳实践,因为这样更易读。

<!-- good -->
<MyComponent 
    foo="a" 
    bar="b" 
    baz="c" 
/>

<!-- bad -->
<MyComponent foo="a" bar="b" baz="c" />

元素特性的顺序

原生属性放前面,指令其次,传参和方法放最后

  - class, id, ref
  - name, data-*, src, alt, for, type, href, value, max, min
  - title, placeholder, aria-*, role
  - required, disabled
  - foo="a" bar="b" baz="c"

关于组件内样式

为组件进行样式隔离,避免变量污染,扩大影响范围

/* bad  */
import './index.css'
<div class="xx" />

/* good  */
import style from './index.css'
<div class={style['xx']} />

关于组件结构

类组件结构:构造器 --> 钩子函数(并且每个函数加上用途注释) --> 自定义函数 --> 渲染函数

class OrderStatus extends Component {
    constructor() {}
    
    // 钩子函数 mount --> update --> unmout
    componentDidMount() {}
    
    // 自定义函数:认按钮点击回调/关闭弹窗回调
    function fn() {}
    
    render() {}
}

函数组件结构:状态 --> 自定义函数 --> 渲染结构

const Componet = (props) => {
    const { key1, key2 } = props
    const [state, setState] = useState({})
    
    // 方法用途注释
    const myFunction = () => {}
    
    // 内置函数
    useEffect(() => {
        // ...
    }, [])
    
    return ()
}

引用依赖:第三方库 --> 绝对路径 --> 相对路径 --> 样式

import React, { useState} from 'react'
import { Modal, Button } from 'antd'
import { Link } from 'react-router-dom';
import PermissionLink from '@components/PermissionLink';
import ApplyChangeMasterModal from '.'
import CancelOrderModal from '../CancelOrderModal'
import style from './index.css'

清除定时器或者事件监听

由于项目中有些页面难免会碰到需要定时器或者事件监听。但是在离开当前页面的时候,定时器如果不及时合理地清除,会造成业务逻辑混乱甚至应用卡死的情况,这个时就需要清除定时器事件监听,即在页面卸载(关闭)的生命周期函数里,清除定时器。

职责单一

任何时候尽量是的一个函数就做一件事情,而不是将各种逻辑全部耦合在一起,提高单个函数的复用性和可读性。比如:每个页面都会在加载完成时进行数据的请求并展示到页面。

// bad
componentDidMount() {
    if(ture or false) {
        // 逻辑代码
    }
}

// good
componentDidMount() {
  this.changeEvent(props.startText)
}

componentWillUnmount() {
  this.timer && clearInterval(this.timer)
  this.timer = null
}
  
// 逻辑处理函数
changeEvent () {}

图片篇:

使用恰当的图片格式。

  • jpg:适用于内容图片多为照片之类的。
  • png:适用于而饰图片,通常更适合用无损压缩。
  • gif: 基本上除了 gif 动画外不要使用。
  • webP:大大减小图片的体积,但是移动端有兼容性问题。

使用雪碧图

雪碧图,CSS Sprites,国内也叫 CSS 精灵,是一种 CSS 图像合成技术,主要用于小图片显示。

雪碧图的优点是把诸多小图片合成一张大图,利用backround-position属性值来确定图片呈现的位置,这样就能减少 http 请求,到达性能优化的效果。

使用 iconfont

iconfont(字体图标),即通过字体的方式展示图标,多用于渲染图标、简单图形、特殊字体等。

使用 iconfont 时,由于只需要引入对应的字体文件即可,这种方法可有效减少 HTTP 请求次数,而且一般字体体积较小,所以请求传输数据量较少。与直接引入图片不同,iconfont 可以像使用字体一样,设置大小、颜色及其他样式,且不存在失真的情况。

图片懒加载

图片懒加载的原理就是暂时不设置图片的 src 属性,而是将图片的 url 隐藏起来,比如先写在 state.src里面,等某些事件触发的时候(比如滚动到底部,点击加载图片)再将图片真实的 url 放进 src 属性里面,从而实现图片的延迟加载。

function lazyload() {
  var images = document.getElementsByTagName('img')
  var len = images.length
  var n = 0 // 存储图片加载到的位置,避免每次都从第一张图片开始遍历
  return function () {
    var seeHeight = document.documentElement.clientHeight
    for (var i = n; i < len; i++) {
      if (images[i].getBoundingClientRect().top < seeHeight) {
        // 方法二: 当图片的视口top出现在视口中
        if (images[i].getAttribute('src') === 'images/default.jpg') {
          images[i].src = images[i].getAttribute('state.src')
        }
        n = n + 1
      }
    }
  }
}

代码错误

是否存在内存泄漏代码

对于 SPA 应用,用户无需刷新浏览器,所以要想确保垃圾回收生效,我们需要在组件对应生命周期里做主动销毁。 
- 存在不必要的全局变量且未及时解除引用 全局变量,除非你关闭窗口或者刷新页面,才会被释放,如果缓存大量数据,很可能导致内存泄露。
一般来说,直接绑定在React实例上即可,组件销毁时该实例也会销毁;但没有绑定在React实例上的一定要主动销毁。
- 闭包内部变量未被销毁
- 计时器是否及时清理 
- 监听事件是否有解绑 

异步操作是否有异常处理

异步操作拿接口请求来说,大家都知道的是,使用 promise 时要有.catch处理。
但使用 async/await 时,有 .catch 处理的,也有try...catch处理的使用方法。
推荐使用 .catch。原因在于: 
- 可以控制接口请求出错后,是否要阻塞后续业务逻辑执行。 
- .catch 里的 error 能明确知道是接口请求导致的错误,而不需要再对 error 进行分类判断,是接口200返回后的业务逻辑处理报错还是接口报错。

取值时是否进行了空判断、调用函数时是否进行了类型判断

尤其是数据结构嵌套多层,取值时一定进行空值处理,避免页面崩溃

CR标准

  1. 编码风格。编码风格交给 eslint/tslint/stylelint

  2. 代码性能。大数据处理、重复渲染等

  3. 代码注释。字段注释、文档注释等

  4. 代码可读性。过多嵌套、低效冗余代码、功能独立、可读性变量方法命名等

  5. 代码可扩展性。功能方法设计是否合理、模块拆分等

  6. 控制 review 时间成本。reviewer 尽量由项目责任人组成,关注代码逻辑,无需逐字逐句理解。