作者|邱俊涛在这篇文章中,我想通过一些小例子来介绍使用jscodeshift进行自动重构的技术。具体来说,我想介绍的是如何在组件库的开发和维护过程中,使用jscodeshift自动修改公共API接口,将对组件用户的影响降到最低。如果您的团队开发的组件由其代码不受您控制的消费者(组织内部或外部)使用,则此处讨论的技术和模式可能会有所帮助。而如果你的日常工作更多的是使用组件库来开发应用,希望这里的知识和技能对你还是有启发的。毕竟在软件系统中,我们往往既是某些库的消费者,同时又是另外一些库的生产者。从一个简单的场景出发,想象这样一个场景,你发布了一个很酷的组件库(fancylib),里面有一个按钮(Button)组件。这个Button的一个属性是在点击后处于加载状态时显示一个小图标表示正在加载。(图片来源:https://xd.adobe.com/ideas/process/ui-design/designing-interactive-buttons-states/)在代码实现中,加载状态被定义为一个名为isInLoadingStatus的publicprop。用户可以通过设置按钮的值来控制按钮的状态:importButtonfrom'@fancylib/button';constapp=()=>(Clickme)Aninternisinacodereviewon某天的时候,提出了一个问题:在组件库的其他地方,所有的boolean状态都是用一个词表示的,比如checked,disabled等,如果按照这个约定,这里应该把isInLoadingStatus简化为loading。好主意!从“@fancylib/button”导入按钮;constapp=()=>(点我)如果所有使用Button的地方都在你的控制范围内,字符串替换大约是一个快速且80%有效的解决方案。但是稍加分析就会发现,单纯的Shift+F6会遇到很多问题。复杂的情况,比如用户为了适应用户的使用习惯,重新封装了它,使得简单的全局字符串替换变得不可能::importButtonasFancyButtonfrom'@fancylib/button';constMyEvenFancierButton=(props:FancyButtonProps)=>(consttheme={backgroundColor:"orangered",color:"white"};点我);除了这些问题之外,由于这是一个非常流行的组件库,Button在许多(内部和外部)产品中都使用,你没有办法访问所有用户代码,也没有办法让每个人都手动查找和更换更新,你需要另谋出路。您需要一个工具——一个可以读取代码意图的工具——来帮助您进行更改,并且整个过程最好是自动化的,例如通过执行脚本。使用jscodeshiftjscodeshift就是这样一个工具(toolset)。简单来说,jscodeshift的工作方式就是将源代码解析成一棵树(抽象语法树),然后提供API修改这棵树,最后将这棵树生成为代码。也就是说,她可以看懂你的代码,并提供指令(API)按照你的意愿修改相应的代码。实现下面我们通过实现一个自动重构脚本来简单介绍下jscodeshift的使用,可以完成上述场景。简单来说,jscodeshift的工作流程是:首先需要定义一个转换脚本(transform),需要满足一定的规范才能让jscodeshift调用;然后jscodeshift的命令行工具会启动runner,将转换脚本应用到一个文件或者一个文件夹下的所有文件:jscodeshift-tmyTransformsrcdefinesatransform也就是说,我们所有的逻辑都会定义在转换脚本中。转换脚本需要导出一个固定格式的函数:import{Transform}from"jscodeshift";consttransform:Transform=(file,api,options)=>{//...};导出默认转换;file是解析出来的File对象,api是jscodeshift的API对象,可以用它来查找和修改文件对象,options是一个可选对象,用来传递其他参数(比如格式化最终输出格式等)。在函数体中,我们可以使用jscodeshift提供的API来操作抽象语法树(AbstractSyntaxTree)来修改代码。这个过程很像在浏览器中通过DOMAPI操作页面元素:根据属性查找元素,对搜索结果进行增删改查,但这里的操作对象是语法树(如变量定义、函数体、函数体等)。条件语句)等)。在详细讨论如何使用jscodeshiftAPI修改代码之前,我们先简单了解一下抽象语法树的概念。这将是我们的脚本需要操作的主要对象。抽象语法树AST抽象语法树是编译器解析源代码后形成的树状结构。简单来说,我们的代码被解析成token,token根据语法规则形成子树,子树最后根据语法合并成一棵树。我们可以使用ASTExplorer工具实时查看代码对应的语法树。比如我们的代码片段:importButtonfrom'@fancylib/button';constapp=()=>(Clickme)被解析(jscodeshift默认使用babel解析,你可以选择其他解析器后),右边会形成一棵树。比如isInLoadingStatus被识别为JSXIdentifier类型,变量app定义被识别为VariableDeclarator等,所有的语法元素都会被抽取成Tokens,体现为树上的一个节点。有了这些基本概念,我们就可以开始编写一个简单的转换。这里我们可以使用ASTExplorer提供的在线IDE中的Transform功能进行实时调试(这里选择jscodeshift作为转换器)。然后我们定义这样一个转换函数://Pressctrl+spaceforcodecompletionexportdefaultfunctiontransformer(file,api){constj=api.jscodeshift;returnj(file.source).find(j.JSXIdentifier).forEach(path=>{if(path.node.name==="isInLoadingStatus"){j(path).replaceWith(j.identifier('loading'))}}).toSource();}例如,在上面的代码中,我们找到所有的j.JSXIdentifiers,并遍历每个找到的节点,如果它的值为isInLoadingStatus,则将其替换为loading。可以在右下角的调试器窗口观察转换结果:测试驱动开发当然,作为一个认真的程序员,我们不应该通过在线IDE进行开发。幸运的是,jscodeshift可以和jest完美配合。同时,我发现编写自动化脚本是一个非常适合测试驱动开发的场景:输入输出非常清晰。各种边界场景很容易想象/写入用例。每一步都可以划分相对较小的jscodeshift提供了一个小工具defineInlineTest,通过它可以很方便的定义测试用例:import{defineInlineTest}from'jscodeshift/dist/testUtils';从“./transformer”导入变压器;describe('transformer',()=>{defineInlineTest({default:transformer,parser:'tsx'},{},`importButtonfrom'@fancylib/button';exportdefault()=>(点击我);`,`importButtonfrom'@fancylib/button';exportdefault()=>(Clickme);`,'changeisInLoadingStatustoloading');});当然,如果你不习惯stringtemplates,它也提供了一个基于文件的测试定义,这样你就可以将测试的输入(转换前)和输出(转换后)外化到一个文件中,构建更多里面有复杂的。要使用的场景。例如,我们希望此转换不会意外伤害我们代码中使用的其他Button。比如我们使用另一个组件库,巧合的是那个库中的Button也有一个isInLoadingStatus。那么相应的测试用例将是:defineInlineTest({default:transformer,parser:'tsx'},{},`importButtonfrom'@facebook/button';exportdefault()=>(Clickme);`,`importButtonfrom'@facebook/button';exportdefault()=>(Clickme);`,'shouldnotchangeisInLoadingStatustoloadingfromotherpackage');相应的,我们需要在代码中加入相应的逻辑://Pressctrl+spaceforcodecompletionexportdefaultfunctiontransformer(file,api){constj=api.jscodeshift;constroot=j(file.source);constspecifiers=root.find(j.ImportDeclaration).filter((path)=>path.node.source.value==="@fancylib/button").find(j.ImportDefaultSpecifier);如果(specifiers.length===0){返回;}//...}即先查找所有import语句,如果没有找到从@fancylib/button导入的Button则跳过后续操作。您应该已经注意到,我们有很多Token定义,例如j.ImportDeclaration和j.ImportDefaultSpecifier。你可以从ASTExplorer的树结构中找到相似的名称,然后使用jscodeshiftAPI查找和访问修改后的节点。.这个过程或多或少类似于我们通过DOMAPI选择HTML节点:document.querySelectorAll('a').filter(anchor=>anchor.classList.includes('button')).forEach(anchor=>anchor.style["text-decoration"]="underline")如果你觉得这里的元素太多了,那是正常的。试着多写几句,你会找到规律的。如果把所有的实现细节都列在一篇文章里,我觉得文章会很无聊(可能写成系列教程之类的),所以这里就不贴代码了。相关的源代码可以在https://github.com/abruzzi/codemod-demo找到。可能的陷阱使用脚本来自动化重构的想法当然是非常诱人的,尤其是对于那些厌倦了修补已发布的API的人来说,这简直是太好了。但平心而论,我还是要稍微说说它的一些缺点。首先,jscodeshift的API略显晦涩,有一定的学习成本。开发过程中可能会有很多调试工作。其次,不一定覆盖100%的使用场景。例如,对于复杂的传播操作,调试和分析的工作量不容小觑,这意味着您仍然需要手动校对一些边缘情况。最后还需要一些脚本来支持组件消费团队的使用,比如自动打补丁工具等,如果有多个transform,如何一次打补丁等。总结本文我们从一个简化的实际例子入手并描述为什么jscodeshift在某些场景下可以提供帮助,比如减少大的变化可能带来的影响(如果影响是不可避免的,如何使用它就变得不那么痛苦)。然后我们描述了jscodeshift中的一些基本概念和基本工作方式,并结合前面讨论的例子实现了部分自动重构。