新的一年babel瞭解一下

我就什麼話也不說發表於2019-02-10

參考文件 Babel 外掛手冊

Babel的作用

Babel是一個JavaScript編譯器

很多瀏覽器目前還不支援ES6的程式碼,Babel的作用就是把瀏覽器不資辭的程式碼編譯成資辭的程式碼。

注意很重要的一點就是,Babel只是轉譯新標準引入的語法,比如ES6的箭頭函式轉譯成ES5的函式, 但是對於新標準引入的新的原生物件,部分原生物件新增的原型方法,新增的API等(如SetPromise),這些Babel是不會轉譯的,需要引入polyfill來解決。

API

Babel實際上是一組模組的集合。

@babel/core

Babel 的編譯器,核心 API 都在這裡面,比如常見的transformparse

npm i @babel/core -D
複製程式碼
  • 使用
import { transform } from '@babel/core';
import * as babel from '@babel/core';
複製程式碼
  • transform

babel.transform(code: string, options?: Object)

babel.transform(code, options, function(err, result) {
  result; // => { code, map, ast }
});
複製程式碼
  • parse

babel.parse(code: string, options?: Object, callback: Function)

@babel/cli

cli是命令列工具, 安裝了@babel/cli就能夠在命令列中使用babel 命令來編譯檔案。

npm i @babel/core @babel/cli -D
複製程式碼
  • 使用
babel script.js
複製程式碼

Note: 因為沒有全域性安裝@babel/cli, 建議用npx命令來執行,或者./node_modules/.bin/babel,關於npx命令,可以看下官方文件

@babel/node

直接在node環境中,執行 ES6 的程式碼

  • 使用
npx babel-node script.js
複製程式碼

babylon

Babel的解析器

首先,安裝一下這個外掛。

npm i babylon -S
複製程式碼

先從解析一個程式碼字串開始:

// src/index.js
import * as babylon from 'babylon';

const code = `function add(m, n) {
  return m + n;
}`;

babylon.parse(code);
複製程式碼
npx babel-node src/index.js
複製程式碼
Node {
   type: "File",
   start: 0,
   end: 38,
   loc: SourceLocation {...},
   program: Node {...},
   comments: [],
   tokens: [...]
}
複製程式碼

babel-traverse

用於對 AST 的遍歷,維護了整棵樹的狀態,並且負責替換、移除和新增節點。

執行以下命令安裝:

npm i babel-traverse -S
複製程式碼
import * as babylon from 'babylon';
import traverse from 'babel-traverse';

const code = `function add(m, n) {
  return m + n;
}`;

const ast = babylon.parse(code);

traverse(ast, {
  enter(path) {
    if (
      path.node.type === 'Identifier' &&
      path.node.name === 'm'
    ) {
      // do something
    }
  }
});
複製程式碼

babel-types

用於 AST 節點的 Lodash 式工具庫, 它包含了構造、驗證以及變換 AST 節點的方法,對編寫處理 AST 邏輯非常有用。

npm i babel-types -S
複製程式碼
import traverse from 'babel-traverse';
import * as t from 'babel-types';

traverse(ast, {
  enter(path) {
    if (t.isIdentifier(path.node, { name: 'm' })) {
      // do something
    }
  }
});
複製程式碼

babel-generator

Babel 的程式碼生成器,它讀取AST並將其轉換為程式碼和原始碼對映(sourcemaps)

npm i babel-generator -S
複製程式碼
import * as babylon from 'babylon';
import generate from 'babel-generator';

const code = `function add(m, n) {
  return m + n;
}`;

const ast = babylon.parse(code);

generate(ast, {}, code);
// {
//   code: "...",
//   map: "...",
//   rawMappings: "..."
// }
複製程式碼

Babel是怎麼工作的

新的一年babel瞭解一下

為了理解Babel,我們從ES6最受歡迎的特性箭頭函式入手。

假設要把下面這個箭頭函式的Javascript程式碼

(foo, bar) => foo + bar;
複製程式碼

編譯成瀏覽器支援的程式碼:

'use strict';
(function (foo, bar) {
  return foo + bar;
});
複製程式碼

