Babel 使用入门

Babel 把用最新标准编写的 JavaScript 代码向下编译成能够在今天随处可用的版本。 这一过程叫作转译。node

例如,Babel 可以将新的 ES2015 箭头函数语法:react

const square = n => n * n;

转译为:git

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

不过 Babel 的用途并不止于此,它支持语法扩展,能支持像 React 所用的 JSX 语法,同时还支持用于静态类型检查的流式语法(Flow Syntax)。es6

更重要的是,Babel 的一切都是简单的插件,谁均可以建立本身的插件,利用 Babel 的所有威力去作任何事情。web

再进一步,Babel 自身被分解成了数个核心模块,任何人均可以利用它们来建立下一代的 JavaScript 工具。chrome

已经有不少人都这样作了,围绕着 Babel 涌现出了很是大规模和多样化的生态系统。 在这本手册中将介绍如何使用 Babel 的内建工具以及一些来自于社区的很是有用的东西。typescript


基本使用

安装 Babel

因为 JavaScript 社区没有统一的构建工具、框架、平台等等,所以 Babel 正式集成了对全部主流工具的支持。 从 Gulp 到 Browserify,从 Ember 到 Meteor,无论你的环境设置如何,Babel 都有正式的集成支持。express

本手册的目的主要是介绍 Babel 内建方式的安装,不过你能够访问交互式的安装页面来查看其它的整合方式。npm

首先新建一个 demo,方便演示:编程

mkdir -p babel-demo && cd babel-demo
npm init -y
git init
// .gitignore
node_modules
// src/index.js
const square = n => n * n;

@babel/cli

@babel/cli 是一种在命令行下使用 Babel 编译文件的简单方法。

先全局安装它来学习基础知识。@babel/cli 依赖于 @babel/core

npm install --save-dev @babel/cli @babel/core

咱们能够这样来编译 src/index.js

npx babel src/index.js

这将把编译后的结果直接输出至终端:

babel-demo git:(master) ✗ npx babel src/index.js
// src/index.js
const square = n => n * n;

使用 --out-file 或着 -o 能够将结果写入到指定的文件。

npx babel src/index.js -o build/index.js
// build/index.js
// src/index.js
const square = n => n * n;

若是咱们想要把一个目录整个编译成一个新的目录,可使用 --out-dir 或者 -d。.

npx babel src -d build

@babel/register

建立 src/index.js 文件。

// src/index.js
const square = n => n * n;
console.log(square(2));

首先安装 @babel/register

npm install --save-dev @babel/register

接着,在项目中建立 src/register.js 文件并添加以下代码:

// src/register.js
require("@babel/register");
require("./index.js");

这样作能够把 babel 注册 到 Node 的模块系统中,以后全部 require.es6, .es, .jsx, .mjs, .js 结尾的文件时,babel 会默认对文件进行转译。这种方式不适合生产环境,适合用于脚本中。

如今咱们可使用 node src/register.js 来运行了。

babel-demo git:(master) ✗ node src/register.js
4

@babel/node

若是你要用 node CLI 来运行代码,那么整合 Babel 最简单的方式就是使用 @babel/node CLI,它是 node CLI 的替代品。这种方式不适合生产环境,适合用于脚本中。

babel-node 来替代 node 运行全部的代码 。

若是用 npm scripts 的话只须要这样作:

{
    "scripts": {
-     "script-name": "node script.js"
+     "script-name": "babel-node script.js"
    }
  }

也能够:

npx babel-node script.js

@babel/core

若是你须要以编程的方式来使用 Babel,可使用 @babel/core 这个包。

首先安装 @babel/core

$ npm install @babel/core
var babel = require("@babel/core");

字符串形式的 JavaScript 代码能够直接使用 babel.transform 来编译。

babel.transform("code();", options);
// => { code, map, ast }

若是是文件的话,可使用异步 api:

babel.transformFile("filename.js", options, function(err, result) {
  result; // => { code, map, ast }
});

或者是同步 api:

babel.transformFileSync("filename.js", options);
// => { code, map, ast }

要是已经有一个 Babel AST(抽象语法树)了就能够直接从 AST 进行转换。

babel.transformFromAst(ast, code, options);
// => { code, map, ast }

options 参考 http://babeljs.io/docs/usage/...


配置 Babel

你或许已经注意到了,目前为止经过运行 Babel 本身咱们并没能“翻译”代码,而仅仅是把代码从一处拷贝到了另外一处。

