Babel 插件开发手册(官方)

这篇文档涵盖了如何创建 Babel 插件等方面的内容。

这本手册提供了多种语言的版本,查看 自述文件 里的完整列表。

目录

介绍

Babel 是一个通用的多功能的 JavaScript 编译器。此外它还拥有众多模块可用于不同形式的静态分析。

静态分析是在不需要执行代码的前提下对代码进行分析的处理过程 (执行代码的同时进行代码分析即是动态分析)。 静态分析的目的是多种多样的, 它可用于语法检查,编译,代码高亮,代码转换,优化,压缩等等场景。

你可以使用 Babel 创建多种类型的工具来帮助你更有效率并且写出更好的程序。

在 Twitter 上关注 @thejameskyle,第一时间获取更新。


基础

Babel 是 JavaScript 编译器,更确切地说是源码到源码的编译器,通常也叫做“转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,然后返回给你新生成的代码。

抽象语法树(ASTs)

这个处理过程中的每一步都涉及到创建或是操作抽象语法树,亦称 AST。

Babel 使用一个基于 ESTree 并修改过的 AST,它的内核说明文档可以在[这里](https://github. com/babel/babel/blob/master/doc/ast/spec. md)找到。.

function square(n) {
  return n * n;
}
复制代码

AST Explorer 可以让你对 AST 节点有一个更好的感性认识。 这里是上述代码的一个示例链接。

这个程序可以被表示成如下的一棵树:

- FunctionDeclaration:
  - id:
    - Identifier:
      - name: square
  - params [1]
    - Identifier
      - name: n
  - body:
    - BlockStatement
      - body [1]
        - ReturnStatement
          - argument
            - BinaryExpression
              - operator: *
              - left
                - Identifier
                  - name: n
              - right
                - Identifier
                  - name: n
复制代码

或是如下所示的 JavaScript Object(对象):

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}
复制代码

你会留意到 AST 的每一层都拥有相同的结构:

{
  type: "FunctionDeclaration",
  id: {...},
  params: [...],
  body: {...}
}
复制代码
{
  type: "Identifier",
  name: ...
}
复制代码
{
  type: "BinaryExpression",
  operator: ...,
  left: {...},
  right: {...}
}
复制代码

注意:出于简化的目的移除了某些属性

这样的每一层结构也被叫做 节点(Node)。 一个 AST 可以由单一的节点或是成百上千个节点构成。 它们组合在一起可以描述用于静态分析的程序语法。

每一个节点都有如下所示的接口(Interface):

interface Node {
  type: string;
}
复制代码

字符串形式的 type 字段表示节点的类型(如: "FunctionDeclaration""Identifier",或 "BinaryExpression")。 每一种类型的节点定义了一些附加属性用来进一步描述该节点类型。

Babel 还为每个节点额外生成了一些属性,用于描述该节点在原始代码中的位置。

{
  type: ...,
  start: 0,
  end: 38,
  loc: {
    start: {
      line: 1,
      column: 0
    },
    end: {
      line: 3,
      column: 1
    }
  },
  ...
}
复制代码

每一个节点都会有 startendloc 这几个属性。

Babel 的处理步骤

Babel 的三个主要处理步骤分别是: 解析(parse)转换(transform)生成(generate)。.

解析

解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:**词法分析(Lexical Analysis) **语法分析(Syntactic Analysis)。.

词法分析

词法分析阶段把字符串形式的代码转换为 令牌(tokens) 流。.

你可以把令牌看作是一个扁平的语法片段数组:

