為啥套娃?聊聊 Babel、Jscodeshift 和阿里媽媽的 Gogocode
首先,我是《babel 插件通關(guān)秘籍》 掘金小冊的作者,對 babel 有源碼級的掌握,算是有資格討論這個話題。
本來會探討以下話題:
- babel 是怎么轉(zhuǎn)換代碼的
- jscodeshift 是怎么轉(zhuǎn)換代碼的
- babel 和 jscodeshift 的區(qū)別
- 為什么不推薦 gogocode
babel 是怎么轉(zhuǎn)換代碼的
babel 編譯流程分為 3 步:parse、transform、generate
- parse:把源碼轉(zhuǎn)成 AST,babel parser(babylon) 支持 esnext 語法,可通過插件支持 typescript、jsx、flow 等語法
- transform:對 AST 進行轉(zhuǎn)換,通過 visitor 對不同的 AST 進行處理
- generate:打印轉(zhuǎn)換后的 AST 為目標(biāo)代碼,并生成 sourcemap
轉(zhuǎn)換插件這樣寫(小冊中的一個 linter 的案例):
- const { declare } = require('@babel/helper-plugin-utils');
- const noFuncAssignLint = declare((api, options, dirname) => {
- api.assertVersion(7);
- return {
- pre(file) {
- file.set('errors', []);
- },
- visitor: {
- AssignmentExpression(path, state) {
- const errors = state.file.get('errors');
- const assignTarget = path.get('left').toString()
- const binding = path.scope.getBinding(assignTarget);
- if (binding) {
- if (binding.path.isFunctionDeclaration() || binding.path.isFunctionExpression()) {
- const tmp = Error.stackTraceLimit;
- Error.stackTraceLimit = 0;
- errors.push(path.buildCodeFrameError('can not reassign to function', Error));
- Error.stackTraceLimit = tmp;
- }
- }
- }
- },
- post(file) {
- console.log(file.get('errors'));
- }
- }
- });
- module.exports = noFuncAssignLint;
聲明 visitor 函數(shù),然后在遍歷的過程中會被調(diào)用,其中可以拿到 path 和 state 的 api:
path 是節(jié)點之間的關(guān)系,每個 path關(guān)聯(lián)父節(jié)點和當(dāng)前節(jié)點,path 對象構(gòu)成一條從當(dāng)前節(jié)點到根結(jié)點的路徑。state 是遍歷過程中的共享數(shù)據(jù)的機制。
通過 path 的一系列增刪改查 AST 的 api 來完成 transform。
比如下列 api:
- getSibling(key)
- getNextSibling()
- getPrevSibling()
- getAllPrevSiblings()
- getAllNextSiblings()
- isXxx(opts)
- assertXxx(opts)
- insertBefore(nodes)
- insertAfter(nodes)
- replaceWith(replacement)
- replaceWithMultiple(nodes)
- replaceWithSourceString(replacement)
- remove()
jscodeshift 是怎么轉(zhuǎn)換代碼的
jscodeshift 也是代碼轉(zhuǎn)換的工具,但是 api 風(fēng)格不同,是主動查找 AST,然后修改成新的 AST,最后生成代碼的形式:
- module.exports = function(fileInfo, api) {
- return api.jscodeshift(fileInfo.source)
- .findVariableDeclarators('foo')
- .renameTo('bar')
- .toSource();
- }
jscodeshift 的優(yōu)勢是更簡潔。
但是 jscodeshift 能代替 babel 么?可以看下大牛給出的答案:
babel 和 jscodeshift 的不同
jscodeshift 的 parser 是 recast,曾經(jīng)有 babel 的維護者想結(jié)合這兩者:
https://github.com/facebook/jscodeshift/issues/168
利用 recast 做 parse,然后基于 babel parser 做轉(zhuǎn)換。
下面有一個很精彩的回復(fù),明確了 babel 和 jscodeshift 的不同:
我來梳理一下:
babel 的 transform api 是visitor 風(fēng)格,也就是聲明對什么 ast 做什么處理,然后在遍歷過程中被調(diào)用,這種不和具體遍歷方式耦合的寫法是一種設(shè)計模式(訪問者模式),處理再復(fù)雜的場景也能應(yīng)對。就是處理簡單場景顯得稍微啰嗦點。
jscodeshift 是 collection 風(fēng)格,類似 jquery,主動查找 ast,放到集合中操作,適合處理簡單場景,要知道每種 ast 是怎么查找到的,然后做轉(zhuǎn)換,要處理很多很多 case,萬一查找路徑不對,那可能就漏掉了一些情況,比起 babel 來,很難在復(fù)雜場景下沒有 bug。
就像 jquery 和 mvvm 的區(qū)別一樣,復(fù)雜場景還是 mvvm 的方式(babel)靠譜,不會漏掉一些 dom 沒處理。(只是一個類比)
所以,簡單場景可以用 jscodeshift,而所有場景都可以用 babel。
babel 的 visitor 的優(yōu)點就是設(shè)計模式中訪問者模式的優(yōu)點,不和具體遍歷方式耦合易于復(fù)用。
為什么不推薦 gogocode
gogocode 是這兩天阿里媽媽出的 ast 修改工具,基于 babel 做了一層封裝,說是簡化了 ast 操作。
api 類似這樣:
- $(code)
- .find('var a = 1')
- .attr('declarations.0.id.name', 'c')
- .root()
- .generate();
沒錯,基于 babel 的 visitor 風(fēng)格的 api 封裝出了 jscodeshift 的 collection 風(fēng)格的 api。
本來 babel 的 visitor 雖然寫起來麻煩一些,但是所有路徑都能夠處理到,而改成 collection 風(fēng)格之后,一旦落掉了某條路徑?jīng)]錯里,就會 bug。處理的 case 特別多,不適合復(fù)雜場景。
babel 本來的 visitor 模式是一種優(yōu)點,結(jié)果又在上層封裝出了 collection api。。如果想這么封裝,為啥不直接基于 jscodeshift 呢。。。我沒看懂這波操作。
我不看好這個 babel 套娃,我沒有自信保證復(fù)雜場景下能夠處理所有路徑而不遺漏 case,復(fù)雜場景我選擇直接用 babel 的 api。
總結(jié)
babel 是訪問者設(shè)計模式的實現(xiàn),分離了遍歷方式和對 AST 的操作,使得操作可以復(fù)用,jscodeshift 是 collection 風(fēng)格,類似 jquery,復(fù)雜場景容易落掉 case。
gogocode 基于babel 實現(xiàn)了 collection 風(fēng)格,不看好,容易落下 case。
一句話總結(jié):簡單場景可以用 jscodeshift,所有場景都可以用 babel,不怎么推薦 gogocode。