Babel的編譯過程和大多數其他語言的編譯器相似,可以分為三個階段:

  • 解析(Parsing):將程式碼字串解析成抽象語法樹。
  • 轉換(Transformation):對抽象語法樹進行轉換操作。
  • 生成(Code Generation): 根據變換後的抽象語法樹再生成程式碼字串。

解析(Parsing)

Babel拿到原始碼會把程式碼抽象出來,變成AST(抽象語法樹),洋文是Abstract Syntax Tree

抽象語法樹是原始碼的抽象語法結構的樹狀表示,樹上的每個節點都表示原始碼中的一種結構,這所以說是抽象的,是因為抽象語法樹並不會表示出真實語法出現的每一個細節,比如說,巢狀括號被隱含在樹的結構中,並沒有以節點的形式呈現。它們主要用於原始碼的簡單轉換。

箭頭函式(foo, bar) => foo + bar;的AST長這樣:

{
  "type": "Program",
  "start": 0,
  "end": 202,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 179,
      "end": 202,
      "expression": {
        "type": "ArrowFunctionExpression",
        "start": 179,
        "end": 202,
        "id": null,
        "expression": true,
        "generator": false,
        "params": [
          {
            "type": "Identifier",
            "start": 180,
            "end": 183,
            "name": "foo"
          },
          {
            "type": "Identifier",
            "start": 185,
            "end": 188,
            "name": "bar"
          }
        ],
        "body": {
          "type": "BinaryExpression",
          "start": 193,
          "end": 202,
          "left": {
            "type": "Identifier",
            "start": 193,
            "end": 196,
            "name": "foo"
          },
          "operator": "+",
          "right": {
            "type": "Identifier",
            "start": 199,
            "end": 202,
            "name": "bar"
          }
        }
      }
    }
  ],
  "sourceType": "module"
}
複製程式碼

上面的AST描述了原始碼的每個部分以及它們之間的關係,可以自己在這裡試一下astexplorer

AST是怎麼來的?解析過程分為兩個步驟:

  • 分詞:將整個程式碼字串分割成語法單元陣列

Javascript程式碼中的語法單元主要指如識別符號(if/else、return、function)、運算子、括號、數字、字串、空格等等能被解析的最小單元

[
    {
        "type": "Punctuator",
        "value": "("
    },
    {
        "type": "Identifier",
        "value": "foo"
    },
    {
        "type": "Punctuator",
        "value": ","
    },
    {
        "type": "Identifier",
        "value": "bar"
    },
    {
        "type": "Punctuator",
        "value": ")"
    },
    {
        "type": "Punctuator",
        "value": "=>"
    },
    {
        "type": "Identifier",
        "value": "foo"
    },
    {
        "type": "Punctuator",
        "value": "+"
    },
    {
        "type": "Identifier",
        "value": "bar"
    }
]
複製程式碼
  • 語法分析:建立分析語法單元之間的關係

語義分析則是將得到的詞彙進行一個立體的組合,確定詞語之間的關係。考慮到程式語言的各種從屬關係的複雜性,語義分析的過程又是在遍歷得到的語法單元組,相對而言就會變得更復雜。

簡單來說語義分析既是對語句和表示式識別,這是個遞迴過程,在解析中,Babel 會在解析每個語句和表示式的過程中設定一個暫存器,用來暫存當前讀取到的語法單元,如果解析失敗,就會返回之前的暫存點,再按照另一種方式進行解析,如果解析成功,則將暫存點銷燬,不斷重複以上操作,直到最後生成對應的語法樹。

轉換(Transformation)

Plugins

外掛應用於babel的轉譯過程,尤其是第二個階段Transformation,如果這個階段不使用任何外掛,那麼babel會原樣輸出程式碼。

Presets

babel官方幫我們做了一些預設的外掛集,稱之為preset,這樣我們只需要使用對應的preset就可以了。每年每個preset只編譯當年批准的內容。 而babel-preset-env 相當於 es2015 ,es2016 ,es2017 及最新版本。

Plugin/Preset 路徑

如果 plugin 是通過 npm 安裝,可以傳入 plugin 名字給 babel,babel 將檢查它是否安裝在node_modules

"plugins": ["babel-plugin-myPlugin"]
複製程式碼

也可以指定你的 plugin/preset 的相對或絕對路徑。

"plugins": ["./node_modules/asdf/plugin"]
複製程式碼

