很多时候,我们看见一些不太优雅的代码、不太整洁的代码,也很容易可以推断出这段代码是怎么来的,甚至是可以推断出写这个代码的人当时的心理状态和那时候的背景。在前端迅速发展的时代,一份一年多两年以上的代码,很可能带着历史的色彩地成为了历史包袱
考古?
如果某一天,你突然看见类似下面这些的代码:
多个if-return
代码语言:javascript复制function f() {
if (a) {
return
}
if (b) {
return
}
if (c) {
return
}
// ...many codes
}
复制代码
推测当事人心理:需求要做这个功能,需要加一个条件。好的,就在最前面加一下
函数有很多参数
代码语言:javascript复制function f(a, b, c, d, config, isAdmin, isEdit, isAdd) {
// ...many codes
}
复制代码
推测当事人心理:后面这个函数功能更复杂了,需要加多一个参数做配置。啊,还要再加一些功能,再多一个参数
很长的类似的判断
代码语言:javascript复制if (a === 1 || a === 2 || a === 3 || a === 4) {
}
推测当事人心理:状态4也要走这个逻辑,那我加多一个&&即可
组件返回值有冗余的短路表达式
代码语言:javascript复制return (
<>
{
isXXX && <div>abc</div>
}
{/* 可能中间有很多其他代码 */}
{
!isXXX && <div>123</div>
}
</>
)
复制代码
推测当事人心理:兜底情况展示123,直接加一下,ok
明显走不到的逻辑
代码语言:javascript复制const data = res[0].some_data || {}
if (!data) {
return
}
// other code
// =================================
const SOME_FLAG1 = 'xxxx1'
const SOME_FLAG2 = 'xxxx2'
const SOME_FLAG3 = 'xxxx3'
const arr = [SOME_FLAG1]
if (xxx) {
// arr
}
// 对arr一顿操作
if (!arr) {
// 怕出错?加了这个
}
推测当事人心理1:可能arr为假值,防止报错
推测当事人心理2:只是改了前面,后面!arr压根没看见
常见的代码片段莫名其妙的出现
代码语言:javascript复制import { func } from 'prop-types';
function a(){
return 1
}
复制代码
推测当事人状况:压根不知道,只是输入了func按了回车,以为是function的补全
想要一些常见的工具函数却代码提示有多个
结果发现,package.json里面有lodash,然后自己又写了一个
又比如想要一个request,发现有3个以上的提示。然后一看,团队统一的request一个,某个人写了一个缓存版本又一个,另一个人又自己写了一个袖珍版,最后还有一个人写了一个预处理参数的...
推测当事人状况1:压根不知道有团队统一的request,自己写还写得很挫
推测当事人状况2:知道有request,但不敢改,所以自己封装多一层,但名字还是取了一样的
翻新?
开启eslint/tslint
本人多次代码优化重构的经验,一个没有lint的项目,开了lint后90%的错误都可以通过autofix解决。例如9000个错误,跑一下即可变成800多个,可以修复那些换行、缩进、函数单参数无括号的问题。这些修复不需要测试介入。剩下的那些错误需要人工解决
最常见的需要人工解决的lint错误合集:
错误 | 解决方法 | 紧急程度 | 风险 |
---|---|---|---|
下划线命名 | 全局搜索,一个个人工修 | 中 | 低 |
解构赋值 | 一般是warning,遇到一个修一个 | 低 | 低 |
无状态class组件 | 改成PureComponent或者函数组件 | 高 | 中 |
willreciveprops | 改成getderivedstatefromprops | 高 | 高 |
componentwillmount/update | 初始state & didmount | 高 | 低 |
== | 确定类型再转化,最后=== | 高 | 高 |
作用域下重复命名 | 看见就修,但还是有必要性 | 中 | 低 |
ts类型报错 | 不影响代码的执行,但也不能长期不管 | 低 | 低 |
html标签缺少属性 | 如img的alt、button的type,看见就修 | 低 | 低 |
promise的rejcet不是error | reject(Error(xxx)) | 高 | 低 |
中等风险以上的修复,需要自测或者测试,过一遍主流程。无论哪一种人工修复,量达到几百个,都需要测试介入
精简if
比如上面的多个if-return、很长的类似的判断,都可以精简为||
、&&
,进一步精简就是数组操作: [1, 2, 3, 4].includes(a)
,在另一篇文章里面有讲到更多的if简化
完整的看一下整个文件上下文
上面提到的一些情况,可能是最开始的时候设计是没什么问题的,但随着需求迭代,就不一样了。比如多个if-return、明显走不到的逻辑、重复写了一些常见的工具函数,这些问题都是因为不完整地看上下文导致的
旧版react的升级迁移
- 旧版升级新版的情况,三个danger的生命周期必须处理掉,其中getderivedstatefromprops风险最大,需要做好各种判断
- 简单的组件尝试使用hook重写(不需要特地去,只是需求涉及到的地方顺便改)
- 用了ref、context的class组件重构为函数组件的时候,记得做好足够的措施,如forwardRef、useContext
- 组件必须带名字,不要
export default class extend
、export default function() {}
,否则调试工具不显示名字 - 如果不是ts项目,要使用props-types(或者在旁边写一个d.ts文件)
- render函数返回内容、函数组件返回内容不宜太长,一定要做好组件拆分。复杂的js运算,建议抽成组件或者renderxxx函数
重构步骤
本人有多次历史大项目重构经历,常见的case和套路已经在上文提过,接下来是操作步骤的总结
不管三七二十一,先lint【动手前】
传统旧项目,通常没有lint,需要自己装。然后找一下团队现在的lint规范(如果没有就找业界出名的如airbnb),接着跑一波lint自动修复,此时可以把缩进换行全部解决,剩下的需要自己手动去修。具体怎么手动修,前面已经提到了
顺便修一下改动文件【动手时】
为什么重构?那必然有一个触发点,或是某个需求,或是发现了很多bug导致无法正常运行。或是开始有大力维护的计划。所以先从触发点开始,在保证功能正常的情况下,顺便把模块一起重构了,这里也有几个要领:
- 新逻辑必须有注释,旧逻辑如果没注释,但又关联这次修改的,自己梳理或者找同事来了解一下背景,补全注释和文档
- 无状态class组件,可以直接改成purecomponent。如果是想拥抱hook,那就必须改函数组件。但是有一个前提:全局搜索一下,确认没有其他地方使用了extend继承当前class组件
- 处理所有的magic number。当你看见
if status === 1
、type === 0
这种代码,这个让人懵逼的数字就是magic number。此时要看上下文,了解这个数字是什么意思,再使用大写常量来维护(如const NORMAL = 1;
,并加上注释:// 【xx模块】状态正常:1
) 。如果多个文件用到,需要提取到更上层 - 奇葩命名也是可以在这个环节修复一下。文件内搜索,非对象key、非解构,都可以直接换掉了
- 不确定、不敢改、有疑问的地方,使用TODO注释标记,方便后续回头解决。如
if a == b
,从代码中无法知道a、b是什么类型,且业务路径很长不好复现,先妥协一下,等有时间再改 - 对于“看不懂”、“不敢改”的函数,你就把它当作一个沙盒就行,能不动的先不要动。时刻记住,保证这段代码上游输入不变即可
- 对于重复的代码,需要提取出来一个函数,然后引进去调用。不必过度封装,大概就一层很薄的封装,能解决掉明显的过多的重复代码即可
- 如果发现not defined的lint报错,但页面没问题,那么只有两种情况:那个文件没用了、那个文件还没执行到。如果是没用的,立刻删掉;如果是没执行到但不敢动的,可以屏蔽掉这行eslint报错并加上TODO,或者直接定义它,让他执行到的时候也能正常且没什么副作用(最好还是要找到相关同事去问一下)
修改范围怎么确定?
有一个这样的文件目录:
代码语言:javascript复制- components
utils.ts
constant.ts
-- Home
index.tsx
Header.tsx
Footer.tsx
...
-- Input
index.tsx
index.scss
...
- 如果改的是
Header.tsx
,Home目录不大,需要全部一起重构;如果Home目录很大,那就只改Header.tsx
和他的父组件index.tsx
- 如果Home和Input里面多了任何常量,导致magic number产生,那么必须去constant里面新增定义,并写好注释,再引入到组件里面用
- 如果Home很大,且多个文件import了一个函数,这个函数已经确定没什么用了或者要抽离,此时需要在Home下全局搜索这个语句,直接删掉或者抽离
补齐基础设施【动手后】
功能已经做好了,也顺便重构了一波,此时还没完。考虑到未来继续维护和重构,所以现在要开始铺路,方便下次,让自己和其他同事更舒服
- 自己封装的新的关键的函数,需要留下自己的大名、日期,并写好注释,甚至可以把参考的网上的资料链接都贴出来
- 定时查看之前留下的TODO。不要标了TODO就不管了,定时看看,有时间就去跟进解决掉
- 将lint-staged加入到git钩子上,不合格不给提交
- 公共utils、common、assets文件夹都是公共资源,这些需要根据业务共同点抽取出来,包括工具函数、公共组件、全局配置、定义文件
- readme补全文档,包括页面逻辑、文件目录组织、开发规范,这些都是你这个时候你来定的了
- js转ts项目,部分迁移,也是改到哪里,哪里就上ts。暂时无法上ts的大组件,先给一个d.ts文件做类型声明也行
- 全透传参数,无法追溯的,只能靠跑业务的时候(如果是服务端的,可以手动curl一下某个接口)console一下获得字段,并抄一份作为注释、文档。node中间层经常会有,params直接透传前端给的转给另一个服务,response也是从另一个服务透传给回前端,或者是
{ ...data }
,经历过的人自然懂这个痛
总结
虽然平时大家总是自黑,讲段子,发自黑、调侃的表情包,吐槽垃圾代码、历史代码、知道没用但不敢动.jpg。但我相信如果真的搁在头上了,大家都不会怂的,也不会轻易妥协,会认真的修好,完美完成重构