Meta Description: 本文介绍如何使用 Codemod 和 jscodeshift 加速 JavaScript 重构,自动化清理遗留代码、统一代码风格,并降低大规模代码迁移中的沟通、审查和合并冲突成本。

在花园里播种、培育并收获作物,固然是一件很有成就感的事。但如果不定期除草,花园迟早会变得杂乱不堪。单独看,每一株杂草似乎都无伤大雅;可一旦它们成片蔓延,就会妨碍作物生长。相比之下,在一座没有杂草的花园里劳作,既高效,也令人愉悦。
代码库也是如此。
对于 JavaScript 项目来说,随着代码规模不断扩大,遗留代码、过时语法和不一致的代码风格会逐渐拖慢团队效率。Codemod 正是解决这类问题的有效工具:它可以通过自动化代码转换,帮助团队更快完成大规模重构和代码清理。
坦白说,我并不喜欢清理代码,所以也常常会忽略这件事,直到问题积累到不得不处理。好在编程领域有 ESLint、SCSS-Lint 这类优秀工具,可以帮助我们在编写代码的过程中持续保持整洁。然而,当我们面对大量遗留代码时,一想到要手动调整成千上万个空格、尾随逗号或语法细节,就很容易感到力不从心。
过去几年里,海外某大型互联网公司已经向版本控制系统提交了数百万行 JavaScript 代码。与此同时,前端 Web 开发也发生了巨大变化:语言特性、框架,甚至 JavaScript 本身都在持续演进。即便从一开始就遵循良好的代码风格指南,可以在一定程度上减少这类问题,但随着时间推移,代码库仍然很容易偏离当前的最佳实践。
每一个细小的不一致,都像是一株等待拔除的杂草。只有清理掉它们,才能为真正有价值的东西腾出空间,也才能让团队更高效地协作。
下面是我们“除草”之前的代码花园:
代码修改之前
我一直关注如何提升团队效率,也深知统一的代码风格和代码检查工具,能够优化反馈循环,降低沟通成本。最近,我们启动了一个 JavaScript 代码清理项目,目标是让大量旧代码重新符合团队的风格指南,并在更多地方启用代码检查。
如果手动完成所有工作,不仅繁琐,而且耗时。因此,我们开始寻找能够自动化部分工作的工具。eslint --fix 是一个很好的起点,但它当前能够自动修复的问题仍然有限。相关开源社区已经开始接受为更多规则添加自动修复能力的 pull request,也在推进 JavaScript 具体语法树,也就是 Concrete Syntax Tree,简称 CST 的相关工作。不过,这些能力距离完全落地仍然需要时间。
幸运的是,我们找到了由海外某科技公司开源的 jscodeshift。它是一个代码转换工具包。所谓代码转换工具,指的是用于辅助大规模、半自动化代码库重构的工具。如果说代码库是一座花园,那么 jscodeshift 就像是一位机器人园丁。
jscodeshift 可以将 JavaScript 解析成抽象语法树,也就是 Abstract Syntax Tree,简称 AST;随后应用指定的转换逻辑,再生成修改后的 JavaScript 代码,并尽可能保留项目原有的编码风格。转换脚本本身也是用 JavaScript 编写的,因此团队成员可以很快上手。
通过查找或编写所需的转换脚本,我们能够显著加快那些繁琐的 JavaScript 重构工作,让团队把精力集中在更有意义的事情上。
运行了一些 codemod 之后,我们的花园看起来清爽多了:
代码修改之后
Codemod 重构策略
大多数代码转换任务即使涉及数千个文件,也能在不到一分钟的时间里完成。因此我发现,codemod 很适合作为一种“支线任务”:当我在等待主线工作推进,比如等待代码审查时,就可以顺手处理一些代码转换。这有助于我在推进更大、更重要项目的同时,最大限度地提升工作效率。
在进行大规模 JavaScript 重构时,我遇到的主要挑战通常可以概括为四个 C:沟通,也就是 Communication;正确性,也就是 Correctness;代码审查,也就是 Code Review;以及合并冲突,也就是 Conflicts。下面是一些对我很有帮助的策略。
对于这类跨多人、跨模块的大规模重构,除了代码转换工具本身,团队还需要把目标、任务拆分、排期、代码审查、测试发布和经验沉淀串联起来;在实际研发管理中,可以借助 PingCode 这类智能化研发管理工具管理从需求清理到发布上线的完整流程,也可以用 Worktile 这类通用项目协作系统承载任务、文档、日历和进度协同,让技术改造不只是“改代码”,而是成为可跟踪、可复盘的团队工程。
并不是所有 codemod 在所有场景下都能生成完全符合预期的结果。因此,运行转换之后,检查并调整变更非常重要。我通常会使用以下命令:
git diff
git add --patch
git checkout --patch
小规模提交和小规模 pull request 通常是最佳选择,codemod 也不例外。我一般一次只处理一种代码转换,这样更便于代码审查,也更容易解决合并冲突。
通常情况下,我会单独提交 codemod 自动生成的结果;如有必要,再额外提交一次手动清理。这样在对分支进行 rebase 时,解决冲突会容易得多,因为我通常可以先执行:
git checkout --ours path/to/conflict
然后重新对该文件运行 codemod,而不会影响我手动调整过的内容。
有时,codemod 会产生非常大的 diff。遇到这种情况,我发现根据路径或文件名,把代码库的不同部分拆成多个独立提交或 pull request 会更合适。例如,一个提交专门修复 *.js 文件,另一个提交专门修复 *.jsx 文件。这样不仅代码审查更轻松,也能简化合并冲突的处理。
得益于 Unix 哲学,对代码库的不同部分运行 codemod 也很简单,只需调整 find 的调用方式即可:
find app/assets/javascripts -name "*.jsx" -not -path "*/vendor/*" | \
xargs jscodeshift -t ~/path/to/transform.js
为了尽量减少对他人的影响,我通常会在周五较早的时候提交 codemod 版本供大家审查,然后在周一较早的时候 rebase 并合并。那时大多数人还没有开始新的工作。这样既能让大家在周末前完成手头任务,也能降低我的代码转换对他们的影响。
常用 JavaScript Codemod 示例
虽然这类工具还比较新,但已经有不少实用的 codemod 可供使用。下面是我们目前成功用过的一些转换脚本。
轻量级 Codemod
这些 codemod 非常实用,而且应用起来相对容易,能够帮助我们快速取得阶段性成果。
js-codemod/arrow-function
保守地将普通函数转换为箭头函数。
转换前:
[1, 2, 3].map(function(x) {
return x * x;
}.bind(this));
转换后:
[1, 2, 3].map(x => x * x);
js-codemod/no-vars
保守地将 var 转换为 const 或 let。
转换前:
var belongs = 'anywhere';
转换后:
const belongs = 'anywhere';
js-codemod/object-shorthand
将对象字面量转换为 ES6 的属性简写和方法简写形式。
转换前:
const things = {
belongs: belongs,
anywhere: function() {},
};
转换后:
const things = {
belongs,
anywhere() {},
};
js-codemod/unchain-variables
拆分链式变量声明。
转换前:
const belongs = 'anywhere', welcome = 'home';
转换后:
const belongs = 'anywhere';
const welcome = 'home';
js-codemod/unquote-properties
移除对象属性名中不必要的引号。
转换前:
const things = {
'belongs': 'anywhere',
};
转换后:
const things = {
belongs: 'anywhere',
};
重量级 Codemod
下面这些 codemod 要么会产生更大的 diff,要么更容易带来合并冲突,要么需要更多后续调整,才能确保生成的代码仍然清晰、易读。
react-codemod/class
将 React.createClass 调用转换为 ES6 class。
这个 codemod 会在存在 mixin 时避免执行转换,同时也能很好地处理其他必要变更,例如如何定义 propTypes、默认 props、初始 state,以及如何在构造函数中绑定回调。
转换前:
const BelongAnywhere = React.createClass({
// ...
});
转换后:
class BelongAnywhere extends React.Component {
// ...
}
react-codemod/sort-comp
重新排列 React 组件方法,使其符合 ESLint 的 react/sort-comp 规则。
由于这个转换会移动大量代码块,Git 往往无法自动解决大多数合并冲突。我发现最好的做法,是在执行转换前充分沟通,并选择最不容易产生冲突的时间窗口,例如周末。
当我在 rebase 时遇到冲突,通常最好的处理方式是执行:
git checkout --ours path/to/conflict
然后重新运行对应的 codemod。
转换前:
class BelongAnywhere extends React.Component {
render() {
return <div>Belong Anywhere</div>;
}
componentWillMount() {
console.log('欢迎回家');
}
}
转换后:
class BelongAnywhere extends React.Component {
componentWillMount() {
console.log('欢迎回家');
}
render() {
return <div>Belong Anywhere</div>;
}
}
js-codemod/template-literals
将字符串拼接转换为模板字面量。
由于我们的代码中存在大量字符串拼接,而这个 codemod 会尽可能将拼接表达式转换为模板字面量,因此有时会导致部分代码的可读性下降。我之所以将它归入“重量级”类别,是因为它涉及的文件数量很大,而且需要大量手动筛选和调整,才能得到最佳效果。
转换前:
const belongs = 'anywhere ' + welcomeHome;
转换后:
const belongs = `anywhere ${welcomeHome}`;
Codemod 学习资源
无论你是想编写自己的 codemod,还是只是想了解它们能做什么,下面这些资源都很有帮助:
- 某海外工程师的《逐步演进复杂系统》:这是他在某海外 JavaScript 技术大会上关于代码转换实践的演讲,也可以参考《高效的 JavaScript Codemod》。
- 《如何编写 Codemod》:一篇教程,带你编写一个将字符串拼接转换为模板字面量的代码转换脚本。
- 某 AST 可视化工具:用于探索不同解析器生成的抽象语法树。它非常适合用来试验,并查看你想转换的代码对应的 AST 结构。
- 《海外某大型体育组织如何使用 Codemod 迁移单体应用》:一个关于使用 codemod 迁移单体应用的案例研究。
react-codemod:React 相关 codemod 的集合。js-codemod:通用 JavaScript codemod 的集合。
Codemod 对 JavaScript 重构的影响
借助现有的 codemod,以及我们自己编写并贡献的一些转换脚本,我们很快就对旧代码做出了显著改进。我轻松地通过 codemod 修改了 4 万行代码,让大量旧代码更好地符合我们的 ES6 风格指南。
如今,我们的代码库状态良好,也已经为未来更高效的开发做好了准备。
运行现有 codemod 只是冰山一角。真正强大的地方在于:拿起键盘,开始编写你自己的转换脚本。Codemod 非常适合处理各种代码变更,从风格层面的重构,到支持破坏性 API 变更,几乎都可以胜任。
大胆发挥想象力吧。这些技术非常值得投入,因为它们能够为你,以及所有使用你项目的人,节省大量时间和精力。
文章包含AI辅助创作,作者:guo,如若转载,请注明出处:https://docs.pingcode.com/baike/5245240