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。

相關文章
相關標籤/搜索