Babel 的工程化實現

介紹

Babel 是一款將將來的 JavaScript 語法編譯成過去語法的 Node.js 工具。本文從 2019 年 11 月的 master 分支源碼入手,介紹 Babel 在解決這類問題時是如何劃分模塊。html

Babel 的模塊劃分

Babel 的模塊劃分

其中 babel-loader 隸屬於 webpack,不在 Babel 主倉庫。node

框架層

常見的編譯器

常見的解析器有 acorn、@babel/parser (babylon)、flow、traceur、typescript、uglify-js 等,各自的 AST 語法樹大體相同。webpack

@babel/parser 的實現

關鍵詞說明

  • Literal:字面量。包括:Boolean、Number、String。
  • Identifier:識別量。包括變量名、undefined、null 等。
  • Val:值。常分爲左值和右值。左值表示一個能夠被賦值的節點,如:[a] 等,左值每每是 Pattern、Identifier 等類型。右值表示一個表明具體值的節點,如:b.c 等,右值每每是 Expression、Identifier、Literal 等類型。左值與右值之間經過等號聯結,表明賦值表達式,如:[a] = b.c。
  • Declaration:賦值。
  • Expression:表達式。經常使用來表示右值。常見的 Expression 有:MemberExpression、BinaryExpression、UnaryExpression、AssignmentExpression、CallExpression 等。
  • Statement:語句。每每由 Expression 組合而成。常見的 Statement 有:ExpressionStatement。
  • Program:程序。全部代碼在一個 Program 下,一個 Program 包含多個並列的 Statement。
let c = 0;
while (a < 10) {
  const b = a % 2;
  if (b == 0) {
    c++;
  }
}
console.log(c);

上面的這段代碼經過 @babel/parser 解析後獲得的 AST 語法樹以下:c++

示例 AST 語法樹

@babel/parser 的 9 層繼承

@babel/parser 的 9 層繼承

  • Parser:初始化
  • StatementParser:解析語句,拼裝成 program,代碼大約有 2100+ 行
  • ExpressionParser:解析表達式,代碼大約有 2400+ 行
  • LValParser:左值處理,將節點變爲能夠被賦值的節點。如:ArrayExpression 轉爲 ArrayPattern
  • NodeUtils:AST 節點操做,如複製等
  • UtilParser:工具函數,如判斷行末等
  • Tokenizer:詞法分析,大約有 1400+ 行
  • LocationParser:文件位置信息
  • CommentsParser:解析註釋
  • BaseParser:插件能力

大部分模塊代碼量在百行左右,其中 StatementParser、ExpressionParser 和 Tokenizer 有較多複雜邏輯。git

@babel/traverse

提供遍歷 AST 語法樹的能力,如:github

traverse(ast, {
  FunctionDeclaration: function(path) {
    path.node.id.name = "x";
  }
});

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

path 對象上有下面的屬性和方法:web

  • 屬性typescript

    • node:節點
    • parent:父節點
    • parentPath:父節點的 path
    • scope:做用域
  • 方法數組

    • get:獲取子節點屬性
    • findParent:向父節點搜尋節點
    • getSibling:獲取兄弟路徑
    • getFunctionParent:獲取包含該節點最近的父路徑,而且是 function
    • getStatementParent:獲取最近的 Statement 類型的父節點
    • replaceWith:用 AST 節點替換該節點
    • replaceWithMultiple:用多個 AST 節點替換該節點
    • replaceWithSourceString:用源碼解析後的 AST 節點替換該節點
    • insertBefore:在該節點前插入兄弟節點
    • insertAfter:在該節點後插入兄弟節點
    • remove:刪除節點
    • pushContainer:將 AST 節點 push 到節點的屬性裏面,相似數組操做

@babel/generator

將 AST 轉爲代碼文本。示例用法:瀏覽器

import { parse } from '@babel/parser';
import generate from '@babel/generator';

const ast = parse('class Example {}');
generate(ast); // => { code: 'class Example {}' }

