现在前端组长都是这样做 Code Review

科技   2024-09-26 23:51   福建  

前言

Code Review 是什么?

Code Review 通常也简称 CR,中文意思就是 代码审查

一般来说 CR只关心代码规范和代码逻辑,不关心业务

但是,如果CR的人是组长,建议有时间还是看下与自己组内相关业务,能避免一些生产事故的发生

作为前端组长做 Code Review 有必要吗?

主要还是看公司业务情况吧,如果前端组长需求不多的情况,是可以做下CR,能避免一些生产事故

  • 锻炼自己的 CR 能力
  • 看看别人的代码哪方面写的更好,学习总结
  • 和同事交流,加深联系
  • 你做了 CR,晋升和面试,不就有东西吹了不是

那要怎么去做Code Review呢?

可以从几个方面入手

  • 项目架构规范
  • 代码编写规范
  • 代码逻辑、代码优化
  • 业务需求

具体要怎么做呢?

传统的做法是PR时查看,对于不合理的地方,打回并在PR中备注原因或优化方案

每隔一段时间,和组员开一个简短的CR分享会,把一些平时CR过程中遇到的问题做下总结

当然,不要直接指出是谁写出的代码有问题,毕竟这不是目的,分享会的目的是交流学习

人工CR需要很大的时间精力,与心智负担

随着 AI 的发展,我们可以借助一些 AI 来帮我们完成CR

接下来,我们来看下,vscode中是怎么借助 AI 工具来 CR

安装插件 CodeGeex

新建一个项目

mkdir code-reviewcd code-review

创建 test.js 并用 vscode 打开

cd .>test.jscode ./

编写下 test.js

function checkStatus() {  if (isLogin()) {    if (isVip()) {      if (isDoubleCheck()) {        done();      } else {        throw new Error("不要重复点击");      }    } else {      throw new Error("不是会员");    }  } else {    throw new Error("未登录");  }}

这是连续嵌套的判断逻辑,要怎么优化呢?

侧边栏选择这个 AI 插件,选择我们需要CR的代码

输入 codeRiview,回车


我们来看下 AI 给出的建议


AI 给出的建议还是很不错的,我们可以通过更多的提示词,优化它给出的修改建议,这里就不过多赘述了

通常我们优化这种类型的代码,基本优化思路也是,前置校验逻辑,正常逻辑后置

除了CodeGeex外,还有一些比较专业的 codeRiview 的 AI 工具

比如:CodeRabbit

那既然都有 AI 工具了,我们还需要自己去CR 吗?

还是有必要的,借助 AI 工具我们可以减少一些阅读大量代码环节,提高效率,减少 CR 的时间

但是仍然需要我们根据 AI 工具的建议进行改进,并且总结,有利于拓宽我们见识,从而写出更优质的代码

具体 CR 实践

判断逻辑优化

1. 深层对象判空

// 深层对象if (	store.getters &&  store.getters.userInfo &&  store.getters.userInfo.menus) {}
// 可以使用 可选链进行优化if (store?.getters?.userInfo?.menus) {}

2. 空函数判断

优化之前

props.onChange && props.onChange(e)

支持 ES11 可选链写法,可这样优化,js 中需要这样,ts 因为有属性校验,可以不需要判断,当然也特殊情况

props?.onChange?.(e)

老项目,不支持 ES11 可以这样写

const NOOP = () => 8const { onChange = NOOP } = propsonChange(e)

3. 复杂判断逻辑抽离成单独函数

// 复杂判断逻辑function checkGameStatus() {  if (remaining === 0 ||    (remaining === 1 && remainingPlayers === 1) ||    remainingPlayers === 0) {      quitGame()  }}
// 复杂判断逻辑抽离成单独函数,更方便阅读function isGameOver() {  return (    remaining === 0 ||    (remaining === 1 && remainingPlayers === 1) ||    remainingPlayers === 0  );}
function checkGameStatus() { if (isGameOver()) { quitGame(); }}

4. 判断处理逻辑正确的梳理方式

// 判断逻辑不要嵌套太深function checkStatus() {  if (isLogin()) {    if (isVip()) {      if (isDoubleCheck()) {        done();      } else {        throw new Error('不要重复点击');      }    } else {      throw new Error('不是会员');    }  } else {    throw new Error('未登录');  }}

这个是不是很熟悉呀~

没错,这就是使用 AI 工具 CR的代码片段

通常这种,为了处理特殊状况,所实现的判断逻辑,都可以采用 “异常逻辑前置,正常逻辑后置” 的方式进行梳理优化

// 将判断逻辑的异常逻辑提前,将正常逻辑后置function checkStatus() {  if (!isLogin()) {    throw new Error('未登录');  }
if (!isVip()) { throw new Error('不是会员'); }
if (!isDoubleCheck()) { throw new Error('不要重复点击'); }
done();}

函数传参优化

// 形参有非常多个const getMyInfo = (  name,  age,  gender,  address,  phone,  email,) => {  // ...}

有时,形参有非常多个,这会造成什么问题呢?

  • 传实参是的时候,不仅需要知道传入参数的个数,还得知道传入顺序
  • 有些参数非必传,还要注意添加默认值,且编写的时候只能从形参的后面添加,很不方便
  • 所以啊,那么多的形参,会有很大的心智负担

怎么优化呢?

// 行参封装成对象,对象函数内部解构const getMyInfo = (options) => {  const { name, age, gender, address, phone, email } = options;  // ...}
getMyInfo( { name: '张三', age: 18, gender: '男', address: '北京', phone: '123456789', email: '123456789@qq.com' })

你看这样是不是就清爽了很多了

命名注释优化

1. 避免魔法数字

// 魔法数字if (state === 1 || state === 2) {  // ...} else if (state === 3) {  // ...}

