为啥套娃?聊聊 Babel、Jscodeshift 和阿里妈妈的 Gogocode
首先,为啥我是套娃《babel 插件通关秘籍》 掘金小册的作者,对 babel 有源码级的聊聊里妈掌握,算是为啥有资格讨论这个话题。
本来会探讨以下话题:
babel 是套娃怎么转换代码的 jscodeshift 是怎么转换代码的 babel 和 jscodeshift 的区别 为什么不推荐 gogocodebabel 是怎么转换代码的
babel 编译流程分为 3 步:parse、transform、聊聊里妈generate
parse:把源码转成 AST,为啥babel parser(babylon) 支持 esnext 语法,套娃可通过插件支持 typescript、聊聊里妈jsx、为啥flow 等语法 transform:对 AST 进行转换,套娃通过 visitor 对不同的聊聊里妈 AST 进行处理 generate:打印转换后的 AST 为目标代码,并生成 sourcemap转换插件这样写(小册中的为啥一个 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 函数,然后在遍历的套娃过程中会被调用,其中可以拿到 path 和 state 的聊聊里妈 api:
path 是节点之间的关系,高防服务器每个 path关联父节点和当前节点,path 对象构成一条从当前节点到根结点的路径。state 是遍历过程中的共享数据的机制。
通过 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 是怎么转换代码的
jscodeshift 也是代码转换的工具,但是 api 风格不同,是主动查找 AST,然后修改成新的 AST,最后生成代码的形式:
module.exports = function(fileInfo, api) { return api.jscodeshift(fileInfo.source) .findVariableDeclarators(foo) .renameTo(bar) .toSource(); }jscodeshift 的优势是更简洁。
但是 jscodeshift 能代替 babel 么?可以看下大牛给出的答案:
babel 和 jscodeshift 的不同
jscodeshift 的 parser 是 recast,曾经有 babel 的维护者想结合这两者:
https://github.com/facebook/jscodeshift/issues/168
利用 recast 做 parse,服务器托管然后基于 babel parser 做转换。
下面有一个很精彩的回复,明确了 babel 和 jscodeshift 的不同:
我来梳理一下:
babel 的 transform api 是visitor 风格,也就是声明对什么 ast 做什么处理,然后在遍历过程中被调用,这种不和具体遍历方式耦合的写法是一种设计模式(访问者模式),处理再复杂的场景也能应对。就是处理简单场景显得稍微啰嗦点。
jscodeshift 是 collection 风格,类似 jquery,主动查找 ast,放到集合中操作,适合处理简单场景,要知道每种 ast 是怎么查找到的,然后做转换,要处理很多很多 case,万一查找路径不对,那可能就漏掉了一些情况,比起 babel 来,很难在复杂场景下没有 bug。
就像 jquery 和 mvvm 的云南idc服务商区别一样,复杂场景还是 mvvm 的方式(babel)靠谱,不会漏掉一些 dom 没处理。(只是一个类比)
所以,简单场景可以用 jscodeshift,而所有场景都可以用 babel。
babel 的 visitor 的优点就是设计模式中访问者模式的优点,不和具体遍历方式耦合易于复用。
为什么不推荐 gogocode
gogocode 是这两天阿里妈妈出的 ast 修改工具,基于 babel 做了一层封装,说是简化了 ast 操作。
api 类似这样:
$(code) .find(var a = 1) .attr(declarations.0.id.name, c) .root() .generate();没错,基于 babel 的 visitor 风格的 api 封装出了 jscodeshift 的 collection 风格的 api。
本来 babel 的 visitor 虽然写起来麻烦一些,但是所有路径都能够处理到,而改成 collection 风格之后,一旦落掉了某条路径没错里,就会 bug。处理的 case 特别多,不适合复杂场景。
babel 本来的 visitor 模式是一种优点,结果又在上层封装出了 collection api。。如果想这么封装,为啥不直接基于 jscodeshift 呢。。。我没看懂这波操作。
我不看好这个 babel 套娃,我没有自信保证复杂场景下能够处理所有路径而不遗漏 case,复杂场景我选择直接用 babel 的 api。
总结
babel 是访问者设计模式的实现,分离了遍历方式和对 AST 的操作,使得操作可以复用,jscodeshift 是 collection 风格,类似 jquery,复杂场景容易落掉 case。
gogocode 基于babel 实现了 collection 风格,不看好,容易落下 case。
一句话总结:简单场景可以用 jscodeshift,所有场景都可以用 babel,不怎么推荐 gogocode。