n * n;
复制代码
[
  { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
  { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
  { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
  ...
]
复制代码

每一个 type 有一组属性来描述该令牌:

{
  type: {
    label: 'name',
    keyword: undefined,
    beforeExpr: false,
    startsExpr: true,
    rightAssociative: false,
    isLoop: false,
    isAssign: false,
    prefix: false,
    postfix: false,
    binop: null,
    updateContext: null
  },
  ...
}
复制代码

和 AST 节点一样它们也有 startendloc 属性。.

语法分析

语法分析阶段会把一个令牌流转换成 AST 的形式。 这个阶段会使用令牌中的信息把它们转换成一个 AST 的表述结构,这样更易于后续的操作。

转换

转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel 或是其他编译器中最复杂的过程 同时也是插件将要介入工作的部分,这将是本手册的主要内容, 因此让我们慢慢来。

生成

代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。.

代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。

遍历

想要转换 AST 你需要进行递归的树形遍历

比方说我们有一个 FunctionDeclaration 类型。它有几个属性:idparams,和 body,每一个都有一些内嵌节点。

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}
复制代码

于是我们从 FunctionDeclaration 开始并且我们知道它的内部属性(即:idparamsbody),所以我们依次访问每一个属性及它们的子节点。

接着我们来到 id,它是一个 IdentifierIdentifier 没有任何子节点属性,所以我们继续。

之后是 params,由于它是一个数组节点所以我们访问其中的每一个,它们都是 Identifier 类型的单一节点,然后我们继续。

此时我们来到了 body,这是一个 BlockStatement 并且也有一个 body节点,而且也是一个数组节点,我们继续访问其中的每一个。

这里唯一的一个属性是 ReturnStatement 节点,它有一个 argument,我们访问 argument 就找到了 BinaryExpression。.

BinaryExpression 有一个 operator,一个 left,和一个 right。 Operator 不是一个节点,它只是一个值因此我们不用继续向内遍历,我们只需要访问 leftright。.

Babel 的转换步骤全都是这样的遍历过程。

Visitors(访问者)

当我们谈及“进入”一个节点,实际上是说我们在访问它们, 之所以使用这样的术语是因为有一个访问者模式(visitor)的概念。.

访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。 这么说有些抽象所以让我们来看一个例子。

const MyVisitor = {
  Identifier() {
    console.log("Called!");
  }
};

// 你也可以先创建一个访问者对象,并在稍后给它添加方法。
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}
复制代码

注意Identifier() { ... }Identifier: { enter() { ... } } 的简写形式。.

这是一个简单的访问者,把它用于遍历中时,每当在树中遇见一个 Identifier 的时候会调用 Identifier() 方法。

所以在下面的代码中 Identifier() 方法会被调用四次(包括 square 在内,总共有四个 Identifier)。).

function square(n) {
  return n * n;
}
复制代码
path.traverse(MyVisitor);
Called!
Called!
Called!
Called!
复制代码

这些调用都发生在进入节点时,不过有时候我们也可以在退出时调用访问者方法。.

假设我们有一个树状结构:

- FunctionDeclaration
  - Identifier (id)
  - Identifier (params[0])
  - BlockStatement (body)
    - ReturnStatement (body)
      - BinaryExpression (argument)
        - Identifier (left)
        - Identifier (right)
复制代码

当我们向下遍历这颗树的每一个分支时我们最终会走到尽头,于是我们需要往上遍历回去从而获取到下一个节点。 向下遍历这棵树我们进入每个节点,向上遍历回去时我们退出每个节点。

让我们以上面那棵树为例子走一遍这个过程。

  • 进入 FunctionDeclaration
    • 进入 Identifier (id)
    • 走到尽头
    • 退出 Identifier (id)
    • 进入 Identifier (params[0])
    • 走到尽头
    • 退出 Identifier (params[0])
    • 进入 BlockStatement (body)
    • 进入 ReturnStatement (body)
      • 进入 BinaryExpression (argument)
      • 进入 Identifier (left)
        • 走到尽头
      • 退出 Identifier (left)
      • 进入 Identifier (right)
        • 走到尽头
      • 退出 Identifier (right)
      • 退出 BinaryExpression (argument)
    • 退出 ReturnStatement (body)
    • 退出 BlockStatement (body)
  • 退出 FunctionDeclaration

所以当创建访问者时你实际上有两次机会来访问一个节点。

const MyVisitor = {
  Identifier: {
    enter() {
      console.log("Entered!");
    },
    exit() {
      console.log("Exited!");
    }
  }
};
复制代码

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

flow-comments 插件中的例子如下:

const MyVisitor = {
  "ExportNamedDeclaration|Flow"(path) {}
};
复制代码

你也可以在访问者中使用别名(如babel-types定义).

例如,

Function is an alias for FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, ObjectMethod and ClassMethod.

const MyVisitor = {
  Function(path) {}
};
复制代码

Paths(路径)

AST 通常会有许多节点,那么节点直接如何相互关联呢? 我们可以使用一个可操作和访问的巨大可变对象表示节点之间的关联关系,或者也可以用Paths(路径)来简化这件事情。.

Path 是表示两个节点之间连接的对象。

例如,如果有下面这样一个节点及其子节点︰

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  ...
}
复制代码

将子节点 Identifier 表示为一个路径(Path)的话,看起来是这样的:

{
  "parent": {
    "type": "FunctionDeclaration",
    "id": {...},
    ....
  },
  "node": {
    "type": "Identifier",
    "name": "square"
  }
}
复制代码

同时它还包含关于该路径的其他元数据:

{
  "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,
  • 5
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值