最近在一些项目编译系统的工作中涉及到了很多关于babel插件的开发,关于babel大多数人的感受可能是既陌生又熟悉,可能大多数人对于babel的应用场景的认识就是在webpack中使用一个babel-loader,但当你真正了解他掌握它的时候,会发现他其实还有些更强的用法。。。

基本概念

babel是什么?

Babel 是一个编译器(输入源码 => 输出编译后的代码)。就像其他编译器一样,编译过程分为三个阶段:解析、转换和打印输出。(官网的解释)。

babel plugin和babel preset是什么?

babel中有很多概念,比如:插件(plugin),预设(preset)和一些比较基础的工具(例如@babel/parser,@babel/traverse等等)。关于他们的关系,可以理解为babel的plugin构建在基础工具之上,而babel的preset是多个babel plugin的打包集合,例如我们所熟悉的@babel/preset-env,@babel/preset-react。

babel深入

本篇文章不对babel官方的plugin,preset库做过多阐述,毕竟这是一篇深入教程。我们要提的是一个更本质的问题:babel是如何转译代码的?

我们大体上把这个转译代码的过程分为三步:

  • 第一步(parse):code=>ast
  • 第二步(transform):ast=>修改过的ast
  • 第三步(generate):修改过的ast=>编译后的code

这三步分别对应babel的三个基本工具,第一步对应@babel/parser,第二步对应@babel/traverse,第三步对应@babel/generator。下面就来详述一下这三个过程。

parse(@babel/parser)

这一步是babel将code转化为ast。ast是Abstract syntax tree的缩写,即抽象语法树,单说抽象语法树可能不太好理解,我们可以先来看一下一个具体的例子,你可以使用IT虾米网来帮你运行@babel/parser:

function mirror(something) { 
  return something 
}

被转译成ast:

{ 
  "type": "File", 
  "start": 0, 
  "end": 49, 
  "loc": { 
    "start": { 
      "line": 1, 
      "column": 0 
    }, 
    "end": { 
      "line": 3, 
      "column": 1 
    } 
  }, 
  "program": { 
    "type": "Program", 
    "start": 0, 
    "end": 49, 
    "loc": { 
      "start": { 
        "line": 1, 
        "column": 0 
      }, 
      "end": { 
        "line": 3, 
        "column": 1 
      } 
    }, 
    "sourceType": "module", 
    "interpreter": null, 
    "body": [ 
      { 
        "type": "FunctionDeclaration", 
        "start": 0, 
        "end": 49, 
        "loc": { 
          "start": { 
            "line": 1, 
            "column": 0 
          }, 
          "end": { 
            "line": 3, 
            "column": 1 
          } 
        }, 
        "id": { 
          "type": "Identifier", 
          "start": 9, 
          "end": 15, 
          "loc": { 
            "start": { 
              "line": 1, 
              "column": 9 
            }, 
            "end": { 
              "line": 1, 
              "column": 15 
            }, 
            "identifierName": "mirror" 
          }, 
          "name": "mirror" 
        }, 
        "generator": false, 
        "async": false, 
        "params": [ 
          { 
            "type": "Identifier", 
            "start": 16, 
            "end": 25, 
            "loc": { 
              "start": { 
                "line": 1, 
                "column": 16 
              }, 
              "end": { 
                "line": 1, 
                "column": 25 
              }, 
              "identifierName": "something" 
            }, 
            "name": "something" 
          } 
        ], 
        "body": { 
          "type": "BlockStatement", 
          "start": 27, 
          "end": 49, 
          "loc": { 
            "start": { 
              "line": 1, 
              "column": 27 
            }, 
            "end": { 
              "line": 3, 
              "column": 1 
            } 
          }, 
          "body": [ 
            { 
              "type": "ReturnStatement", 
              "start": 31, 
              "end": 47, 
              "loc": { 
                "start": { 
                  "line": 2, 
                  "column": 2 
                }, 
                "end": { 
                  "line": 2, 
                  "column": 18 
                } 
              }, 
              "argument": { 
                "type": "Identifier", 
                "start": 38, 
                "end": 47, 
                "loc": { 
                  "start": { 
                    "line": 2, 
                    "column": 9 
                  }, 
                  "end": { 
                    "line": 2, 
                    "column": 18 
                  }, 
                  "identifierName": "something" 
                }, 
                "name": "something" 
              } 
            } 
          ], 
          "directives": [] 
        } 
      } 
    ], 
    "directives": [] 
  }, 
  "comments": [] 
}

