代码的优化与重构与每位开发者都密不可分,由于系统复杂度的提升、维护人数的增多、框架的升级等因素,代码的性能、质量往往会变得不可控。随着业务需求的不断变化和更新,代码也随着时间的推移变得越来越糟,可能会出现诸如重复的代码、过长的函数、冗赘类、过长的类等坏味道。
同时,代码的优化与重构话题也随着技术社区版本的更新,越来也成为了讨论的热点。比如如何高效解决从 Vue 2 升级到 Vue 3?是不断打补丁还是进行重构?重构过程繁杂,可以一次性开发个工具来解决重构问题吗?大型项目的重构相较于中小型项目又会遇到哪些特别的挑战?如何评估当前阶段是否适合进行重构升级呢?
为了寻找对以上挑战的最佳解决方案,我们特地采访了阿里妈妈资深前端工程师冯雨老师,他是阿里广告投放平台的前端 owner,负责过大型广告平台代码重构迁移工作。
GoGoCode (https://github.com/thx/gogocode)
冯雨老师也是开源代码转换工具 GoGoCode 团队的成员,他和他所在的阿里妈妈前端团队在迁移大型前端项目的过程中逐渐孵化出了这一工具,GoGoCode 上线后完成了十万行以上项目的前端框架升级,也为社区贡献了 Vue 2 升级到 Vue 3 的自动化升级方案。
同时,冯雨老师也在即将上线的 QCon+ 大厂案例「代码优化与重构」专题中带来了《写个程序来帮你自动化重构代码》的实践分享。因此我们针对代码优化与重构方向的技术对冯雨老师进行了采访,让我们一起来看看他的实践和思考吧。
冯雨:我目前在阿里妈妈前端技术部,日常的工作自然是开发和维护阿里广告投放相关的前端工程,有 PC 端也有小程序。坦白说,我做的是大部分前端开发者都在做的工作,所以遇到的也是大家普遍会遇到的问题。
这其中,我们有一个维护多年的大型项目想要整体升级前端框架,但是新的框架存在 breaking change,从模板到脚本都需要进行修改。如果人工来完成,工作量是很让人头大的,于是我们开始研究如何通过写个程序来进行重构,在此期间就做了 GoGoCode 这样一款代码转换工具。
冯雨:大家对于代码优化和代码重构可能都有着自己的定义,我先分享一下我的看法吧:代码优化就是让程序在功能不变的同时跑得更快;代码重构则是让程序在功能不变的同时理解起来更清晰、延展性更好。这两者其实有着共同的前提和不同的目的。
在企业项目中,对于代码性能优化,我认为可以在“不得不做”的时候去做。性能优化的代价有时候不仅仅是程序员的时间和发量,还可能是代码的可读性和未来的延展性,当性能不是瓶颈问题时做性能优化可能是弊大于利的。我甚至有几次对“代码优化”后理论性能更好的代码做重构,把它变成更简单的样子……卡农响起,这不是进入了循环么!
当你到了“不得不做”性能优化时,比如使用了特殊的数据结构或者并发地做了某些事情,我的经验是尽可能把这些属于计算机世界的操作丢到单独的地方,进行合理的封装和注释,这样我们的业务代码专注在描述业务的逻辑,而不是并发任务之间的调度。
对于代码重构,我建议在以下三种情况发生时就可以开动了:
当你把差不多的代码复制粘贴了 3~5 遍的时候这时候
一般意味着一些功能上的共性自然而然地显现了,不需要去提前假想,最佳的抽象时间就是它们站在你眼前的时候,我们把近似的功能抽取成函数和类,提高了可读性、减轻了书写负担和统一改动的麻烦。
当你对面前的一堆代码感到心烦意乱的时候这时候
可能需要一些分层设计了,就像上面对性能优化的建议一样,把属于计算机世界的代码和属于业务的代码分开,让自己脱离只见树木不见森林的困境;也有可能是代码里逻辑和数据的组织方式十分凌乱,这时只需要像整理房间一样打扫一遍即可。
当你的项目中使用的技术已经落后业界太多的时候
比如这几年我们能看到很多人把基于 jQuery 的前端项目重构成基于 MVVM 的项目,获得了巨大的效率提升;Angular 1 到 Angular 2、Vue 2 到 Vue 3 也同时带来了好用的新特性和 breaking change。这种重构的特点是做起来工作量巨大且有大量重复的 API 适配工作,所以我们可以考虑引入自动化重构工具来帮助我们实现。
值得强调的是,重构从来不是一劳永逸的,互联网公司的项目是迭代速度非常快而且有时候是没有章法的。产品经理经常会提出超出之前抽象层次的需求,这导致以前的封装和分层设计可能会成为新功能的制约。如果只是应急,我们通过 hack 的手段打一个不优雅的补丁是可以容忍的(通常的做法是在通用的函数里加一个标记参数和分支),但是当这种补丁累积到再一次让你心烦意乱时,就可以考虑定义抽象的范围了。
冯雨:如果说是在日常工作中进行代码重构,我的经验来看,最容易被忽视但其实非常难搞是“人”的问题。程序终归是简单的,但每个人都有自己的思维方式和标准。
大部分的企业项目都是多人维护同一个仓库,避免不了互相阅读和修改别人的代码,维护共有的抽象和封装。当我们决定重构时,多半是认为当前的代码不够好,或者糟透了;同时大概率这段代码还不是自己写的,那在撸起袖子改动前,一定要提醒自己,这段代码不好可能只是自己的结论,最好提前和协作的同事们问个明白,也许有盘根错节的掣肘之处。
而且如果改动形成了新的抽象,也会影响到其他开发者之后的开发,无论是通过文档还是 code review。所以一定要想办法告诉对方你的思路,如果我们的新设计在对方着急赶工时才被踩坑发现,相信我,无论它多么巧夺天工,也一定会被对方看做是一堆垃圾。
冯雨:正如我母亲在收看春晚节目时说的:“我发现长的节目很难不臭,臭的节目很难不长,又臭又长是一种必然现象。”大型项目的“大”,让重构难度从量变升级到了质变。
首先,“大”会让你丧失重构的决心。比如就在去年,维护时的痛苦终于促使我们下定决心对团队维护的一个近 10 年、代码量超过 10w 行的老项目的底层框架进行了升级。
接下来面临的就是第二个问题,因为动了底层框架,所以这 10w 行大几百个文件我们要逐一改过,与此同时还不断有新的业务需要支持,因此这个改动不能一蹴而就。所以我们为新框架设立了一个全新的仓库,通过微前端的方案解决了老仓库老框架下引入新仓库新框架的页面问题,这样就可以渐进式地迁移了。
再到后面发现文件实在是太多了,为了加快效率,我们又决定试一试自动化的代码重构,因为框架升级主要是涉及 API 的变化,基于 AST 抽象语法树来写一个程序帮我们重构代码是可行的,这最终促成了我们的开源项目 GoGoCode 的诞生。
还需要注意的是,一个大项目,大概率是比较重要的项目,所以对稳定性的要求会非常高。如果条件允许,在重构前完善单元测试会让这个过程信心倍增。我们还做了很长时间的灰度测试,慢慢把重构后的代码推给用户使用。如果你的公司 / 部门有专业的测试团队,可以考虑搭上一些项目的顺风车,优先重构有项目的模块,这样就能得到比较完善的回归测试了。
冯雨:在最开始有这样自动化重构的需求时,我们原本打算直接使用社区已经较为成熟的 Babel 或者 jscodeshift 去做,这些项目基于 AST 带来了操作的精确性。但是在使用过程中,想要写一个很小的功能都需要走一遍繁杂的 AST 查找、构造和修改的过程,而且之后的可读性也比较差。
此时我们联想到平时批量修改代码最常用的其实是全局的“查找 / 替换”,还有用 CSS 选择器查询 DOM 树的 jQuery 语法,如果把二者结合,能不能用更加直观的字符串来构造一个“代码选择器”去查询 AST 里的节点,通过通配符的形式去捕获我们需要的 AST 结构。这样岂不是就能像“查找 / 替换”一样去编写代码修改程序了?
我们根据这样的设想实现了代码转换工具 GoGoCode,写转换程序的效率就大幅度提升了。比如,假设你想在下面代码中挑选出名为 log 的函数:
function log(a) {
console.log(a);
}
function alert(a) {
alert(a);
}
只需要按照如下方式使用 find 方法即可:
const ast = $(source);
const test1 = ast.find('function log() {}');
如果使用通配符,我们能匹配到所有名字的函数定义,我们提供 each 方法来遍历这个结果集合,下面的例子把 match 到的函数名收集到了名为 names 的数组里:
const fns = ast.find(`function $_$0() {}`);
const names = [];
fns.each((fnNode) => {
const fnName = fnNode.match[0][0].value;
names.push(fnName);
});
如果我们想把 log 函数改名成 record,用 replace 做会非常简单:
ast.replace('function log($$$0) { $$$1 }', 'function record($$$0) { $$$1 }');
这看起来就像字符串的替换,但底层实现上是基于 AST 的替换,所以无视了代码的结构,也可以随时对 AST 节点进行高级操作。
经过几个大规模项目的实验,GoGoCode 已经足够健壮,于是我们就把它给开源出去了,也获得了不少 star 和社区反馈。这之后我们又做了 Vue 2 到 Vue 3 的转换插件,也帮助到很多社区小伙伴进行了框架升级。
要说缺点,我们的生态和稳定程度显然是不能和 Babel 这样的底层设施相比的,我觉得 Babel 是更适合大家去被动使用的,而 GoGoCode 可能会让你有兴趣亲自写一个代码转换程序。
冯雨:理想中的大厂前端应该总是站在前端之巅挥舞着最新的技术,但其实大部分的白天黑夜,我们只是伏在一座座代码山下卖力挖掘,这些项目看起来可能不是那样精致,却不停地奔跑在数以万计的计算机上去帮助海量用户。所以除了新鲜的技术,我也会格外重视这些每天看守的项目,想办法让它们变得更好。就像我们今天一直在聊的重构经历,在让项目变好的同时,我们也在让自己的水平变得更好。
在这个过程中,如果你能形成一种经验,千万不要吝惜,把它先通过一些线下交流分享给你的同事们,或是记录成文章分享到互联网上;更可以“码以载道”,通过一个开源项目帮助更多人,这样你也会收获别人对你的认可。
欢迎关注开源项目:GoGoCode
https://github.com/thx/gogocode
如果你对阿里妈妈前端技术部感兴趣,欢迎加入我们,联系方式:chibing.fy@alibaba-inc.com
冯雨
阿里巴巴阿里妈妈
营销研究和体验中心资深前端工程师
阿里广告投放平台前端 owner,开源项目 GoGoCode 团队成员,负责过大型广告平台代码重构迁移工作。
InfoQ 技术大会元宵节限时活动正在进行中。2 月 11-18 日,双人购票即可享受最高立减 5680 元优惠。QCon、ArchSummit、GMTC、PCon、AICon 等 5 大会议品牌、11 场大会,涵盖架构、前端、产品、AI 等方向。不论你是架构师、产品、技术、研发、前端,总有适合你的,扫码下图二维码咨询活动详情吧!