如何使用jscodeshift来自动修改公开的API接口

在这篇文章里我想要通过一些小例子来介绍使用jscodeshift来进行自动化重构的技术。具体来说,我想要介绍在一个组件库的开发和维护过程中,如何使用jscodeshift来自动修改公开的api接口,从而尽可能小的产生对组件用户的影响。
如果你们团队开发的组件被其消费者(组织内部或者外部)使用了,而这些代码又不在你的控制之内,那么这里讨论的技术和模式可能对你很有帮助。而如果你的日常工作更多的是使用组件库来开发应用程序,我希望这里的知识和技巧仍然对你有所启发,毕竟在软件系统中,我们往往都既是某些库的消费者,又同时是另外一些库的生产者。
从一个简单场景出发 设想这样一个场景,你发布了一个酷炫的组件库(fancylib),其中有一个按钮(button)组件。这个button的一个属性是当点击后处于加载中(loading)状态时现实一个表示加载中的小图标。
(图片来源:https://xd.adobe.com/ideas/process/ui-design/designing-interactive-buttons-states/)
在代码实现中,这个加载中状态被定义为了名为isinloadingstatus公开prop。用户可以通过设置其值来控制button的状态:
import button from '@fancylib/button';const app = () => (click me) 一个实习生在某一天code review的时候提出了一个问题:在组件库中的其他地方,所有的boolean状态都是用一个单词来表示的,比如checked, disabled等。如果按照这个惯例,这里应该把isinloadingstatus简化为loading。好主意!
import button from '@fancylib/button';const app = () => (click me) 假如所有用到button的地方都在你的控制之内,字符串替换大约是一个快速且80%有效的方案。不过稍微分析一下,你就会发现简单的shift+f6会遇到很多问题。
复杂情况 比如用户对其做了二次包装以适配更符合自己用户的使用习惯,这使得简单的全局字符串替换变成了不可能::
import button as fancybutton from '@fancylib/button';const myevenfancierbutton = (props: fancybuttonprops) => ( const theme = { backgroundcolor: orangered, color: white }; click me); 除了这些问题之外,由于这是一个非常受欢迎的组件库,button在很多(包括内部和外部的)产品中都有使用,你没有办法访问所有的用户代码,更没有办法让所有人都用手工的查找替换来做更新,你需要另寻出路。
你需要一个工具 -- 一个可以读懂代码意图的工具 -- 来帮助你做修改,而且整个过程最好可以自动化,比如通过执行一个脚本来完成。
使用jscodeshift jscodeshift就是这样一个工具(工具集)。简单来说,jscodeshift的工作方式就是将源代码分析成一棵树(抽象语法树),然后提供api来修改这棵树,最后再把树生成为代码。
也就是说,她可以读懂你的代码,并提供指令(api)来根据你的意愿修改相应的代码。
实现 接下来,我们可以通过实现一个可以完成上述场景的自动重构的脚本来对jscodeshift的使用做一个简单介绍。简单来说,jscodeshift的工作流程是:首先你需要定义一个转换脚本(transform),这个脚本需要符合一定的规范以便jscodeshift调用;然后jscodeshift的命令行工具会启动runner,并将转换脚本应用到某个文件或者某个文件夹中的所有文件中:
jscodeshift -t mytransform src
定义一个transform 也就是说,我们所有的逻辑都会定义在转换脚本中。transform脚本需要导出一个固定格式的函数:
import { transform } from jscodeshift;const transform: transform = (file, api, options) => { //...};export default transform; file为解析后的文件对象,api是jscodeshift的api对象,可以通过它来查找,修改文件对象,options是一个可选的,用来传递其他参数(比如格式化最终输出格式等)的对象。在函数体中,我们可以使用jscodeshift提供的api来操纵抽象语法树(abstract syntax tree)来实现对代码的修改。这个过程和通过dom api来操作浏览器中的页面元素非常类似:按照属性查找元素,对查找结果进行增删改等操作,只不过这里的操作对象是语法树(比如变量定义,函数体,条件语句等等)。
在详细讨论如何使用jscodeshift的api来修改代码之前,我们来略微看一下抽象语法树的概念。这将是我们脚本需要操作的主要对象。
抽象语法树ast 抽象语法树,是编译器将源码解析(parse)之后形成的一课树形结构。简单来说,我们的代码被解析成为token,token再根据语法规则形成子树,子树最终根据文法归并成一颗树。我们可以通过ast explorer工具来实时查看代码对应的语法树。
举个例子,我们的代码片段:
import button from '@fancylib/button';const app = () => (click me) 经过解析(jscodeshift默认使用babel来解析,你可以选择其他的解析器)之后,会形成右侧的一颗树,比如isinloadingstatus被识别成jsxidentifier类型,而变量app定义则被识别为variabledeclarator等。所有符合语法的元素都会被抽取成token,并体现为树上的一个节点。
有了这些基本概念之后,我们就可以开始编写一个简单的transform了。这里我们可以通过ast explorer提供的在线ide中的transform功能来实时调试(此处选择jscodeshift作为转换器)。
然后我们定义这样一个转换函数:
// press ctrl+space for code completionexport default function transformer(file, api) { const j = api.jscodeshift; return j(file.source) .find(j.jsxidentifier) .foreach(path => { if(path.node.name === isinloadingstatus) { j(path).replacewith( j.identifier('loading') ) } }) .tosource();} 比如上述代码中,我们查找所有的j.jsxidentifier,并迭代每一个找到的节点,如果它的值是isinloadingstatus的话,就将其替换为loading。可以观察到右下侧的调试器窗口中的转换结果:
测试驱动开发 当然了,作为一个严肃的程序员,我们不应该通过一个在线ide来进行开发。幸运的是jscodeshift可以和jest完美配合,同时我发现编写自动化脚本是一个非常适合测试驱动开发的场景:
输入输出都非常明确 各种不同的边界场景很容易想象/编写成用例 每一个步骤都可以划分的比较小 jscodeshift提供了一个小工具defineinlinetest,通过它你可以很方便的定义测试用例:
import { defineinlinetest } from 'jscodeshift/dist/testutils';import transformer from './transformer';describe('transformer', () => { defineinlinetest( { default: transformer, parser: 'tsx' }, {}, ` import button from '@fancylib/button'; export default () => (click me ); `, ` import button from '@fancylib/button'; export default () => (click me ); `, 'change isinloadingstatus to loading' );}); 当然,如果你不习惯字符串模板的话,它同时还提供了基于文件形式的测试定义,这样你可以将测试的输入(转化前)和输出(转化后)外置到文件中,并在其中构建较为复杂的使用场景。
比如我们希望这个transform不要误伤我们代码中使用的其他button,比如我们使用了另外一个组件库,而巧合的是那个库中button也有一个isinloadingstatus。
那么对应的测试用例会是:
defineinlinetest( { default: transformer, parser: 'tsx' }, {}, ` import button from '@facebook/button'; export default () => (click me ); `, ` import button from '@facebook/button'; export default () => (click me ); `, 'should not change isinloadingstatus to loading from other package' ); 对应的我们需要在代码中加入相应的逻辑:
// press ctrl+space for code completionexport default function transformer(file, api) { const j = api.jscodeshift; const root = j(file.source); const specifiers = root .find(j.importdeclaration) .filter((path) => path.node.source.value === @fancylib/button) .find(j.importdefaultspecifier); if (specifiers.length === 0) { return; } //...} 即,我们先查找所有的import语句,如果没有找到从@fancylib/button导入的button就跳过后续的操作。你应该已经注意到了,我们这里又很多的诸如j.importdeclaration和j.importdefaultspecifier之类的token定义,你可以从ast explorer的树结构中找到类似的名称,然后用jscodeshift的api来查找并访问改节点。
这个过程或多或少有点像我们通过dom的api来选择html节点一样:
document.queryselectorall('a') .filter(anchor => anchor.classlist.includes('button')) .foreach(anchor => anchor.style[text-decoration] = underline) 如果你觉得这里要素太多,这是很正常的。尝试着多写几个就会发现规律。
如果把所有的实现细节都列举在一篇文章中,我觉得文章会非常枯燥(可能写成一个系列教程等),因此这里我不再贴代码,相关的源码可以在这里找到。
可能的陷阱 使用脚本来自动化重构的想法当然非常有诱惑了,特别是对于疲于为已经公布的api打补丁的人们来说,简直太过于美好。不过公平起见,我还是得略微说一些它的一些drawbacks。
首先,jscodeshift 的api略显晦涩,有一定的学习成本。开发过程中可能会有很多调试的工作。其次,它并不定覆盖100%的使用场景,比如对于复杂的spreading操作,需要调试和分析的工作量不容小觑,也就是说你仍然需要人工校对一些edge cases。最后,需要一些脚本来支持组件的消费团队使用,比如自动化补丁工具等,如果有多个transform,如何一次patch等问题。
小结 在这篇文章中,我们从一个简化了的实际例子出发,描述了为何jscodeshift在某些场景下可以提供的帮助,比如降低大型修改可能带来的影响(而如果影响不可避免,那么如何使其变得不那么痛苦)。随后我们描述了jscodeshift中的一些基本概念和基本的工作方式,并结合之前讨论的例子实现了部分的自动化重构。


大数据热潮下,如何对税收风险进行管理
华为P40 Pro迎来大幅优惠,价格下降300元
LED驱动器如何实现理想的驱动
JOLED获四家日企出资470亿日圆,对抗韩厂扩大OLED生产
哪款蓝牙耳机配置比较的高,高配置蓝牙耳机推荐
如何使用jscodeshift来自动修改公开的API接口
PCB布线电路板设计
基于低功耗单片机MSC1212实现绝热材料导热系数测定仪的设计
英飞凌针对车载通信推出经过认证并具备ESD防护性能的FlexRay收发器
蓝海华腾伺服产品家族又添新成员!
虹科携手其合作伙伴Port推出新的CC-Link IE TSN 方案,以提高精度和生产力
2021年的Xbox系列有哪些改进?
搭载了语音控制技术的显示器将会变得更加智能
开关式DC/DC转换器的选择要点详悉
MediaTek与广和通研发的5G模组助力终端用户畅享高速网络连接体验
学习嵌入式需要具备怎样的基础
硅基锂电池是什么?充电容量比传统电池翻10倍
智能电子皮肤为可穿戴医疗器械带来了新希望
面部识别为何成为全球投资者的新宠?
小米铁蛋机器狗多少钱 功能有哪些