乍一看似乎很复杂,但是你要做的是从中找到关键信息,我们将当中影响阅读的字段去除(去除loc,start,end,以及函数体外层的嵌套):

{ 
  "type": "FunctionDeclaration", 
  "id": { 
    "type": "Identifier", 
    "name": "mirror" 
  }, 
  "generator": false, 
  "async": false, 
  "params": [ 
    { 
      "type": "Identifier", 
      "name": "something" 
    } 
  ], 
  "body": { 
    "type": "BlockStatement", 
    "body": [ 
      { 
        "type": "ReturnStatement", 
        "argument": { 
          "type": "Identifier", 
          "name": "something" 
        } 
      } 
    ], 
    "directives": [] 
  } 
}

这样是不是简单很多!我们看一下这个json描述了什么:外层是一个叫mirror的函数声明,他的传参有一个,叫something,函数体内部return了一个叫something的变量。我们把这个描述与上边的js代码对照着看,竟然不谋而合(其实从这一点也能看出code<=>ast这个过程是可逆的)。对于初学者而言,上边的抽象语法树难以理解的可能是这些名字冗长的节点type,下边简单列举一下js中的常见的节点名称(慎看,可以选择性跳过,但是了解这些节点名称可以加深你对babel甚至js语言本身的理解)。详见IT虾米网

FunctionDeclaration(函数声明) 
 
function a() {} 
 
FunctionExpression(函数表达式) 
 
var a = function() {} 
 
ArrowFunctionExpression(箭头函数表达式) 
 
()=>{}(此处可以思考:为什么没有箭头函数声明,以及Declaration和Expression的区别) 
 
AwaitExpression(await表达式) 
 
async function a () { await b() } 
 
CallExpression(调用表达式) 
 
a() 
 
MemberExpression(成员表达式) 
 
a.b 
 
VariableDeclarator(变量声明) 
 
var,const,let(var,const,let用Node中的kind区分) 
 
Identifier(变量标识符) 
 
var a(这里a是一个Identifier) 
 
NumericLiteral(数字字面量) 
 
var a = 1 
 
StringLiteral(字符串字面量) 
 
var a = 'a' 
 
BooleanLiteral(布尔值字面量) 
 
var a = true 
 
NullLiteral(null字面量) 
 
var a = null(此处可以思考:为什么没有undefined字面量) 
 
BlockStatement(块) 
 
{} 
 
ArrayExpression(数组表达式) 
 
[] 
 
ObjectExpression(对象表达式) 
 
var a = {} 
 
SpreadElement(扩展运算符) 
 
{...a},[...a] 
 
ObjectProperty(对象属性) 
 
{a:1}(这里的a:1是一个ObjectProperty) 
 
ObjectMethod(函数属性) 
 
{a(){}} 
 
ExpressionStatement(表达式语句) 
 
a() 
 