这是由于咱们还没告诉 Babel 要作什么,默认状况下它什么都不作。

能够经过安装插件(plugins)预设(presets,也就是一组插件)来指示 Babel 去作什么事情。

.babelrc

可使用配置文件的形式来告诉 babel 如何转译代码。你须要作的就是在项目的根路径下建立 .babelrc 文件:

{
  "presets": [],
  "plugins": []
}

能够用其余方式给 Babel 传递选项,但 .babelrc 文件是约定也是最好的方式。

@babel/preset-env

@babel/preset-es2015 废弃了,咱们使用 @babel/preset-env 来将全部 ECMAScript 2015+ 的代码转换成 ES5 代码:

npm install --save-dev @babel/preset-env

咱们修改 .babelrc 来包含这个预设。在.babelrc中配置pluginpreset时,@babel/preset-env = preset-env 以此类推。

{
  "presets": [
    "@babel/preset-env"
  ],
  "plugins": []
}

package.json

"scripts": {
    "babel-script": "babel-node src/index.js",
    "build": "babel src/index.js "
  },

运行 npx babel src/index.js

babel-demo git:(master) ✗ npx babel src/index.js 
"use strict";

// src/index.js
var square = function square(n) {
  return n * n;
};

console.log(square(2));

使用 @babel/preset-env 作代码兼容:须要着重考虑 useBuiltInstargetcore-js 三个字段。

@babel/preset-env 有不少配置选项,参考官网 ,其中 useBuiltIns = "usage" | "entry" 时,会进行代码兼容。

false: useBuiltIns 的默认值,不进行兼容。

usage: 推荐。将代码转换为 target 指定的目标环境可运行的代码,原代码用到了哪些新特性 babel 自动对这些新特性进行兼容。

.babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "targets": {
          "node": "6"
        },
        "corejs": 3
      }
    ]
  ]
}

src/index.js

// src/index.js
const a = async n => {
  await Promise.resolve(123)
}

运行 npx babel src -d build :

// build/index.js
"use strict";
const a = /*#__PURE__*/function () {
  var _ref = _asyncToGenerator(function* (n) {
    yield Promise.resolve(123);
  });

  return function a(_x) {
    return _ref.apply(this, arguments);
  };
}();

entry: 使用此选项,须要手动在入口 js 文件 import/require 进来 core-js regenerator-runtime/runtime ,babel 会将此 import/require 包的语句转换为 target 指定的目标环境对应的兼容语句(目标环境相对于彻底支持 core-js 的环境还缺乏哪些特性,就 import/require 对应的哪些包。

// src/index.js
import "core-js/stable";
import "regenerator-runtime/runtime";
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "targets": {
          "chrome": "72"
        },
        "corejs": 3
      }
    ]
  ]
}

运行 npx babel src -d build :

// build/index.js
"use strict";

require("core-js/modules/es.array.unscopables.flat");

require("core-js/modules/es.array.unscopables.flat-map");

require("core-js/modules/es.math.hypot");

require("core-js/modules/es.object.from-entries");

require("core-js/modules/web.immediate");

访问官网连接 try-it-out,能很是直观的测试各个选项对应的转译结果。

@babel/preset-react

WIP: 没有用过,之后更新。

设置 React 同样容易。只须要安装这个预设:

$ npm install --save-dev @babel/preset-react

而后在 .babelrc 文件里补充:

{
    "presets": [
      "es2015",
+     "react"
    ],
    "plugins": []
  }

@babel/preset-stage-x

@babel/preset-stage-x都被废弃了。不介绍了。


使用 Babel 进行 polyfill

即使你已经用 Babel 编译了你的代码,但这还不算完。

@babel/polyfill

已废弃,推荐使用 @babel/preset-env 进行特性兼容。

@babel/runtime

推荐使用 @babel/preset-env进行特性兼容。


配置 Babel(进阶)

WIP: 对大多数人来讲不太经常使用,有时间再研究。

实现 Babel 插件

接下来介绍如何建立 Babel 插件等方面的内容。

基础

抽象语法树(ASTs)

这个处理过程当中的每一步都涉及到建立或操做抽象语法树( AST)。

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。

转换

转换步骤接收 AST 并对其进行遍历,在此过程当中对节点进行添加、更新及移除等操做。 这是 Babel 或是其余编译器中最复杂的过程,同时也是插件将要介入的部分。

