关于babel:babel插件手册md语法修复版本

70次阅读

共计 28768 个字符,预计需要花费 72 分钟才能阅读完成。

Babel 的官网插件手册的中文翻译版存在 md 语法错误,本文是 md 语法整顿后的文档。当然,更加倡议你浏览英文原版,本文能够做一个疾速查阅文档。

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

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

目录

  • 介绍
  • 根底

    • 形象语法树(ASTs)
    • Babel 的解决步骤
    • 解析

      • 词法剖析
      • 语法分析
    • 转换
    • 生成
    • 遍历
    • Visitors(访问者)
    • Paths(门路)

      • Paths in Visitors(存在于访问者中的门路)
    • State(状态)
    • Scopes(作用域)

      • Bindings(绑定)
  • API

    • babylon
    • babel-traverse
    • babel-types
    • Definitions(定义)
    • Builders(构建器)
    • Validators(验证器)
    • Converters(变换器)
    • babel-generator
    • babel-template
  • 编写你的第一个 Babel 插件
  • 转换操作

    • 拜访
    • 获取子节点的 Path
    • 查看节点(Node)类型
    • 查看门路(Path)类型
    • 查看标识符(Identifier)是否被援用
    • 找到特定的父门路
    • 获取同级门路
    • 进行遍历
    • 解决
    • 替换一个节点
    • 用多节点替换单节点
    • 用字符串源码替换节点
    • 插入兄弟节点
    • 插入到容器(container)中
    • 删除节点
    • 替换父节点
    • 删除父节点
    • Scope(作用域)
    • 查看本地变量是否被绑定
    • 生成 UID
    • 晋升变量申明至父级作用域
    • 重命名绑定及其援用
  • 插件选项

    • 插件的筹备和收尾工作
    • 在插件中启用其余语法
  • 构建节点
  • 最佳实际

    • 尽量避免遍历形象语法树(AST)
    • 及时合并访问者对象
    • 能够手动查找就不要遍历
    • 优化嵌套的访问者对象
    • 注意嵌套构造
    • 单元测试

介绍

Babel 是一个通用的多功能的 JavaScript 编译器。此外它还领有泛滥模块可用于不同模式的动态剖析。

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

你能够应用 Babel 创立多种类型的工具来帮忙你更有效率并且写出更好的程序。

在 Twitter 上关注 @thejameskyle,第一工夫获取更新。


根底

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

形象语法树(ASTs)

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

Babel 应用一个基于 ESTree 并批改过的 AST,它的内核阐明文档能够在这里. 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,
  "scope": null,
  "type": null,
  "typeAnnotation": null
}

当然门路对象还蕴含增加、更新、挪动和删除节点无关的其余很多办法,稍后咱们再来看这些办法。

在某种意义上,门路是一个节点在树中的地位以及对于该节点各种信息的响应式 Reactive 示意。当你调用一个批改树的办法后,门路信息也会被更新。Babel 帮你治理这所有,从而使得节点操作简略,尽可能做到无状态。

Paths in Visitors(存在于访问者中的门路)

当你有一个 Identifier() 成员办法的访问者时,你实际上是在拜访门路而非节点。通过这种形式,你操作的就是节点的响应式示意(译注:即门路)而非节点自身。

const MyVisitor = {Identifier(path) {console.log("Visiting:" + path.node.name);
  }
};
a + b + c;
path.traverse(MyVisitor);
Visiting: a
Visiting: b
Visiting: c

State(状态)

状态是形象语法树 AST 转换的敌人,状态治理会一直牵扯你的精力,而且简直所有你对状态的假如,总是会有一些未思考到的语法最终证实你的假如是谬误的。

思考下列代码:

function square(n) {return n * n;}

让咱们写一个把 n 重命名为 x 的访问者的疾速实现.

let paramName;

const MyVisitor = {FunctionDeclaration(path) {const param = path.node.params[0];
    paramName = param.name;
    param.name = "x";
  },

  Identifier(path) {if (path.node.name === paramName) {path.node.name = "x";}
  }
};

对下面的例子代码这段访问者代码兴许能工作,但它很容易被突破:

function square(n) {return n * n;}
n;

更好的解决形式是应用递归,上面让咱们来像克里斯托佛·诺兰的电影盗梦空间那样来把一个访问者放进另外一个访问者外面。

const updateParamNameVisitor = {Identifier(path) {if (path.node.name === this.paramName) {path.node.name = "x";}
  }
};

const MyVisitor = {FunctionDeclaration(path) {const param = path.node.params[0];
    const paramName = param.name;
    param.name = "x";

    path.traverse(updateParamNameVisitor, { paramName});
  }
};

path.traverse(MyVisitor);

当然,这只是一个刻意编写的例子,不过它演示了如何从访问者中打消全局状态。

