babel知識體系漫談

釘釘前端團隊發表於2020-04-03

釘釘前端團隊原創,點選右上角關注我們,瞭解更多前端技術

作者: 燭象

引言

在JavaScript蓬勃發展的今天,ES6/7、typescript已經成為程式碼編寫的標配。

上一篇文章,我們介紹了釘釘IDL和自動生成typescript定義的工具,本文將會介紹AST相關js知識:babel。

關於babel

一句話闡述什麼是babel:

babel是一個主要用於將ES2015+版本的程式碼編譯成向下相容(比如ES5/ES3)js版本的編譯器。

// Babel Input: ES2015 arrow function
[1, 2, 3].map((n) => n + 1);
// Babel Output: ES5 equivalent
[1, 2, 3].map(function(n) {
  return n + 1;
});
複製程式碼

結合實際使用場景,我們接觸到的babel使用方式一般為

  • .babelrc/babel.config.json (babel配置檔案)
  • babel-loader (webpack/rollup等)

然而,.babelrc的每一塊配置後面究竟代表著babel怎樣的處理方式,這個估計很少有人能講得清楚。

babel知識體系

1、主要元件

  • 原始碼到AST: babel/parser(前身為babylon)

依賴acorn/acorn-jsx,用於將原始碼(如ES2015程式碼) 編譯成 AST(抽象語法樹)

  • AST到輸出程式碼: babel/generator

用於將AST轉換為最終程式碼,根據不同的引數option,實現程式碼功能(比如sourceMap的實現)

2、結構轉換

  • 對AST實現顆粒化改造: @babel/traverse

通過AST節點遍歷,方便使用方對AST節點進行邏輯重組。

import * as parser from "@babel/parser";
import traverse from "@babel/traverse";
// 原始碼
const code = `function square(n) {
  return n * n;
}`;
const ast = parser.parse(code);
traverse(ast, {
  enter(path) {
  // 節點轉換
    if (path.isIdentifier({ name: "n" })) {
      path.node.name = "x";
    }
  }
});
// 新程式碼 function square(x) {\n  return x * x;\n}
console.log(generate(ast))
複製程式碼

3、工具元件

  • 程式碼行列定位: @babel/code-frame

對程式碼行列進行定位

import { codeFrameColumns } from '@babel/code-frame';
const rawLines = `class Foo {
  constructor()
}`;
const location = { start: { line: 2, column: 16 } };
const result = codeFrameColumns(rawLines, location, { /* options */ });
console.log(result);
// 結果如下
  1 | class Foo {
> 2 |   constructor()
    |                ^
  3 | }
複製程式碼
  • 執行時優化(比如重複程式碼等): @babel/runtime(包含regenerator-runtime)

對程式碼重複率進行優化,比如優化如下class語法轉換

class Circle {}
function _classCallCheck(instance, Constructor) {
  //...
}
var Circle = function Circle() {
  _classCallCheck(this, Circle);
};
複製程式碼

通過@babel/runtime,將class語法以模組化的方式替換具體的實現,以達到減少重複程式碼的目的。

var _classCallCheck = require("@babel/runtime/helpers/classCallCheck");
var Circle = function Circle() {
  _classCallCheck(this, Circle);
};
複製程式碼
  • 程式碼模板: @babel/template

模板程式碼,可以對比printf、Mustache的語法

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"),
});
// 最終的程式碼變為var myModule = require("my-module");
console.log(generate(ast).code);
複製程式碼

除了以上幾個大件之外,babel工具體系還有types、helpers等來優化AST和generate最終程式碼。接下來要分享的是babel中非常重要的一環: plugins和presets。

4、babel plugins和 babel presets

babel作為編譯器,它對程式碼的轉換工作全部依賴於plugins(外掛)。如果開發者連babelrc都沒有配置的話,程式碼轉換將什麼都不做。

對於babel來說plugins的作用是轉換程式碼。然而,js語法何其多,箭頭函式、class語法、async/await等等,這麼多語法需要非常龐大的外掛體系。對於開發者來說,極其不友好。

babel在外掛的概念基礎上新增了個外掛列表的概念,叫做presets(預設)。 比如@babel/preset-stage-0,代表的是支援stage-0語法的外掛列表,通過presets解放了開發者極大的配置babel的工作量。

關於babel-polyfill

對於babel7.4.0,這個庫官方已經廢棄了,取而代之的是core-js/stableregenerator-runtime/runtime。 對於很多babel-polyfill的使用方而言,這個庫確實對bundle的最終大小產生了影響。官方推薦的是採用@babel/preset-envuseBuiltIns這個option配合起來,以便只引入你所需要的polyfill。

來看下這個能力有多酷炫:

var a = new Promise();
=轉換為=>
import "core-js/modules/es.promise";
var a = new Promise();

var b = new Map();
=轉換為=>
import "core-js/modules/es.map";
var b = new Map();
複製程式碼

寫在後面

關於babel更多的知識,歡迎大家移步babel官網。 希望讀完本文你有一定的收穫。

招人時間:

釘釘前端團隊社招全面開啟,求各路技術達人加入。

簡歷投遞郵箱: xiaogang.hxg@alibaba-inc.com

筆者釘釘號: huangxiaogang

掃碼關注更多釘釘技術

釘釘技術圈

?釘釘歡迎掃碼關注《釘釘技術圈》

釘釘微信公眾號

?歡迎掃碼關注《釘釘大前端團隊》微信公眾號

相關文章