生成

代码生成步骤把最终(通过转换以后)的 AST 转换成字符串形式的代码,同时还会建立source maps。

遍历

想要转换 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)的概念。

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

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

// 你也能够先建立一个Visitor对象,并在稍后给它添加方法。
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}
注意Identifier() { ... }Identifier: { enter() { ... } } 的简写形式。.

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

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

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

这些调用都发生在进入节点时,不过有时候咱们也能够在退出时调用Visitor的方法。

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

- 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

因此当建立Visitor时你实际上有两次机会来访问一个节点。

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

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

在flow-comments 插件中的例子以下:

const MyVisitor = {
  "ExportNamedDeclaration|Flow"(path) {}
};

你也能够在Visitor中使用别名(如@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
}

固然 path 对象还包含添加、更新、移动和删除节点有关的其余不少方法,稍后咱们再来看这些方法。

在某种意义上,path 是一个 node 在树中的位置以及关于该 node 各类信息的响应式 Reactive 表示。 当你调用一个修改树的方法后,路径信息也会被更新。 Babel 帮你管理这一切,从而使得节点操做简单,尽量作到无状态。

Paths in Visitors

使用 Visitor 中的 Identifier() 成员方法的时,你其实是在访问路径而非节点。 经过这种方式,你操做的就是节点的响应式表示(即 path )而非节点自己。

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

State(状态)

考虑下列代码:

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

让咱们写一个把 n 重命名为 x 的 Visitor 。

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";
    }
  }
};

对上面的例子代码这段Visitor代码也许能工做,但它很容易被打破:

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

更好的处理方式是使用递归,下面让咱们把一个Visitor放进另一个Visitor里面。

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);

固然,这只是一个刻意编写的例子,不过它演示了如何从Visitor中消除全局状态。

Scopes(做用域)

接下来让咱们介绍做用域(scope))的概念。

// 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.";
  }
}

当编写一个转换时,必须当心做用域。咱们得确保在改变代码的各个部分时不会破坏已经存在的代码。

咱们在添加一个新的引用时须要确保新增长的引用名字和已有的全部引用不冲突。 或者咱们仅仅想在给定的做用域中找出使用一个变量的全部引用。

做用域能够被表示为以下形式:

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

WIP: 本节后面的内容

当你建立一个新的做用域时,须要给出它的路径和父做用域,以后在遍历过程当中它会在该做用域内收集全部的引用(“绑定”)。

一旦引用收集完毕,你就能够在做用域(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 其实是一组模块的集合。本节咱们将探索一些主要的模块,解释它们是作什么的以及如何使用它们。

@babel/parser

@babel/parser 是 Babel 的解析器。

首先,让咱们安装它。

$ npm install --save @babel/parser

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

// api-test.js
const parser = require("@babel/parser")

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

console.log(parser.parse(code))

运行:

// node "babel-demo/src/api-test.js"
Node {
  type: 'File',
  start: 0,
  end: 38,
  loc: SourceLocation {
    start: Position { line: 1, column: 0 },
    end: Position { line: 3, column: 1 }
  },
  errors: [],
  program: Node {
    type: 'Program',
    start: 0,
    end: 38,
    loc: SourceLocation { start: [Position], end: [Position] },
    sourceType: 'script',
    interpreter: null,
    body: [ [Node] ],
    directives: []
  },
  comments: []
}

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

parser.parse(code, {
  plugins: ["jsx"] // default: []
});

@babel/traverse

@babel/traverse(遍历)模块维护了整棵树的状态,而且负责替换、移除和添加节点。

运行如下命令安装:

npm install --save @babel/traverse

咱们能够和 Babylon 一块儿使用来遍历和更新节点:

const traverse = require("@babel/traverse").default
const parser = require("@babel/parser")

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

const ast = parser.parse(code);

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

能够运行 node inspect "babel-demo/src/api-test.js" 查看修改后的 ast

@babel/types

@babel/types 模块是一个用于 AST 节点的 工具库, 它包含了构造、验证以及变换 AST 节点的方法,对编写处理AST逻辑很是有用。

npm install --save @babel/types

而后按以下所示来使用:

const traverse = require("@babel/traverse").default
const parser = require("@babel/parser")
const t = require("@babel/types")


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

const ast = parser.parse(code);

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(构建器)

WIP: 之后再写

你会注意到上面的 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(验证器)

WIP: 之后再写

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

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

运行如下命令来安装它:

npm install --save @babel/generator

而后按以下方式使用:

const traverse = require("@babel/traverse").default
const parser = require("@babel/parser")
const t = require("@babel/types")
const generate = require("@babel/generator").default;

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

const ast = parser.parse(code);

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

运行

// node "babel-demo/src/api-test.js"
{
  code: 'function square(x) {\n  return x * x;\n}',
  map: null,
  rawMappings: null
}

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

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

@babel/template

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

$ npm install --save @babel/template
const traverse = require("@babel/traverse").default
// const parser = require("@babel/parser")
const t = require("@babel/types")
const generate = require("@babel/generator").default;

const template = require("@babel/template").default

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);
// node "babel-demo/src/api-test.js"
var myModule = require("my-module");