Scopes(作用域)

接下来让咱们介绍 作用域(scope))的概念。JavaScript 反对词法作用域 #Lexical_scoping_vs._dynamic_scoping),在树状嵌套构造中代码块创立出新的作用域。

// global scope

function scopeOne() {
  // scope 1

  function scopeTwo() {// scope 2}
}

在 JavaScript 中,每当你创立了一个援用,不论是通过变量(variable)、函数(function)、类型(class)、参数(params)、模块导入(import)还是标签(label)等,它都属于以后作用域。

var global = "I am in the global scope";

function scopeOne() {var one = "I am in the scope created by `scopeOne()`";

  function scopeTwo() {var two = "I am in the scope created by `scopeTwo()`";
  }
}

更深的外部作用域代码能够应用外层作用域中的援用。

function scopeOne() {var one = "I am in the scope created by `scopeOne()`";

  function scopeTwo() {one = "I am updating the reference in `scopeOne` inside `scopeTwo`";}
}

内层作用域也能够创立和外层作用域同名的援用。

function scopeOne() {var one = "I am in the scope created by `scopeOne()`";

  function scopeTwo() {var one = "I am creating a new `one` but leaving reference in `scopeOne()` alone.";
  }
}

当编写一个转换时,必须小心作用域。咱们得确保在扭转代码的各个局部时不会毁坏曾经存在的代码。

咱们在增加一个新的援用时须要确保新减少的援用名字和已有的所有援用不抵触。或者咱们仅仅想找出应用一个变量的所有援用,咱们只想在给定的作用域(Scope)中找出这些援用。

作用域能够被示意为如下模式:

{
  path: path,
  block: path.node,
  parentBlock: path.parent,
  parent: parentScope,
  bindings: [...]
}

当你创立一个新的作用域时,须要给出它的门路和父作用域,之后在遍历过程中它会在该作用域内收集所有的援用(“绑定”)。

一旦援用收集结束,你就能够在作用域(Scopes)上应用各种办法,稍后咱们会理解这些办法。

Bindings(绑定)

所有援用属于特定的作用域,援用和作用域的这种关系被称作:绑定(binding)。.

function scopeOnce() {
  var ref = "This is a binding";

  ref; // This is a reference to a binding

  function scopeTwo() {ref; // This is a reference to a binding from a lower scope}
}

单个绑定看起来像这样︰

Text for Translation
{
  identifier: node,
  scope: scope,
  path: path,
  kind: 'var',

  referenced: true,
  references: 3,
  referencePaths: [path, path, path],

  constant: false,
  constantViolations: [path]
}

有了这些信息你就能够查找一个绑定的所有援用,并且晓得这是什么类型的绑定(参数,定义等等),查找它所属的作用域,或者拷贝它的标识符。你甚至能够晓得它是不是常量,如果不是,那么是哪个门路批改了它。

在很多状况下,晓得一个绑定是否是常量十分有用,最有用的一种情景就是代码压缩时。

function scopeOne() {
  var ref1 = "This is a constant binding";

  becauseNothingEverChangesTheValueOf(ref1);

  function scopeTwo() {
    var ref2 = "This is *not* a constant binding";
    ref2 = "Because this changes the value";
  }
}

API

Babel 实际上是一组模块的汇合。本节咱们将摸索一些次要的模块,解释它们是做什么的以及如何应用它们。

留神:本节内容不是具体的 API 文档的替代品,正式的 API 文档将很快提供进去。

[babylon](https://github.com/babel/babylon)

Babylon 是 Babel 的解析器。最后是 从 Acorn 我的项目 fork 进去的。Acorn 十分快,易于应用,并且针对非标准个性(以及那些将来的规范个性) 设计了一个基于插件的架构。

首先,让咱们装置它。

$ npm install --save babylon

先从解析一个代码字符串开始:

import * as babylon from "babylon";

const code = `function square(n) {return n * n;}`;

babylon.parse(code);
// Node {
//   type: "File",
//   start: 0,
//   end: 38,
//   loc: SourceLocation {...},
//   program: Node {...},
//   comments: [],
//   tokens: [...]
// }

咱们还能像上面这样传递选项给 parse()办法:

babylon.parse(code, {
  sourceType: "module", // default: "script"
  plugins: ["jsx"] // default: []});

sourceType 能够是 "module" 或者 "script",它示意 Babylon 应该用哪种模式来解析。"module" 将会在严格模式下解析并且容许模块定义,"script" 则不会。

留神: sourceType 的默认值是 "script" 并且在发现 importexport 时产生谬误。应用 scourceType: "module" 来防止这些谬误。

因为 Babylon 应用了基于插件的架构,因而有一个 plugins 选项能够开关内置的插件。留神 Babylon 尚未对外部插件凋谢此 API 接口,不排除将来会凋谢此 API。

要查看残缺的插件列表,请参见 Babylon README 文件。.

[babel-traverse](https://github.com/babel/babel/tree/master/packages/babel-traverse)

Babel Traverse(遍历)模块保护了整棵树的状态,并且负责替换、移除和增加节点。

运行以下命令装置:

$ npm install --save babel-traverse

咱们能够和 Babylon 一起应用来遍历和更新节点:

import * as babylon from "babylon";
import traverse from "babel-traverse";

const code = `function square(n) {return n * n;}`;

const ast = babylon.parse(code);

traverse(ast, {enter(path) {
    if (
      path.node.type === "Identifier" &&
      path.node.name === "n"
    ) {path.node.name = "x";}
  }
});

[babel-types](https://github.com/babel/babel/tree/master/packages/babel-types)

Babel Types 模块是一个用于 AST 节点的 Lodash 式工具库(译注:Lodash 是一个 JavaScript 函数工具库,提供了基于函数式编程格调的泛滥工具函数),它蕴含了结构、验证以及变换 AST 节点的办法。该工具库蕴含考虑周到的工具办法,对编写解决 AST 逻辑十分有用。

能够运行以下命令来装置它:

$ npm install --save babel-types

而后按如下所示来应用:

import traverse from "babel-traverse";
import * as t from "babel-types";

traverse(ast, {enter(path) {if (t.isIdentifier(path.node, { name: "n"})) {path.node.name = "x";}
  }
});

Definitions(定义)

Babel Types 模块领有每一个繁多类型节点的定义,包含节点蕴含哪些属性,什么是非法值,如何构建节点、遍历节点,以及节点的别名等信息。

繁多节点类型的定义模式如下:

defineType("BinaryExpression", {builder: ["operator", "left", "right"],
  fields: {
    operator: {validate: assertValueType("string")
    },
    left: {validate: assertNodeType("Expression")
    },
    right: {validate: assertNodeType("Expression")
    }
  },
  visitor: ["left", "right"],
  aliases: ["Binary", "Expression"]
});

Builders(构建器)

你会留神到下面的 BinaryExpression 定义有一个 builder 字段。.

builder: ["operator", "left", "right"]

这是因为每一个节点类型都有结构器办法 builder,按相似上面的形式应用:

t.binaryExpression("*", t.identifier("a"), t.identifier("b"));

能够创立如下所示的 AST:

{
  type: "BinaryExpression",
  operator: "*",
  left: {
    type: "Identifier",
    name: "a"
  },
  right: {
    type: "Identifier",
    name: "b"
  }
}

当打印进去之后是这样的:

a * b

结构器还会验证本身创立的节点,并在谬误应用的情景下会抛出描述性谬误,这就引出了下一个办法类型。

Validators(验证器)

BinaryExpression 的定义还蕴含了节点的字段 fields 信息,以及如何验证这些字段。

fields: {
  operator: {validate: assertValueType("string")
  },
  left: {validate: assertNodeType("Expression")
  },
  right: {validate: assertNodeType("Expression")
  }
}

能够创立两种验证办法。第一种是 isX。.

t.isBinaryExpression(maybeBinaryExpressionNode);

这个测试用来确保节点是一个二进制表达式,另外你也能够传入第二个参数来确保节点蕴含特定的属性和值。

t.isBinaryExpression(maybeBinaryExpressionNode, { operator: "*"});

这些办法还有一种断言式的版本,会抛出异样而不是返回 truefalse。.

t.assertBinaryExpression(maybeBinaryExpressionNode);
t.assertBinaryExpression(maybeBinaryExpressionNode, { operator: "*"});
// Error: Expected type "BinaryExpression" with option {"operator": "*"}

Converters(变换器)

[WIP]

[babel-generator](https://github.com/babel/babel/tree/master/packages/babel-generator)

Babel Generator 模块是 Babel 的代码生成器,它读取 AST 并将其转换为代码和源码映射(sourcemaps)。

运行以下命令来装置它:

$ npm install --save babel-generator

而后按如下形式应用:

import * as babylon from "babylon";
import generate from "babel-generator";

const code = `function square(n) {return n * n;}`;

const ast = babylon.parse(code);

generate(ast, {}, code);
// {
//   code: "...",
//   map: "..."
// }

你也能够给 generate() 办法传递选项。.

generate(ast, {
  retainLines: false,
  compact: "auto",
  concise: false,
  quotes: "double",
  // ...
}, code);

[babel-template](https://github.com/babel/babel/tree/master/packages/babel-template)

babel-template 是另一个尽管很小但却十分有用的模块。它能让你编写字符串模式且带有占位符的代码来代替手动编码,尤其是生成的大规模 AST 的时候。在计算机科学中,这种能力被称为准援用(quasiquotes)。

$ npm install --save babel-template
import template from "babel-template";
import generate from "babel-generator";
import * as t from "babel-types";

const buildRequire = template(`
  var IMPORT_NAME = require(SOURCE);
`);

const ast = buildRequire({IMPORT_NAME: t.identifier("myModule"),
  SOURCE: t.stringLiteral("my-module")
});

console.log(generate(ast).code);
var myModule = require("my-module");

编写你的第一个 Babel 插件

当初你曾经相熟了 Babel 的所有基础知识了,让咱们把这些常识和插件的 API 交融在一起来编写第一个 Babel 插件吧。

先从一个接管了以后 babel 对象作为参数的 [function](https://github.com/babel/babel/tree/master/packages/babel-core) 开始。

export default function(babel) {// plugin contents}

因为你将会常常这样应用,所以间接取出 babel.types 会更不便:(译注:这是 ES2015 语法中的对象解构,即 Destructuring)

export default function({types: t}) {// plugin contents}

接着返回一个对象,其 visitor 属性是这个插件的次要访问者。

export default function({types: t}) {
  return {
    visitor: {// visitor contents}
  };
};

Visitor 中的每个函数接管 2 个参数:pathstate

export default function({types: t}) {
  return {
    visitor: {Identifier(path, state) {},
      ASTNodeTypeHere(path, state) {}}
  };
};

让咱们疾速编写一个可用的插件来展现一下它是如何工作的。上面是咱们的源代码:

foo === bar;

其 AST 模式如下:

{
  type: "BinaryExpression",
  operator: "===",
  left: {
    type: "Identifier",
    name: "foo"
  },
  right: {
    type: "Identifier",
    name: "bar"
  }
}

咱们从增加 BinaryExpression 访问者办法开始:

export default function({types: t}) {
  return {
    visitor: {BinaryExpression(path) {// ...}
    }
  };
}

而后咱们更确切一些,只关注哪些应用了 ===BinaryExpression

visitor: {BinaryExpression(path) {if (path.node.operator !== "===") {return;}

    // ...
  }
}

当初咱们用新的标识符来替换 left 属性:

BinaryExpression(path) {if (path.node.operator !== "===") {return;}

  path.node.left = t.identifier("sebmck");
  // ...
}

于是如果咱们运行这个插件咱们会失去:

sebmck === bar;

当初只须要替换 right 属性了。

BinaryExpression(path) {if (path.node.operator !== "===") {return;}

  path.node.left = t.identifier("sebmck");
  path.node.right = t.identifier("dork");
}

这就是咱们的最终后果了:

sebmck === dork;

完满!咱们的第一个 Babel 插件。


转换操作

拜访

获取子节点的 Path

为了失去一个 AST 节点的属性值,咱们个别先拜访到该节点,而后利用 path.node.property 办法即可。

// the BinaryExpression AST node has properties: `left`, `right`, `operator`
BinaryExpression(path) {
  path.node.left;
  path.node.right;
  path.node.operator;
}

如果你想拜访到该属性外部的 path,应用 path 对象的get 办法,传递该属性的字符串模式作为参数。

BinaryExpression(path) {path.get('left');
}
Program(path) {path.get('body.0');
}

查看节点的类型

如果你想查看节点的类型,最好的形式是:

BinaryExpression(path) {if (t.isIdentifier(path.node.left)) {// ...}
}

你同样能够对节点的属性们做浅层查看:

BinaryExpression(path) {if (t.isIdentifier(path.node.left, { name: "n"})) {// ...}
}

性能上等价于:

BinaryExpression(path) {
  if (
    path.node.left != null &&
    path.node.left.type === "Identifier" &&
    path.node.left.name === "n"
  ) {// ...}
}

查看门路(Path)类型

一个门路具备雷同的办法查看节点的类型:

BinaryExpression(path) {if (path.get('left').isIdentifier({name: "n"})) {// ...}
}

就相当于:

BinaryExpression(path) {if (t.isIdentifier(path.node.left, { name: "n"})) {// ...}
}

查看标识符(Identifier)是否被援用

Identifier(path) {if (path.isReferencedIdentifier()) {// ...}
}

或者:

Identifier(path) {if (t.isReferenced(path.node, path.parent)) {// ...}
}

找到特定的父门路

有时你须要从一个门路向上遍历语法树,直到满足相应的条件。

对于每一个父门路调用 callback 并将其 NodePath 当作参数,当 callback 返回真值时,则将其 NodePath 返回。.

path.findParent((path) => path.isObjectExpression());

如果也须要遍历以后节点:

path.find((path) => path.isObjectExpression());

查找最靠近的父函数或程序:

path.getFunctionParent();

向上遍历语法树,直到找到在列表中的父节点门路

path.getStatementParent();

获取同级门路

如果一个门路是在一个 Function/Program中的列表外面,它就有同级节点。

  • 应用 path.inList 来判断门路是否有同级节点,
  • 应用 path.getSibling(index) 来取得同级门路,
  • 应用 path.key获取门路所在容器的索引,
  • 应用 path.container获取门路的容器(蕴含所有同级节点的数组)
  • 应用 path.listKey获取容器的 key

这些 API 用于 babel-minify. 中应用的 transform-merge-sibling-variables 插件.

var a = 1; // pathA, path.key = 0
var b = 2; // pathB, path.key = 1
var c = 3; // pathC, path.key = 2
export default function({types: t}) {
  return {
    visitor: {VariableDeclaration(path) {
        // if the current path is pathA
        path.inList // true
        path.listKey // "body"
        path.key // 0
        path.getSibling(0) // pathA
        path.getSibling(path.key + 1) // pathB
        path.container // [pathA, pathB, pathC]
      }
    }
  };
}

进行遍历

如果你的插件须要在某种状况下不运行,最简略的做法是尽早写回。

BinaryExpression(path) {if (path.node.operator !== '**') return;
}

如果您在顶级门路中进行子遍历,则能够应用 2 个提供的 API 办法:

path.skip() skips traversing the children of the current path. path.stop() stops traversal entirely.

outerPath.traverse({Function(innerPath) {innerPath.skip(); // if checking the children is irrelevant
  },
  ReferencedIdentifier(innerPath, state) {
    state.iife = true;
    innerPath.stop(); // if you want to save some state and then stop traversal, or deopt}
});

解决

替换一个节点

BinaryExpression(path) {
  path.replaceWith(t.binaryExpression("**", path.node.left, t.numberLiteral(2))
  );
}
  function square(n) {
-   return n * n;
+   return n ** 2;
  }

用多节点替换单节点

ReturnStatement(path) {
  path.replaceWithMultiple([t.expressionStatement(t.stringLiteral("Is this the real life?")),
    t.expressionStatement(t.stringLiteral("Is this just fantasy?")),
    t.expressionStatement(t.stringLiteral("(Enjoy singing the rest of the song in your head)")),
  ]);
}
  function square(n) {
-   return n * n;
+   "Is this the real life?";
+   "Is this just fantasy?";
+   "(Enjoy singing the rest of the song in your head)";
  }

留神:当用多个节点替换一个表达式时,它们必须是   申明。这是因为 Babel 在更换节点时宽泛应用启发式算法,这意味着您能够做一些十分疯狂的转换,否则将会十分简短。

用字符串源码替换节点

FunctionDeclaration(path) {path.replaceWithSourceString(`function add(a, b) {return a + b;}`);
}
- function square(n) {
-   return n * n;
+ function add(a, b) {+   return a + b;}

留神:不倡议应用这个 API,除非您正在解决动静的源码字符串,否则在访问者内部解析代码更有效率。

插入兄弟节点

FunctionDeclaration(path) {path.insertBefore(t.expressionStatement(t.stringLiteral("Because I'm easy come, easy go.")));
  path.insertAfter(t.expressionStatement(t.stringLiteral("A little high, little low.")));
}
+ "Because I'm easy come, easy go.";
  function square(n) {return n * n;}
+ "A little high, little low.";

留神: 这里同样应该应用申明或者一个申明数组。这个应用了在用多个节点替换一个节点中提到的雷同的启发式算法。

插入到容器(container)中

如果您想要在 AST 节点属性中插入一个像 body 那样的数组。
它与 insertBefore/insertAfter 相似, 但您必须指定 listKey(通常是 注释).

ClassMethod(path) {path.get('body').unshiftContainer('body', t.expressionStatement(t.stringLiteral('before')));
  path.get('body').pushContainer('body', t.expressionStatement(t.stringLiteral('after')));
}
 class A {constructor() {
+   "before"
    var a = 'middle';
+   "after"
  }
 }

删除一个节点

FunctionDeclaration(path) {path.remove();
}
- function square(n) {
-   return n * n;
- }

替换父节点

只需应用 parentPath:path.parentPath调用 replaceWith 即可

BinaryExpression(path) {
  path.parentPath.replaceWith(t.expressionStatement(t.stringLiteral("Anyway the wind blows, doesn't really matter to me, to me."))
  );
}
  function square(n) {
-   return n * n;
+   "Anyway the wind blows, doesn't really matter to me, to me.";
  }

删除父节点

BinaryExpression(path) {path.parentPath.remove();
}
  function square(n) {-   return n * n;}

Scope(作用域)

查看本地变量是否被绑定

FunctionDeclaration(path) {if (path.scope.hasBinding("n")) {// ...}
}

这将遍历范畴树并查看特定的绑定。

您也能够查看一个作用域是否有 本人的 绑定:

FunctionDeclaration(path) {if (path.scope.hasOwnBinding("n")) {// ...}
}

创立一个 UID

这将生成一个标识符,不会与任何本地定义的变量相冲突。

FunctionDeclaration(path) {path.scope.generateUidIdentifier("uid");
  // Node {type: "Identifier", name: "_uid"}
  path.scope.generateUidIdentifier("uid");
  // Node {type: "Identifier", name: "_uid2"}
}

晋升变量申明至父级作用域

有时你可能想要推送一个VariableDeclaration,这样你就能够调配给它。

FunctionDeclaration(path) {const id = path.scope.generateUidIdentifierBasedOnNode(path.node.id);
  path.remove();
  path.scope.parent.push({id, init: path.node});
}
- function square(n) {+ var _square = function square(n) {
    return n * n;
- }
+ };

重命名绑定及其援用

FunctionDeclaration(path) {path.scope.rename("n", "x");
}
- function square(n) {
-   return n * n;
+ function square(x) {+   return x * x;}

或者,您能够将绑定重命名为生成的惟一标识符:

FunctionDeclaration(path) {path.scope.rename("n");
}
- function square(n) {
-   return n * n;
+ function square(_n) {+   return _n * _n;}

插件选项

如果您想让您的用户自定义您的 Babel 插件的行为您能够承受用户能够指定的插件特定选项,如下所示:

{
  plugins: [
    ["my-plugin", {
      "option1": true,
      "option2": false
    }]
  ]
}

这些选项会通过 state 对象传递给插件访问者:

export default function({types: t}) {
  return {
    visitor: {FunctionDeclaration(path, state) {console.log(state.opts);
        // {option1: true, option2: false}
      }
    }
  }
}

这些选项是特定于插件的,您不能拜访其余插件中的选项。

插件的筹备和收尾工作

插件能够具备在插件之前或之后运行的函数。它们能够用于设置或清理 / 剖析目标。

export default function({types: t}) {
  return {pre(state) {this.cache = new Map();
    },
    visitor: {StringLiteral(path) {this.cache.set(path.node.value, 1);
      }
    },
    post(state) {console.log(this.cache);
    }
  };
}

在插件中启用其余语法

插件能够启用babylon plugins,以便用户不须要装置 / 启用它们。这能够避免解析谬误,而不会继承语法插件。

export default function({types: t}) {
  return {inherits: require("babel-plugin-syntax-jsx")
  };
}

抛出一个语法错误

如果您想用 babel-code-frame 和一个音讯抛出一个谬误:

export default function({types: t}) {
  return {
    visitor: {StringLiteral(path) {throw path.buildCodeFrameError("Error message here");
      }
    }
  };
}

该谬误看起来像:

file.js: Error message here
   7 |
   8 | let tips = [
>  9 |   "Click on any AST node with a'+'to expand it",
     |   ^
  10 |
  11 |   "Hovering over a node highlights the \
  12 |    corresponding part in the source code",

构建节点

编写转换时,通常须要构建一些要插入的节点进入 AST。如前所述,您能够应用 [babel-types](#babel-types) 包中的 builder 办法。

构建器的办法名称就是您想要的节点类型的名称,除了第一个字母小写。例如,如果您想建设一个 MemberExpression 您能够应用t.memberExpression(...)

这些构建器的参数由节点定义决定。有一些正在做的工作,以生成易于浏览的文件定义,但当初他们都能够在此处找到。

节点定义如下所示:

defineType("MemberExpression", {builder: ["object", "property", "computed"],
  visitor: ["object", "property"],
  aliases: ["Expression", "LVal"],
  fields: {
    object: {validate: assertNodeType("Expression")
    },
    property: {validate(node, key, val) {
        let expectedType = node.computed ? "Expression" : "Identifier";
        assertNodeType(expectedType)(node, key, val);
      }
    },
    computed: {default: false}
  }
});

在这里你能够看到对于这个特定节点类型的所有信息,包含如何构建它,遍历它,并验证它。

通过查看 builder 属性, 能够看到调用生成器办法所需的 3 个参数 (t.memberExpression).

builder: ["object", "property", "computed"],

请留神,有时在节点上能够定制的属性比 builder 数组蕴含的属性更多。这是为了避免生成器有太多的参数。在这些状况下,您须要手动设置属性。一个例子是[ClassMethod](https://github.com/babel/babel/blob/bbd14f88c4eea88fa584dd877759dd6b900bf35e/packages/babel-types/src/definitions/es2015.js#L238-L276).

// Example
// because the builder does not contain `async` as a property
var node = t.classMethod(
  "constructor",
  t.identifier("constructor"),
  params,
  body
)
// set it manually after creation
node.async = true;

You can see the validation for the builder arguments with the fields object.

fields: {
  object: {validate: assertNodeType("Expression")
  },
  property: {validate(node, key, val) {
      let expectedType = node.computed ? "Expression" : "Identifier";
      assertNodeType(expectedType)(node, key, val);
    }
  },
  computed: {default: false}
}

You can see that object needs to be an Expression, property either needs to be an Expression or an Identifier depending on if the member expression is computed or not and computed is simply a boolean that defaults to false.

So we can construct a MemberExpression by doing the following:

t.memberExpression(t.identifier('object'),
  t.identifier('property')
  // `computed` is optional
);

Which will result in:

object.property

However, we said that object needed to be an Expression so why is Identifier valid?

Well if we look at the definition of Identifier we can see that it has an aliases property which states that it is also an expression.

aliases: ["Expression", "LVal"],

So since MemberExpression is a type of Expression, we could set it as the object of another MemberExpression:

t.memberExpression(
  t.memberExpression(t.identifier('member'),
    t.identifier('expression')
  ),
  t.identifier('property')
)

Which will result in:

member.expression.property

It’s very unlikely that you will ever memorize the builder method signatures for every node type. So you should take some time and understand how they are generated from the node definitions.

You can find all of the actual definitions here and you can see them documented here


最佳实际

Create Helper Builders and Checkers

It’s pretty simple to extract certain checks (if a node is a certain type) into their own helper functions as well as extracting out helpers for specific node types.

function isAssignment(node) {return node && node.operator === opts.operator + "=";}

function buildAssignment(left, right) {return t.assignmentExpression("=", left, right);
}

尽量避免遍历形象语法树(AST)

Traversing the AST is expensive, and it’s easy to accidentally traverse the AST more than necessary. This could be thousands if not tens of thousands of extra operations.

Babel optimizes this as much as possible, merging visitors together if it can in order to do everything in a single traversal.

及时合并访问者对象

When writing visitors, it may be tempting to call path.traverse in multiple places where they are logically necessary.

path.traverse({Identifier(path) {// ...}
});

path.traverse({BinaryExpression(path) {// ...}
});

However, it is far better to write these as a single visitor that only gets run once. Otherwise you are traversing the same tree multiple times for no reason.

path.traverse({Identifier(path) {// ...},
  BinaryExpression(path) {// ...}
});

能够手动查找就不要遍历

It may also be tempting to call path.traverse when looking for a particular node type.

const nestedVisitor = {Identifier(path) {// ...}
};

const MyVisitor = {FunctionDeclaration(path) {path.get('params').traverse(nestedVisitor);
  }
};

However, if you are looking for something specific and shallow, there is a good chance you can manually lookup the nodes you need without performing a costly traversal.

const MyVisitor = {FunctionDeclaration(path) {path.node.params.forEach(function() {// ...});
  }
};

优化嵌套的访问者对象

当您嵌套访问者(visitor)时,把它们嵌套在您的代码中可能是有意义的。

const MyVisitor = {FunctionDeclaration(path) {
    path.traverse({Identifier(path) {// ...}
    });
  }
};

然而,每当调用 FunctionDeclaration() 时都会创立一个新的访问者对象。That can be costly, because Babel does some processing each time a new
visitor object is passed in (such as exploding keys containing multiple types,
performing validation, and adjusting the object structure). Because Babel stores
flags on visitor objects indicating that it’s already performed that processing,
it’s better to store the visitor in a variable and pass the same object each
time.

const nestedVisitor = {Identifier(path) {// ...}
};

const MyVisitor = {FunctionDeclaration(path) {path.traverse(nestedVisitor);
  }
};

如果您在嵌套的访问者中须要一些状态,像这样:

const MyVisitor = {FunctionDeclaration(path) {var exampleState = path.node.params[0].name;

    path.traverse({Identifier(path) {if (path.node.name === exampleState) {// ...}
      }
    });
  }
};

您能够将它作为状态传递给 traverse()</ 0> 办法,并有权拜访 <code>this 在访问者中。

const nestedVisitor = {Identifier(path) {if (path.node.name === this.exampleState) {// ...}
  }
};

const MyVisitor = {FunctionDeclaration(path) {var exampleState = path.node.params[0].name;
    path.traverse(nestedVisitor, { exampleState});
  }
};

注意嵌套构造

有时候在思考给定的转换时,可能会遗记给定的转换构造能够是嵌套的。

例如,设想一下,咱们想要查找 构造函数 ClassMethod Foo ClassDeclaration.

class Foo {constructor() {// ...}
}
const constructorVisitor = {ClassMethod(path) {if (path.node.name === 'constructor') {// ...}
  }
}

const MyVisitor = {ClassDeclaration(path) {if (path.node.id.name === 'Foo') {path.traverse(constructorVisitor);
    }
  }
}

咱们疏忽了类能够嵌套的事实,应用遍历的话,下面咱们也会失去一个嵌套的constructor

class Foo {constructor() {
    class Bar {constructor() {// ...}
    }
  }
}

单元测试

有几种次要的办法来测试 babel 插件:快照测试,AST 测试和执行测试。对于这个例子,咱们将应用jest,因为它反对盒外快照测试。咱们在这里创立的示例是托管在这个库。

首先咱们须要一个 babel 插件,咱们将把它放在 src/index.js 中。

<br />module.exports = function testPlugin(babel) {
  return {
    visitor: {Identifier(path) {if (path.node.name === 'foo') {path.node.name = 'bar';}
      }
    }
  };
};

快照测试

接下来,用 npm install --save-dev babel-core jest 装置咱们的依赖关系,
那么咱们能够开始写咱们的第一个测试:快照。快照测试容许咱们直观地查看咱们的 babel 插件的输入。咱们给它一个输出,通知它一个快照,并将其保留到一个文件。咱们查看快照到 git 中。这容许咱们来看看咱们什么时候影响了咱们任何试用例子测试的输入。它也给出了应用差别在拉申请的时候。当然,您能够用任何测试框架来做到这一点,然而要更新一下快照就像 jest -u 一样简略。

// src/__tests__/index-test.js
const babel = require('babel-core');
const plugin = require('../');

var example = `
var foo = 1;
if (foo) console.log(foo);
`;

it('works', () => {const {code} = babel.transform(example, {plugins: [plugin]});
  expect(code).toMatchSnapshot();});

这给了咱们一个快照文件在src/__tests__/__snapshots__/index-test.js.snap

exports[`test works 1`] = `
"
var bar = 1;
if (bar) console.log(bar);"
`;

如果咱们在插件中将“bar”更改为“baz”并再次运行,则能够失去以下后果:

接管到的值与存储的快照 1 不匹配。- Snapshot
    + Received

    @@ -1,3 +1,3 @@
     "
    -var bar = 1;
    -if (bar) console.log(bar);"
    +var baz = 1;
    +if (baz) console.log(baz);"

咱们看到咱们对插件代码的扭转如何影响了咱们插件的输入 如果输入看起来不错,咱们能够运行 jest -u 来更新快照。

AST 测试

除了快照测试外,咱们还能够手动查看 AST。这是一个简略然而软弱的例子。对于更多波及的状况,您可能心愿利用 Babel- 遍历。它容许您用 visitor 键指定一个对象,就像您应用插件自身。

it('contains baz', () => {const {ast} = babel.transform(example, {plugins: [plugin]});
  const program = ast.program;
  const declaration = program.body[0].declarations[0];
  assert.equal(declaration.id.name, 'baz');
  // or babelTraverse(program, {visitor: ...})
});

Exec Tests

在这里,咱们将转换代码,而后评估它的行为是否正确。请留神,咱们在测试中没有应用assert。这确保如果咱们的插件做了奇怪的操作,如意外删除断言线,但测试依然失败。

it('foo is an alias to baz', () => {
  var input = `
    var foo = 1;
    // test that foo was renamed to baz
    var res = baz;
  `;
  var {code} = babel.transform(input, {plugins: [plugin]});
  var f = new Function(`
    ${code};
    return res;
  `);
  var res = f();
  assert(res === 1, 'res is 1');
});

Babel 外围应用相似的办法去获取快照和执行测试。

[babel-plugin-tester](https://github.com/kentcdodds/babel-plugin-tester)

这个包使测试插件更容易。如果您相熟 ESLint 的 RuleTester 您应该对这是相熟的。您能够看看 the docs 去充沛理解可能的状况,但这里有一个简略的例子:

import pluginTester from 'babel-plugin-tester';
import identifierReversePlugin from '../identifier-reverse-plugin';

pluginTester({
  plugin: identifierReversePlugin,
  fixtures: path.join(__dirname, '__fixtures__'),
  tests: {
    'does not change code with no identifiers': '"hello";','changes this code': {
      code: 'var hello ="hi";',
      output: 'var olleh ="hi";',
    },
    'using fixtures files': {
      fixture: 'changed.js',
      outputFixture: 'changed-output.js',
    },
    'using jest snapshots': {
      code: `
        function sayHi(person) {return 'Hello' + person + '!'}
      `,
      snapshot: true,
    },
  },
});

对于未来的更新,请追随 **_@thejameskyle_****_@babeljs_**的 Twitter。

起源参考:babel 手册

正文完
 0