咋一看,这 1、2、3 又是什么意思啊?这是判断啥的?

语义就很不明确,当然,你也可以在旁边写注释

更优雅的做法是,将魔法数字改用常量

这样,其他人一看到常量名大概就知道,判断的是啥了

// 魔法数字改用常量const UNPUBLISHED = 1;const PUBLISHED = 2;const DELETED = 3;
if (state === UNPUBLISHED || state === PUBLISHED) { // ...} else if (state === DELETED) { // ...}

2. 注释别写只表面意思

注释的作用:提供代码没有提供的额外信息

// 无效注释let id = 1 // id 赋值为 1
// 有效注释,写业务逻辑 what & whylet id = 1 // 赋值文章 id 为 1

3. 合理利用命名空间缩短属性前缀

// 过长命名前缀class User {  userName;  userAge;  userPwd;
userLogin() { }; userRegister() { };}

如果我们把前面的类里面,变量名、函数名前面的 user 去掉

似乎,也一样能理解变量和函数名称所代表的意思

代码却,清爽了不少

// 利用命名空间缩短属性前缀class User {  name;  age;  pwd;
login() {}; register() {};}

分支逻辑优化

什么是分支逻辑呢?

使用 if else、switch case ...,这些都是分支逻辑

// switch caseconst statusMap = (status: string) => {    switch(status) {        case 'success':            return 'SuccessFully'        case 'fail':            return 'failed'        case 'danger'            return 'dangerous'        case 'info'            return 'information'        case 'text'            return 'texts'        default:            return status    }}
// if elseconst statusMap = (status: string) => {    if(status === 'success') return 'SuccessFully'    else if (status === 'fail') return 'failed'		else if (status === 'danger') return 'dangerous'    else if (status === 'info') return 'information'    else if (status === 'text') return 'texts'    else return status}

这些处理逻辑,我们可以采用 映射代替分支逻辑

// 使用映射进行优化const STATUS_MAP = {    'success': 'Successfull',    'fail': 'failed',    'warn': 'warning',    'danger': 'dangerous',    'info': 'information',    'text': 'texts'}
return STATUS_MAP[status] ?? status

【扩展】

??TypeScript 中的 “空值合并操作符”

当前面的值为 null 或者 undefined 时,取后面的值

对象赋值优化

// 多个对像属性赋值const setStyle = () => {    content.body.head_style.style.color = 'red'    content.body.head_style.style.background = 'yellow'  	content.body.head_style.style.width = '100px'  	content.body.head_style.style.height = '300px'  	// ...}

这样一个个赋值太麻烦了,全部放一起赋值不就行了

可能,有些同学就这样写

const setStyle = () => {  	content.body.head_style.style = {      color: 'red',      background: 'yellow',      width: '100px',      height: '300px'    }}

咋一看,好像没问题了呀?那 style 要是有其他属性呢,其他属性不就直接没了吗~

const setStyle = () => {  	content.body.head_style.style = {      ...content.body.head_style.style      color: 'red',      background: 'yellow',      width: '100px',      height: '300px'    }}

采用展开运算符,将原属性插入,然后从后面覆盖新属性,这样原属性就不会丢了

隐式耦合优化

// 隐式耦合function responseInterceptor(response) {  const token = response.headers.get("authorization");  if (token) {    localStorage.setItem('token', token);  }}
function requestInterceptor(response) { const token = localStorage.getItem('token'); if (token) { response.headers.set("authorization", token); }}

这个上面两个函数有耦合的地方,但是不太明显

比如这样的情况,有一天,我不想在 responseInterceptor 函数中保存 tokenlocalStorage

function responseInterceptor(response) {  const token = response.headers.get("authorization");}
function requestInterceptor(response) { const token = localStorage.getItem('token'); if (token) { response.headers.set("authorization", token); }}

会发生什么?

localStorage.getItem('token')一直拿不到数据,requestInterceptor 这个函数就报废了,没用了

函数 responseInterceptor改动,影响到函数 requestInterceptor 了,隐式耦合了

怎么优化呢?

// 将隐式耦合的常数抽离成常量const TOKEN_KEY = "authorization";const TOKEN = 'token';
function responseInterceptor(response) { const token = response.headers.get(TOKEN_KEY); if (token) { localStorage.setItem(TOKEN_KEY, token); }}
function requestInterceptor(response) { const token = localStorage.getItem(TOKEN_KEY); if (token) { response.headers.set(TOKEN_KEY, token); }}

这样做有什么好处呢?比刚才好在哪里?

还是刚才的例子,我去掉了保存 localStorage.setItem(TOKEN_KEY, token)

我可以根据TOKEN_KEY这个常量来查找还有哪些地方用到了这个 TOKEN_KEY,从而进行修改,就不会出现冗余,或错误

不对啊,那我不用常量,用token也可以查找啊,但你想想 token 这个词是不是得全局查找,其他地方也会出现token

查找起来比较费时间,有时可能还会改错了

用常量的话,全局查找出现重复的概率很小

而且如果你是用 ts 的话,window 下鼠标停在常量上,按 ALT 键就能看到使用到这个常量的地方了,非常方便

小结

codeRiview(代码审查)不仅对个人技能的成长有帮助,也对我们在升职加薪、面试有所裨益

CR 除了传统的方式外,也可以借助 AI 工具,来简化其中流程,提高效率

上述的优化案例,虽然优化方式不同,但是核心思想都是一样,都是为了代码 更简洁、更容易理解、更容易维护

当然了,优化方式还有很多,如果后期遇到了也会继续补充进来

前端工匠
我是浪里行舟,Github博客6000+star作者,掘金、CSDN社区活跃作者,致力于打造一系列能够帮助前端工程师提高的优质文章。
 最新文章