New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jscodeshift 简易教程 #10

Open
whxaxes opened this Issue Sep 23, 2017 · 2 comments

Comments

Projects
None yet
3 participants
@whxaxes
Owner

whxaxes commented Sep 23, 2017

背景

jscodeshift 是 fb 出的一个 codemod toolkit,基于 recast 这个 js 解析器封装了很多方便使用的工具方法。但是由于官网对使用方式的描述有点谜,刚用起来会有点蛋疼,所以写篇教程说一下。

简单先说明一下 jscodeshift 能用来干嘛,其实就是能够解析 js ,将 js 内容解析成 AST 语法树,然后提供一些便利的操作接口,方便我们对各个节点进行更改,比如更改所有的属性名之类的。比如这个官方提供的最简单的 demo:

const j = require('jscodeshift');

j(jsContent)
    .find(j.Identifier)
    .replaceWith(
      p => j.identifier(p.node.name.split('').reverse().join(''))
    );

可以实现的效果就是:

console.log('123')

会被转换为

elosnoc.gol('123')

更复杂一些的话,我们甚至可以基于 jscodeshift 来做类似于 babel 的功能,将 es6 转换为 es5,当然已经有 babel 的情况下就没必要去再实现了,那还可以做啥?就是 codemod,也就是代码自动升级工具,比如框架进行了一个大的升级,业务代码要升级框架要进行大量更改,而这些更改操作就可以通过 jscodeshift 来实现了。

使用

配套工具

在具体说 jscodeshift 如何使用之前,有个网站是必须得配合使用的,就是 jscodeshift 提供的一个配套的 ast 可视化工具 AST explorer

基本上使用 jscodeshift 都要配合这个站点上可视化的 ast tree 来实现。

比如我有一串 js 内容为下面这段

app.say = function(test) {
  console.log(test);
}

app.get('/api/config/save', checkConfigHighRiskPermission, function() {
  console.log('cool')
});

app.say('123')

你们可以自己把代码贴到 ast explorer 中用鼠标移到各个节点看看,在这里不好截那么大的图,就只截了 ast tree 的结构:

image

可以看到有三个 ExpressionStatement 结构,如果我们点开中间那个,其实也就是 app.get 那串代码,结果就如下:

image

可以看到上面那串代码被转换成了这么一种树形结构,其中 ExpressionStatement 代表的是表达式模块,也就是 app.get 整个串代码,而其中的 MemberExpression 代表的是 app.get,arguments 代表的是后面的方法参数那串,然后按顺序,Literal 就是 '/api/config/save',Identifier 就是 checkConfigHighRiskPermission,然后 FunctionExpression 就是最后的那个方法。

那么,如果我需要把上面代码中的 app.get... 的那段代码,把里面的 app.get 换成 app.post,并且把 app.get 中的那个回调方法,换成一个 generator 该怎么换?下面就介绍如何增删改查。

jscodeshift 提供了方便的 find 方法供我们快速查找到我们需要处理的节点,而查找方式就是按照 ast explorer 中的结构来查找

const ast = j(jsContent).find(j.CallExpression, {
    callee: {
        object: {
            name: 'app'
        },
        property: {
            name: 'get'
        }
    }
});

通过 find 方法,查找所有的 CallExpression,然后传入查询条件,查询条件其实就是 CallExpression 中的 json 结构,所以传入 callee.object.name 为 app,然后传入 callee.property.name 为 get,找到的 path 就是我们要的 path 了。

找到我们需要的 CallExpression 之后,先替换 app.get 为 app.post,直接接着上面的代码写:

// 找到名称为 get 的 Identifier ,然后替换成一个新的 identifier
ast.find(j.Identifier, { name: 'get' })
    .forEach(path => {
        j(path).replaceWith(j.identifier('post'));
    });

然后是替换 function 为 generator:

// 找到 app.get 表达式中的 function,替换成 generator function
ast.find(j.FunctionExpression)
    .forEach(path => {
        j(path).replaceWith(
            j.functionExpression(
                path.value.id,     // identify 方法名
                path.value.params, // 方法参数
                path.value.body,   // 方法体
                true,              // 是否为 generator
                false              // expression
            )
        )
  	})

然后再调用:

ast.toSource();

就可以看到代码已经被改成:

app.say = function(test) {
  console.log(test);
}

app.post('/api/config/save', checkConfigHighRiskPermission, function*() {
  console.log('cool')
});

app.say('123')

简单来说,在 ast explorer 出现了的 type,在 jscodeshift 中都可以用来查找,比如我要找 MemberExpression 就 j.MemberExpression,我要找 Identifier 就 j.Identifier。所以需要什么类型的节点,就 j.类型名称 就能查到所有这个类型的节点。

如果想了解所有的类型:可以戳这个链接 https://github.com/benjamn/ast-types/tree/master/def

说完类型,如果我们要创建一个某种类型的节点,就像上面的通过 replaceWith 成新的 generator 节点,也是跟类型一样的,只是首字母小写了,比如我要创建一个 MemberExpression 就调用 j.memberExpression(...args),我要创建一个 FunctionExpression 就调用 j.functionExpression(...args),而至于入参要传什么,在 ast explorer 写代码的时候,只要写了这个方法就会有入参提示:

image

知道了这些,再举个例子,我要把上面的 function 不替换成 generator 了,而是替换成箭头函数也是一样,就只需要改成使用 arrowFunctionExpression 方法即可:

ast.find(j.FunctionExpression)
    .forEach(path => {
        j(path).replaceWith(
            j.arrowFunctionExpression(
                path.value.params,   // 方法参数
                path.value.body,     // 方法体
                false                // expression
            )
        )
  	})

如果要增加节点的话 jscodeshift 也提供了两个方法,分别是 insertAfter 和 insertBefore,看方法名就可以知道,这两个方法分别是用于插前面,还是插后面。比如也是上面的 app.get 中,我想在后面的回调中再插入一个回调。就可以直接用 insertAfter:

ast.find(j.FunctionExpression)
    .forEach(path => {
        j(path).insertAfter(
            j.arrowFunctionExpression(
                path.value.params,   // 方法参数
                path.value.body,     // 方法体
                false                // expression
            )
        )
  	})

如果想删掉某个节点,则只需要 replaceWith 传入空值即可。

// 删除
j(path).replaceWith();

小技巧

再说个小技巧,如果我们需要插入一大段代码,如果按照上面的写法,就得使用 jscodeshift 的 type 方法生成一个又一个节点对象。相当繁琐。那如何来偷懒呢?比如我要在某个 path 后面加一段 console 的代码:

j(path).insertAfter(
    j(`console.log('123123')`).find(j.ExpressionStatement).__paths[0].value
)

也就是将代码转换成 ast 对象,然后再找到根节点插入到 path 后面。就可以了。

最后

上面说的 findforEachreplaceWithinsertAfterinsertBefore 方法都是比较常用,除此之外还有 filterget 等方法,具体有哪些方法可以直接看 jscodeshift 的 collection 源码。个人觉得直接看源码比看文档简单多了。

@leeleoo

This comment has been minimized.

leeleoo commented Mar 27, 2018

小技巧真是救了我的命

@yozosann

This comment has been minimized.

yozosann commented Nov 6, 2018

赞了 一直没有时间看源码,,基础操作用起来都蛋疼,感谢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment