一、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 介入时机
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" />
自闭合标签无需闭合
例如: img, input, br, hr 等
<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事件发生时 - 计算
offsetWidth和offsetHeight属性 - 设置
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标准
-
编码风格。编码风格交给 eslint/tslint/stylelint
-
代码性能。大数据处理、重复渲染等
-
代码注释。字段注释、文档注释等
-
代码可读性。过多嵌套、低效冗余代码、功能独立、可读性变量方法命名等
-
代码可扩展性。功能方法设计是否合理、模块拆分等
-
控制 review 时间成本。reviewer 尽量由项目责任人组成,关注代码逻辑,无需逐字逐句理解。