jscodeshift 簡易教程

本文首發於 https://github.com/whxaxes/blog/issues/10node

背景

jscodeshift 是 fb 出的一個 codemod toolkit,基於 recast 這個 js 解析器封裝了不少方便使用的工具方法。可是因爲官網對使用方式的描述有點謎,剛用起來會有點蛋疼,因此寫篇教程說一下。git

簡單先說明一下 jscodeshift 能用來幹嗎,其實就是可以解析 js ,將 js 內容解析成 AST 語法樹,而後提供一些便利的操做接口,方便咱們對各個節點進行更改,好比更改全部的屬性名之類的。好比這個官方提供的最簡單的 demo:es6

const j = require('jscodeshift');

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

能夠實現的效果就是:github

console.log('123')

會被轉換爲express

elosnoc.gol('123')

更復雜一些的話,咱們甚至能夠基於 jscodeshift 來作相似於 babel 的功能,將 es6 轉換爲 es5,固然已經有 babel 的狀況下就不必去再實現了,那還能夠作啥?就是 codemod,也就是代碼自動升級工具,好比框架進行了一個大的升級,業務代碼要升級框架要進行大量更改,而這些更改操做就能夠經過 jscodeshift 來實現了。json

使用

配套工具

在具體說 jscodeshift 如何使用以前,有個網站是必須得配合使用的,就是 jscodeshift 提供的一個配套的 ast 可視化工具 AST explorerapi

基本上使用 jscodeshift 都要配合這個站點上可視化的 ast tree 來實現。babel

好比我有一串 js 內容爲下面這段app

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 源碼。我的以爲直接看源碼比看文檔簡單多了。

相關文章
相關標籤/搜索