IfStatement(ifif () {} 
 
ForStatement(forfor (;;){} 
 
ForInStatement(for infor (a in b) {} 
 
ForOfStatement(for of) 
 
for (a of b) {} 
 
ImportDeclaration(import声明) 
 
import 'a' 
 
ImportDefaultSpecifier(import default说明符) 
 
import a from 'a' 
 
ImportSpecifier(import说明符) 
 
import {a} from 'a' 
 
NewExpression(new表达式) 
 
new A() 
 
ClassDeclaration(class声明) 
 
class A {} 
 
ClassBody(class body) 
 
class A {}(类的内部)

常见的列举的差不多了。。。就先写到这吧。

generate(@babel/generator)

generate本来应该是第三步,为什么将第三步放到这里呢?因为他比较简单,而且当我们使用traverse时,需要用到它。在这里我们简单的把一段code转换为ast,再转换为code:

先安装好依赖。这一点以后不再赘述

yarn add @babel/parser @babel/generator 
const parser = require('@babel/parser') 
const generate = require('@babel/generator').default 
 
const code = `function mirror(something) { 
  return something 
}` 
const ast = parser.parse(code, { 
  sourceType: 'module', 
}) 
const transformedCode = generate(ast).code 
console.log(transformedCode)
  • 结果:
function mirror(something) { 
  return something; 
}

这就是generator的基本用法,详细参照IT虾米网

transform(@babel/traverse,@babel/types,@babel/template)

到了最为关键的transform步骤了,这里的主角是@babel/traverse,@babel/types和@babel/template是辅助工具。我们首先来谈一下visitor这个概念。

visitor

  1. visitor是什么
访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。

假如你这样写了一个visitor传递给babel:

const visitor = { 
  Identifier () { 
    enter () { 
      console.log('Hello Identifier!') 
    }, 
    exit () { 
      console.log('Bye Identifier!') 
    } 
  } 
}

那么babel会使用他的递归遍历器去遍历整棵ast,在进入和退出Identifier节点时,会执行我们定义的函数。

2.在一般情况下exit较少使用,所以可以简写成:

const visitor = { 
  Identifier () { 
    console.log('Hello Identifier!') 
  } 
}

3.如有必要,你还可以把方法名用|分割成a节点类型|b节点类型形式的字符串,把同一个函数应用到多种访问节点。

const visitor = { 
  'FunctionExpression|ArrowFunctionExpression' () { 
    console.log('A function expression or a arrow function expression!') 
  } 
}

好了,现在以上边的mirror函数为例,来动手写一个traverse的简单示例吧:

const parser = require('@babel/parser') 
const traverse = require('@babel/traverse').default 
 
const code = `function mirror(something) { 
  return something 
}` 
const ast = parser.parse(code, { 
  sourceType: 'module', 
}) 
const visitor = { 
  Identifier (path) { 
    console.log(path.node.name) 
  } 
} 
traverse(ast, visitor)
  • 结果:mirror,something,something

与你的预估是否一致呢?如果一致,那我们可以继续往下。此处你可能提出疑问:这个path是什么?

path

可以简单地认为path是对当前访问的node的一层包装。例如使用path.node可以访问到当前的节点,使用path.parent可以访问到父节点,这里列出了path所包含的内容(尚未列出path中所包含的一些方法)。

{ 
  "parent": {...}, 
  "node": {...}, 
  "hub": {...}, 
  "contexts": [], 
  "data": {}, 
  "shouldSkip": false, 
  "shouldStop": false, 
  "removed": false, 
  "state": null, 
  "opts": null, 
  "skipKeys": null, 
  "parentPath": null, 
  "context": null, 
  "container": null, 
  "listKey": null, 
  "inList": false, 
  "parentKey": null, 
  "key": null, 
  "scope": null, 
  "type": null, 
  "typeAnnotation": null 
}
当你有一个 Identifier() 成员方法的访问者时,你实际上是在访问路径而非节点。 通过这种方式,你操作的就是节点的响应式表示(译注:即路径)而非节点本身。 babel handbook

path中还提供了一系列的工具函数,例如traverse(在当前path下执行递归),remove(删除当前节点),replaceWith(替换当前节点)等等。

解释完了path之后,我们试着真正的来转换一下代码吧,在这里使用了@babel/generator来将ast转换为code

const parser = require('@babel/parser') 
const traverse = require('@babel/traverse').default 
const generate = require('@babel/generator').default 
 
const code = `function mirror(something) { 
  return something 
}` 
const ast = parser.parse(code, { 
  sourceType: 'module', 
}) 
const visitor = { 
  Identifier (path) { 
    path.node.name = path.node.name.split('').reverse().join('') 
  } 
} 
traverse(ast, visitor) 
const transformedCode = generate(ast).code 
console.log(transformedCode)
  • 结果:
function rorrim(gnihtemos) { 
  return gnihtemos; 
}

这段代码应该不难理解,就是将所有的变量做了个字符串翻转。是不是事情已经变得有趣起来了?

@babel/types

Babel Types模块是一个用于 AST 节点的 Lodash 式工具库(译注:Lodash 是一个 JavaScript 函数工具库,提供了基于函数式编程风格的众多工具函数), 它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。(依然是handbook原话)

展示一下最常用的使用方式,用来判断节点的类型

const parser = require('@babel/parser') 
const traverse = require('@babel/traverse').default 
const t = require('@babel/types') 
 
const code = `function mirror(something) { 
  return something 
}` 
const ast = parser.parse(code, { 
  sourceType: 'module', 
}) 
const visitor = { 
  enter(path) { 
    if (t.isIdentifier(path.node)) { 
      console.log('Identifier!') 
    } 
  } 
} 
traverse(ast, visitor)
  • 结果:Identifier! Identifier! Identifier!

@babel/types还可以用来生成节点,结合上边的知识,我们试着改动mirror函数的返回值

const parser = require('@babel/parser') 
const traverse = require('@babel/traverse').default 
const generate = require('@babel/generator').default 
const t = require('@babel/types') 
 
const code = `function mirror(something) { 
  return something 
}` 
const ast = parser.parse(code, { 
  sourceType: 'module', 
}) 
const strNode = t.stringLiteral('mirror') 
const visitor = { 
  ReturnStatement (path) { 
    path.traverse({ 
      Identifier(cpath){ 
        cpath.replaceWith(strNode) 
      } 
    }) 
  } 
} 
traverse(ast, visitor) 
const transformedCode = generate(ast).code 
console.log(transformedCode)
  • 结果:
function mirror(something) { 
  return "mirror"; 
}

在这里我们用到了t.stringLiteral('mirror')去创建一个字符串字面量节点,然后递归遍历ReturnStatement下的Identifier,并将其替换成我们所创建的字符串字面量节点(注意此处我们已经开始使用了一些path下的公共方法)。

@babel/template

使用@babel/type创建一些简单节点会很容易,但是如果是大段代码的话就会变得困难了,这个时候我们可以使用@babel/template。下面写了一个简单示例,为mirror函数内部写了一些逻辑判断。

const parser = require('@babel/parser') 
const traverse = require('@babel/traverse').default 
const generate = require('@babel/generator').default 
const template = require('@babel/template').default 
const t = require('@babel/types') 
 
const code = `function mirror(something) { 
  return something 
}` 
const ast = parser.parse(code, { 
  sourceType: 'module', 
}) 
const visitor = { 
  FunctionDeclaration(path) { 
    // 在这里声明了一个模板,比用@babel/types去生成方便很多 
    const temp = template(` 
      if(something) { 
        NORMAL_RETURN 
      } else { 
        return 'nothing' 
      } 
    `) 
    const returnNode = path.node.body.body[0] 
    const tempAst = temp({ 
      NORMAL_RETURN: returnNode 
    }) 
    path.node.body.body[0] = tempAst 
  } 
} 
traverse(ast, visitor) 
const transformedCode = generate(ast).code 
console.log(transformedCode)
  • 结果:
function mirror(something) { 
  if (something) { 
    return something; 
  } else { 
    return 'nothing'; 
  } 
}

完美!以上,babel基本的工具使用方式就介绍的差不多了,下边步入正题:尝试写一个babel插件。

写一个babel插件

其实到这里,编写一个babel插件已经非常简单了,我们尝试直接将上边的代码移植成为一个babel插件

module.exports = function (babel) { 
  const { 
    types: t, 
    template 
  } = babel 
  const visitor = { 
    FunctionDeclaration(path) { 
      const temp = template(` 
        if(something) { 
          NORMAL_RETURN 
        } else { 
          return 'nothing' 
        } 
      `) 
      const returnNode = path.node.body.body[0] 
      const tempAst = temp({ 
        NORMAL_RETURN: returnNode 
      }) 
      path.node.body.body[0] = tempAst 
    } 
  } 
  return { 
    name: 'my-plugin', 
    visitor 
  } 
}

babel插件暴露了一个函数,函数的传参是babel,你可以使用解构赋值获取到types,template这些工具。函数返回值中包含一个name和一个visitor,name是插件的名称,visitor就是我们上边多次编写的visitor。

你可能注意到了一些babel插件是可以传参的,那我们如何在babel插件中接收参数呢

module.exports = function (babel) { 
  const { 
    types: t, 
    template 
  } = babel 
  const visitor = { 
    FunctionDeclaration(path, state) { 
      const temp = template(` 
        if(something) { 
          NORMAL_RETURN 
        } else { 
          return '${state.opts.whenFalsy}' 
        } 
      `) 
      const returnNode = path.node.body.body[0] 
      const tempAst = temp({ 
        NORMAL_RETURN: returnNode 
      }) 
      path.node.body.body[0] = tempAst 
    } 
  } 
  return { 
    name: 'my-plugin', 
    visitor 
  } 
}

在上边的例子中我们看到在visitor中可以传入第二个参数state,在这个state中,使用state.opts[配置名]就可访问到用户所传递的对应配置名的值

如何测试你所编写的babel插件是可以使用的呢?引用你所编写的插件并测试一下:

const babel = require("@babel/core") 
 
const code = `function mirror(something) { 
  return something 
}` 
const res = babel.transformSync(code, { 
  plugins: [ 
    [require('你编写的插件地址'), { 
      whenFalsy: 'Nothing really.' 
    }] 
  ] 
}) 
 
console.log(res.code)
  • 结果:
function mirror(something) { 
  if (something) { 
    return something; 
  } else { 
    return 'Nothing really.'; 
  } 
}

以上,我们基本对babel的原理有了一个基本的认识,并且可以自己写出一个babel插件了。至于如何将babel的威力发挥在日常的工作中呢?就需要各位去自行探索了。

 


评论关闭
IT序号网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!