编写你的第一个 Babel 插件

如今咱们已经熟悉了 Babel 的全部基础知识了,咱们来编写一个 Babel 插件吧。

先从一个接收了当前babel对象做为参数的 function 开始。

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

由于使用频繁,因此直接取出 babel.types 会更方便。

export default function({ types: t }) {
// 上面等价于 export default function(babel) {
// let t = babel.types
// 这是 ES2015 语法中的对象解构
  
  // plugin contents
}

接着返回一个对象,其 visitor 属性是这个插件的主要 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 Visitor方法开始:

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 插件。

完整的代码以下:

// src/api-test.js
function myPlugin({ types: t }) {
  return {
    visitor: {
      BinaryExpression(path) {
        if (path.node.operator !== "===") {
          return;
        }
        path.node.left = t.identifier("sebmck");
        path.node.right = t.identifier("dork");
      }
    }
  };
}

module.exports = myPlugin
// babel.config.js
const path = require("path")
const config = {
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "targets": {
          "chrome": "72"
        },
        "corejs": 3
      }
    ]
  ],
  "plugins": [path.resolve(__dirname, "./src/api-test")]
}

module.exports = config

源码:

// src/index.js
foo === bar;

运行 npx babel src -d build ,输出:

// build/index.js
sebmck === dork;

转换操做

访问

获取子节点的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');
}

详细解释以下:

src/api-test.js 修改成

// src/api-test.js
function myPlugin({ types: t }) {
  return {
    visitor: {
      BinaryExpression(path) {
        debugger
        path.get('left');
      },
      Program(path) {
        debugger
        path.get('body.0');
      }
    }
  };
}
module.exports = myPlugin

运行 babel : npx -n inspect babel src/index.js -o build

path.get('left') 获取的是 path 的node 属性里的 left 属性对应的 path。

Snip20200613_27.png

能够看到 path.get('body.0') 获取的是 path 的node 属性里的 body 数组的第一个值。

Snip20200613_26.png

image-20200613223436540.png

检查节点的类型

若是要检查节点的类型,最好的方式是:

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();

向上遍历语法树,直到找到最近的 Statement 类型的父节点

path.getStatementParent();

获取同级路径

若是一个路径是在一个 FunctionProgram中的列表里面,它就有同级节点。

  • 使用path.inList来判断路径是否有同级节点,
  • 使用path.getSibling(index)来得到同级路径,
  • 使用 path.key获取路径所在容器的索引,
  • 使用 path.container获取路径的容器(包含全部同级节点的数组)
  • 使用 path.listKey获取容器的key
@babel/minify 中的 transform-merge-sibling-variables 插件用到了这些API
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]
      }
    }
  };
}

中止遍历

若是你的插件在某种状况下不须要运行,最好尽快 return。

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

WIP: 之后再写

若是您在顶级路径中进行子遍历,则可使用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)";
  }

用字符串源码替换节点

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

插入兄弟节点

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)中

WIP: 不经常使用,仅了解。

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;
- }

替换父节点

只需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(做用域)

检查本地变量是否被绑定

先挖坑,之后再填

建立一个 UID

先挖坑,之后再填

提高变量声明至父级做用域

先挖坑,之后再填

重命名绑定及其引用

先挖坑,之后再填


结束语

掌握本文详细介绍的这些知识,已经足以应该90%以上的使用babel的场景了。

关于插件选项、构建节点、最佳实践等方面的内容将在下篇文章里介绍。

原天堂再无babel。

相关文章
相关标签/搜索