Plugin/Preset 排序

如果兩次轉譯都訪問相同的節點,則轉譯將按照 plugin 或 preset 的規則進行排序然後執行。

  • Plugin 會執行在 Preset 之前。
  • Plugin 會從第一個開始順序執行。
  • Preset 的順序則剛好相反(從最後一個逆序執行)。

例如:

{
  "plugins": [
    "transform-decorators-legacy",
    "transform-class-properties"
  ]
}
複製程式碼

將先執行transform-decorators-legacy再執行transform-class-properties

但 preset 是反向的

{
  "presets": [
    "es2015",
    "react",
    "stage-2"
  ]
}
複製程式碼

會按以下順序執行: stage-2react, 最後es2015

生成(Code Generation)

babel-generator通過 AST 樹生成 ES5 程式碼

編寫一個Babel外掛

基礎的東西講了些,下面說下具體如何寫外掛。

外掛格式

先從一個接收了當前babel物件作為引數的function開始。

export default function(babel) {
  // plugin contents
}
複製程式碼

我們經常會這樣寫

export default function({ types: t }) {
    //
}
複製程式碼

接著返回一個物件,其visitor屬性是這個外掛的主要訪問者。

export default function({ types: t }) {
  return {
    visitor: {
      // visitor contents
    }
  };
};
複製程式碼

visitor中的每個函式接收2個引數:pathstate

export default function({ types: t }) {
  return {
    visitor: {
      CallExpression(path, state) {}
    }
  };
};
複製程式碼

寫一個簡單的外掛

我們寫一個簡單的外掛,把所有定義變數名為a的換成b, 先從astexplorer看下var a = 1的 AST

{
  "type": "Program",
  "start": 0,
  "end": 10,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 9,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 9,
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 5,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 8,
            "end": 9,
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "module"
}
複製程式碼

從這裡看,要找的節點型別就是VariableDeclarator,下面開搞

export default function({ types: t }) {
  return {
    visitor: {
      VariableDeclarator(path, state) {
        if (path.node.id.name == 'a') {
          path.node.id = t.identifier('b')
        }
      }
    }
  }
}
複製程式碼

我們要把id屬性是 a 的替換成 b 就好了。但是這裡不能直接path.node.id.name = 'b'。如果操作的是object,就沒問題,但是這裡是 AST 語法樹,所以想改變某個值,就是用對應的 AST 來替換,現在我們用新的識別符號來替換這個屬性。

測試一下

import * as babel from '@babel/core';
const c = `var a = 1`;

const { code } = babel.transform(c, {
  plugins: [
    function({ types: t }) {
      return {
        visitor: {
          VariableDeclarator(path, state) {
            if (path.node.id.name == 'a') {
              path.node.id = t.identifier('b')
            }
          }
        }
      }
    }
  ]
})

console.log(code); // var b = 1
複製程式碼

實現一個簡單的按需打包功能

例如我們要實現把import { Button } from 'antd'轉成import Button from 'antd/lib/button'

通過對比 AST 發現,specifiers裡的typesource不同。

// import { Button } from 'antd'
"specifiers": [
    {
        "type": "ImportSpecifier",
        ...
    }
]
複製程式碼
// import Button from 'antd/lib/button'
"specifiers": [
    {
        "type": "ImportDefaultSpecifier",
        ...
    }
]
複製程式碼
import * as babel from '@babel/core';
const c = `import { Button } from 'antd'`;

const { code } = babel.transform(c, {
  plugins: [
    function({ types: t }) {
      return {
        visitor: {
          ImportDeclaration(path) {
            const { node: { specifiers, source } } = path;
            if (!t.isImportDefaultSpecifier(specifiers[0])) { // 對 specifiers 進行判斷
              const newImport = specifiers.map(specifier => (
                t.importDeclaration(
                  [t.ImportDefaultSpecifier(specifier.local)],
                  t.stringLiteral(`${source.value}/lib/${specifier.local.name}`)
                )
              ))
              path.replaceWithMultiple(newImport)
            }
          }
        }
      }
    }
  ]
})

console.log(code); // import Button from "antd/lib/Button";
複製程式碼

總結

主要介紹了一下幾個babel的 API,和babel編譯程式碼的過程以及簡單編寫了一個babel外掛

相關文章