能夠生成 source map。

import { parse } from '@babel/parser';
import generate from '@babel/generator';

const code = 'class Example {}';
const ast = parse(code);

const output = generate(ast, { sourceMaps: true, sourceFileName: code }); // => { code: 'class Example {}', rawMappings: ... }
// or
const output = generate(ast, { sourceMaps: true, sourceFileName: 'source.js' }, code); // => { code: 'class Example {}', rawMappings: ... }

還能夠合併多個文件,同時生成 source map。

import { parse } from '@babel/parser';
import generate from '@babel/generator';

const a = 'var a = 1;';
const b = 'var b = 2;';
const astA = parse(a, { sourceFilename: 'a.js' });
const astB = parse(b, { sourceFilename: 'b.js' });
const ast = {
  type: 'Program',
  body: [...astA.program.body, ...astB.program.body]
};

const { code, map } = generate(ast, { sourceMaps: true }, {
  'a.js': a,
  'b.js': b
});

@babel/core

主要提供 transform 和 parse 相關的 API。

transform 的流程主要是 parse -> traverse -> generate。

parse 主要提供對 @babel/parser 的封裝。

實現層

@babel/plugin

@babel/plugin-syntax-x

經過插件開關打卡語法解析能力。@babel/parser 中判斷了 plugin 開關,實現了這些語法解析能力。如 @babel/plugin-syntax-jsx:

parserOpts.plugins.push("jsx");

@babel/plugin-transform-x

實現語法的轉換。如 @babel/plugin-transform-exponentiation-operator:

export default {
  name: "transform-exponentiation-operator",
  visitor: build({
    operator: "**",
    build(left, right) {
      return t.callExpression(
        t.memberExpression(t.identifier("Math"), t.identifier("pow")),
        [left, right],
      );
    },
  }),
}

@babel/plugin-proposal-x

支持草案級別的語法轉換。如 @babel/plugin-proposal-numeric-separator:

export default {
  name: "proposal-numeric-separator",
  inherits: syntaxNumericSeparator,

  visitor: {
    CallExpression: replaceNumberArg,
    NewExpression: replaceNumberArg,
    NumericLiteral({ node }) {
      const { extra } = node;
      if (extra && /_/.test(extra.raw)) {
        extra.raw = extra.raw.replace(/_/g, "");
      }
    },
  },
}

@babel/preset-x

提供各種組合好的 plugins、syntax 和 helpers。

經常使用的是 @babel/preset-env,結合 browserslist 設置代碼的兼容性。

@babel/polyfill

從 Babel 7.4.0 起廢棄,推薦使用 core-js 和 regenerator-runtime。其中 core-js 提供了 ECMAScript 的全部兼容代碼,regenerator-runtime 提供了 async、generator 等函數的執行環境。

@babel/helpers

定義了 Babel 運行環境的輔助函數。如在 class 模塊前插入 classCallCheck 的 helper。

plugin 內的函數調用方式:

export default {
  visitor: {
    ClassExpression(path) {
        this.addHelper("classCallCheck");
      // ...
  }
};

生成的代碼中將包含 classCallCheck:

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Circle = function Circle() {
  _classCallCheck(this, Circle);
};

@babel/runtime

提供 Babel 的運行環境,包括 regenerator-runtime。運行環境會提供一些輔助代碼,如:

使用 @babel/helpers 的狀況:

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Circle = function Circle() {
  _classCallCheck(this, Circle);
};

使用 @babel/plugin-transform-runtime 能夠把這些代碼複用起來:

var _classCallCheck = require("@babel/runtime/helpers/classCallCheck");

var Circle = function Circle() {
  _classCallCheck(this, Circle);
};

@babel/runtime 源碼中沒有內容,依賴構建腳本將 @babel/helpers 中的代碼複製過去。

除了這個運行環境以外,Babel 還提供了 @babel/runtime-corejs2 和 @babel/runtime-corejs3,分別是基於 core-js v2 和 v3 提供的運行環境。能夠在 @babel/plugin-transform-runtime 的 corejs 參數中設置使用的運行環境。

輔助層

@babel/types

提供基礎的類型值,建立類型的函數,便於 @babel/plugin、@babel/parser 等使用。

const binaryExpression = t.binaryExpression('+', t.numericLiteral(1), t.numericLiteral(2))

@babel/code-frame

打印出錯位置。示例代碼:

import { codeFrameColumns } from '@babel/code-frame';

const rawLines = `class Foo {
  constructor()
}`;
const location = { start: { line: 2, column: 16 } };
codeFrameColumns(rawLines, location);

輸出的結果是:

1 | class Foo {
> 2 |   constructor()
    |                ^
  3 | }

@babel/highlight

面向控制檯輸出有顏色的代碼片斷。

import highlight from "@babel/highlight";

const code = `class Foo {
  constructor()
}`;
highlight(code);                                                // => "\u001b[36mclass\u001b[39m \u001b[33mFoo\u001b[39m {\n  constructor()\n}"

展現在控制檯上:

@babel/highlight

@babel/template

模板引擎。

import template from "@babel/template";
import generate from "@babel/generator";
import * as t from "@babel/types";

const buildRequire = template(`
  var %%importName%% = require(%%source%%);
`);

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

generate(ast).code                                            // => var myModule = require('my-module');

@babel/helper-x

Babel 的輔助函數,包含經常使用操做、測試函數等,內容比較龐雜。

應用層

@babel/cli

在命令行編譯。

babel script.js # 輸出編譯的結果

@babel/standalone

在瀏覽器編譯。如:Babel 官網等會用到。

<div id="input"></div>
<div id="output"></div>
<button id="transform">轉換</button>
<!-- 加載 @babel/standalone -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script>
document.getElementById('transform').addEventListener('click', function() {
    const input = document.getElementById('input').value;
    const output = Babel.transform(input, { presets: ['es2015'] }).code;
    document.getElementById('output').value = output;
});
</script>

@babel/standalone 也會自動編譯和執行 <script type="text/babel"></script><script type="text/jsx"></script> 中的代碼。

<div id="output"></div>
<!-- 加載 @babel/standalone -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- ES2015 代碼會被編譯執行 -->
<script type="text/babel">
const getMessage = () => "Hello World";
document.getElementById('output').innerHTML = getMessage();
</script>

@babel/node

提供在命令行執行高級語法的環境。@babel/cli 只轉換,不執行,@babel/node 會執行。不適合生產環境使用。

babel-node -e script.js # script.js 裏面可使用高級語法

@babel/register

提供在 Node.js 運行環境內編譯和執行高級語法。不適合生產環境使用。

require("@babel/register")();
require("./script.js");                     // script.js 裏面可使用高級語法

常見的語法轉換結果

Array.from

// input
Array.from([1, 2, 3])

// output
var _array_from_ = require('@babel/runtime-corejs3/core-js-stable/array/from');
_array_from_([1, 2, 3]);

JSX

// input
<div className="text">{content}</div>

// output
React.createElement('div', { className: 'text' }, content);

class

// input
class Example extends Component { constructor(props) { super(props) } }

// output
var _inherits_ = require('@babel/runtime-corejs3/helpers/interits');
var _class_call_check_ = require('@babel/runtime-corejs3/helpers/classCallCheck');
var _possible_constructor_return_ = require('@babel/runtime-corejs3/helpers/possibleConstructorReturn');
var _get_prototype_of_ = require('@babel/runtime-corejs3/helpers/getPrototypeOf');
var _create_class_ = require('@babel/runtime-corejs3/helpers/createClass');

var Example = function (_Component) {
  _inherits_(Example, _Component);

  function Example(props) {
    _class_call_check_(this, Example);

    return _possible_constructor_return_(this, _get_prototype_of_(Example).call(this, props));
  }

  _create_class_(Example, []);

  return Example;
}(Component);

參考資料

相關文章
相關標籤/搜索