代码自动化重构利器——jscodeshift 初探

点击上方 程序员成长指北,关注公众号

回复1,加入Node进阶交流群

背景

开发维护规模较大的前端项目,难免时不时需要进行一些代码重构工作。举一个简单的 ????:

  • 有一个 npm 包 an-npm-package-containing-constants,用于维护项目埋点时使用的字符串常量,其代码主要内容如下:

export const ConstantsForTrack = {
  Track1: 'track_in_scene_1',
  Track2: 'track_in_scene_2',
  ...
};
  • 调用方以如下方式使用此包:

import { ConstantsForTrack } from 'an-npm-package-containing-constants';
track(ConstantsForTrack.Track1, ...otherParams);
  • 随着业务迭代,此包体积不断增长,已经开始影响到项目的整体性能。所有业务的埋点标识字符串都集中于此包中,因此我们改变代码的导出方式:

export const Track1 = 'track_in_scene_1';
export const Track2 = 'track_in_scene_2';
...
  • 各业务通过按需引入,控制代码体积:

import { Track1 } from 'an-npm-package-containing-constants/es/constants';

track(Track1, ...otherParams);

包内代码的重构不算特别复杂,但是要对项目内各处引入该包以及埋点调用的方式进行修改,还是一件比较头疼的事情。IDE 的全局替换功能显然无法胜任这一工作;人工替换违背了程序员的 DRY[1] 原则,过程枯燥且有可能出错;写一个基于正则的替换脚本倒不是不可行,只是这样的脚本一般可维护性和可复用性都不太好,并且可能存在一些 bad case(如可能会匹配到注释或字符串字面量中的内容、需要考虑不同代码风格的情况)。

此时笔者想起了之前听说到的一个叫作 codemod 的概念,据说是一种对代码进行批量修改操作的方法;并且 react[2]、vue[3] 还有 Ant Design[4] 都提供了自己的官方 codemod,以帮助用户迁移到更新版本,避免其面对升级新版本后手动处理接口变更的痛苦。这次不妨尝尝鲜,也算是为今后这种类似的重构工作先踩踩坑。

相关概念简介

Codemod[5]

Codemod is a tool/library to assist you with large-scale codebase refactors that can be partially automated but still require human oversight and occasional intervention.

Codemod 是一个诞生于 Facebook 内部的概念,可以理解为 "code modification" 的缩写。如官方介绍所述,codemod 针对的场景是规模较大的代码库中的重构工作。当某个在代码中被频繁使用的接口发生了无法向前兼容的重大变化,codemod 提供了快速且可靠的、半自动的工具来对代码库中所有相关代码进行重构,以帮助开发者对代码进行快速迭代。

jscodeshift[6]

jscodeshift 是一个基于 codemod 理念的 JavaScript/TypeScript 重构工具,其原理是将 JS/TS 代码解析为抽象语法树(Abstract Syntax Tree,AST),并提供一系列用于访问和修改 AST 的 API 以实现自动化的代码重构。jscodeshift 将 babel parser、ast-types[7](用于快速创建新的 AST 节点)和 recast[8](维护生成代码的代码风格信息)三大工具整合在一起,提供了简便快捷的操作接口;同时它还提供了多任务并行执行的功能,使其对于海量代码文件的重构操作可以并行运行,充分利用多核 CPU 算力,缩短重构任务执行时间。

抽象语法树

相信大家都在编译原理的课程中了解过抽象语法树的概念,这里先引用一段维基百科上的描述:

在计算机科学中,抽象语法树Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节

我们再以一段简单代码为例:

if (1 + 1 == 3) {
  alert('time to wake up!');
}

来更加具体地了解一下抽象语法树是什么样子:(由 http://nhiro.org/learn_language/AST-Visualization-on-browser.html 生成)

在 JavaScript 工程领域中,不仅仅只有 JavaScript 引擎解析代码会涉及到 AST,在代码转译(babel)、静态分析(eslint)、打包构建(webpack、rollup...)中,都会将代码解析为 AST 以作进一步的操作。

动手开干,写一个 codemod

jscodeshift 虽然很好很强大,但是官方文档提供的信息对于帮助我们快速写出一个 codemod 来说却相对有限。因此了解掌握 jscodeshift 最好的办法就是 get hands dirty,参考一些已有的 codemod 脚本和网上的文章,自己写一个 codemod。我们以解决背景一节提及的这个问题为例,写一个 codemod 来完成 an-npm-package-containing-constants 引入和使用方式的修改。

在动手之前,先介绍一个超强的 AST 可视化工具—— AST Explorer[9]。如下图所示,我们把想要修改的代码粘贴在左侧,即可即时在右侧看到解析代码获得的语法树并查看其中各个节点的属性。通过这个工具,我们可以对 AST 有一个更加直观的认识;在编写 codemod 的过程中,我们也可以通过这个工具快速定位需要修改的节点。打开 "Transform" 开关,你还可以直接在浏览器里书写 codemod 脚本,并即时看到 codemod 转换后的效果。

通过对代码及其 AST 的大致分析,我们可以把替换工作拆解为以下几步:

  1. 遍历文件,筛选出引入了 "an-npm-package-containing-constants" 的代码文件;

  2. 查找筛选出的文件中所有对 ConstantsForTrack 对象成员的访问,并将 ConstantsForTrack 成员访问表达式直接替换为常量名的表达式;

  3. 收集代码中使用的常量名,生成新的 import 语句并替换旧语句。

现在我们可以开始编写 codemod 了!根据官方文档[10],codemod 代码需导出一个函数,jscodeshift 在运行此函数时会通过参数注入 API 和一些相关信息。由此我们可以写出一个 codemod 的初步的框架:

module.exports = function (
  fileInfo, // 当前处理文件的相关信息,包括文件路径与内容
  api, // jscodeshift 提供的接口
  options // 通过 jscodeshift CLI 传入的参数
) {
  const { source, path } = fileInfo;
  const { jscodeshift: j } = api;
  const root = j(source);  // 解析代码获得 AST

  // 在这里编写操作 AST 的代码

  return root.toSource({ quote: 'single' }); // 将 AST 转换为代码字符串后返回
}

需要注意的是,代码最后的 toSource() 函数可以传入一些代码风格相关的配置。recast 在解析代码时,会将代码风格相关信息维护在语法树中,在 toSource() 过程中再将代码还原成原本的样子。而在代码转换为语法树后新插入的节点并没有这些具体的代码风格信息。具体如何配置这些代码的风格可以参考这个文件[11]里的接口定义。

第一步:筛选需要修改的文件

由于我们只需关注引入了 an-npm-package-containing-constants 的代码文件,所以我们先在 AST Explorer 里观察解析 import { ConstantsForTrack } from 'an-npm-package-containing-constants'; 语句获得的 AST 子树,可以发现这棵子树的根结点类型是 "ImportDeclaration",节点中 source 属性的 value 字段就是我们所寻找的节点特征 "an-npm-package-containing-constants"。

接下来就是编写代码将这类节点筛选出来。鉴于 AST 的数据结构特征,一般的语法解析器 (parser) 以及 recast 提供的 API 都基于访问者模式来对 AST 进行遍历 (traversal)。jscodeshift 基于 Collection 的概念对 API 进行了进一步封装[12],令使用者对 AST 节点的筛选及修改操作变得更加简单。一些常用的 Collection API 我们会在接下来的实践过程中用到,完整的 API 列表可以参阅以下源码:

集合基本操作:https://github.com/facebook/jscodeshift/blob/master/src/Collection.js AST 节点访问与修改:https://github.com/facebook/jscodeshift/blob/master/src/collections/Node.js

这里我们使用 find() 从语法树中获得目标节点的集合,若集合为空,则跳过此文件。代码如下:

const trackConstantsImportDeclarations = root.find(j.ImportDeclaration, {
  source: { value: 'an-npm-package-containing-constants' }
});

if (!trackConstantsImportDeclarations.length) {
  // 返回 undefined 表示此文件无需修改
  return;
}

第二步:收集代码中所有相关常量的访问并进行替换

我们再来分析一下 ConstantsForTrack.Track2

可以看到这个表达式被解析成了一个 MemberExpression 类型的节点,该节点的 object 属性是一个 name 为 "ConstantsForTrack" 的 Identifier 节点,property 是一个 name 为 "Track2" 的 Identifier 节点。

我们要把所有 ConstantForTrack.[constant name] 替换为 [constant name],只需使用 jscodeshift 提供的 replaceWith() 接口,把相应的 MemberExpression 节点替换为其 property 属性中的 Identifier 节点即可。另外为了下一步将这些常量引入进来,我们需要把这些 Identifier 的 name 属性收集起来。代码如下:

let usedKeys = [];
const trackConstantsMemberExpressions = root.find(j.MemberExpression, {
  object: { name: 'ConstantsForTrack' }
});
trackConstantsMemberExpressions.replaceWith((nodePath) => {
  // replaceWith 在遍历集合的回调函数中传入的参数类型是 NodePath
  // NodePath 除了节点自身的信息外还包含节点的上下文信息,因此需要先把节点从中取出来
  const { node } = nodePath;
  const keyId = node.property;
  if (keyId.name) {
    usedKeys.push(keyId.name);
    return keyId;
  }
});
if (!usedKeys.length) {
  return;
}

第三步:替换 import 语句

这一步的目标是将 import { ConstantsForTrack } from 'an-npm-package-containing-constants'; 替换为 import { Track2 } from 'an-npm-package-containing-constants/es/constants';。我们先分析一下后者的语法树:

可见 ImportDeclaration 的 specifiers 属性记录了 import 语句所引入的内容,以 ImportSpecifier 数组的形式表示。现在我们可以重新构造一个新的 ImportDeclaration 节点,代码如下:

usedKeys = [...new Set(usedKeys)];

const keyIds = usedKeys.map((key) => j.importSpecifier(j.identifier(key)));
const trackConstantsEsImportDeclaration = j.importDeclaration(
  keyIds,
  j.literal("an-npm-package-containing-constants/es/constants")
);
// 替换原来的 import 语句
trackConstantsImportDeclarations.at(0).replaceWith(
  () => trackConstantsEsImportDeclaration
);

jscodeshift 基于 ast-types 提供了各种节点的构建接口[13],接口形式如以上代码所示,以驼峰命名法(小写字母开头)形式表示,与第一步中用于筛选的节点类型(帕斯卡命名法表示,大写字母开头)区分开来。不同节点构建方法的具体参数可以参阅源代码[14],AST Explorer 中也提供了相关代码提示。

至此我们的 codemod 脚本已经完成,可以尝试执行一下:(在运行之前,你需要保证已经通过 npm install -g jscodeshift 全局安装上了 jscodeshift)

自动重构顺利完成!这里介绍几个常用的 CLI 参数,更加具体的信息可以参阅这里[15]

-c, --cpus=N            最多开启 N 个子进程并行运行 codemod 脚本
                       (默认为 max(CPU 总核心数 - 1, 1))
-d, --(no-)dry          测试运行,不对文件作实际修改(默认关闭)
--extensions=EXT        需要处理的文件扩展名(多个用“,”隔开,默认为 js)
--parser=babel|babylon|flow|ts|tsx
                        解析文件使用的 parser,默认为 babel
-t, --transform=FILE    codemod 脚本的路径或 URL,默认为 "./transform.js"
-v, --verbose=0|1|2     展示 codemod 执行过程中的相关信息

总结

文章的最后,总结一下使用 jscodeshift 常用的 API 以及相关的参考文档和源码:

  • AST 的查找与筛选:find()filter()

  • Collection 访问:get()at()(两者区别在于前者返回 NodePath,后者返回 Collection

  • 节点的插入与修改:replaceWith()insertBefore()insertAfter()

Collection 常用 API: https://github.com/facebook/jscodeshift/blob/master/src/Collection.js https://github.com/facebook/jscodeshift/blob/master/src/collections/Node.js

AST 节点构建参数:https://github.com/benjamn/ast-types/tree/master/def

jscodeshift CLI 参数:https://github.com/facebook/jscodeshift#usage-cli

希望本文可以为大家的代码重构工作提供一些启发,帮助大家从一些重复的代码修改工作中解放出来。

来源:字节前端 Byte FE

最后

如果觉得这篇文章还不错

点击下面卡片关注我

来个【分享、点赞、在看】三连支持一下吧

   “分享、点赞、在看” 支持一波  

特别说明 -------- 新版本请访问网站www.bluefishes.net. 考虑到稳定性,新版本不支持Visual Studio.NET 2002. 产品名称 -------- SharpRefactor(C#代码重构工具) 产品简述 -------- 本工具用于代码重构代码自动生成。现阶段主要用于C#代码重构。 所谓重构也就是“保持软件的外在功能不变,重新调整其内部结构”。 关于每种重构模式的含义,请参见http://www.refactoring.com/ 具体功能参见具体版本的特性列表。 对重构很感兴趣或是很关注使用效率的用户,希望[使用指南]一节对你有所助益。 版本 ---- 1.0.0(BETA). 发布日期 -------- 2003/6/13 作者 ---- C# Refactor Team. 制作 ---- Blue Workshop. 环境要求 -------- Visual Studio.Net 2003 Windows 2000 + SP2 + SMTP Service 特别提示 -------------- 本插件使用了异常处理和报告机制。 一般而言,环境、代码以及其他原因都会导致程序出错。因此,在您使用本插件的过程中,可能会弹出错误报告。一部分错误不会影响使用,另一部分会影响使用。 C# Refactor Team愿意随时提供技术支持,及时为你解除问题。 版本1.0.0特性 ------------- Rename Parameter Rename Local Variable Rename Field Rename Property Rename Class Rename NameSpace Safe Delete Parameter Safe Delete Local Variable Safe Delete Field Safe Delete Property Safe Delete Method Safe Delete Class Safe Delete NameSpace Extract Interface Undo/Redo Preview usage before refactor(重构前预览) Auto build after refactor(重构后自动生成) Options(工具选项) User feedback(用户反馈) 使用指南 -------- 所有功能暂不支持静态成员。 尽量使用鼠标右键菜单。 尽量使用快捷方式,比如:单击鼠标右键,弹出菜单后再连续按‘R’键和‘C’键就可以调用[Rename]菜单下的[Rename Class]命令。 在使用Rename系列命令时,需要先转到定义代码元素的地方。此时,可以先使用右键菜单中的[转到定义]命令。 在Option中可以设置首选项。 由于Visual Studio在生成较大的解决方案时有时会不成功,所以Auto build after refactor通常用于较小的解决方案。 Rename NameSpace与Move Class不同。Move Class的焦点在Class,即改变类所在的NameSpace。而Rename NameSpace的焦点在NameSpace,即改变指定NameSpace的名字,并更新该NameSpace的所有引用(Usages)。 错误报告以及建议功能需要网络连接和Windows自带的SMTP服务。因为发送速度很快,所以不会占用您宝贵的时间。 可以使用User feedback功能提出您睿智的建议、批评、任何意见。 技术支持 -------- Tiger.BlueWorkshop@163.net 下载 ---- www.csdn.net 版本 发布日期 ----------------------------- 1.0.0(Beta) 2003/6/13
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值