Babel淺談

語言: CN / TW / HK

一. 介紹

  1. Babel 是什麼

官方:Babel 是一個 JavaScript 編譯器!

我:Babel 是一個原始碼到目的碼的轉換器!

如圖可以看到, Babel 的作用就是將「原始碼」轉換為「目的碼」,至於轉換中間的過程,下文討論。

  1. Babel 的發展歷史

Babel 在 4.0 版本之前叫做 "6to5",由澳大利亞的 Sebastian (早已離開 Babel 團隊,目前就職於 Facebook,依然致力於前端工具鏈開發中)在他的 2014 年十一月(那是他的高中時期)開始釋出的,通過 6to5 這個名字我們就能知道,最初的 Babel 就是將 ES6 的程式碼轉換成 ES5 的程式碼,2015 年與 ESNext [1] 合併並改名成 Babel。Babel 的本意是 " 巴別塔 [2] ",取自神話小說中。由 6to5 重新命名為 Babel 前,6to5 已經走過了三個版本,所以改名後的 Babel 從版本 4.0 開始,此時已經能夠轉譯 ES7 與 JSX,2015 年釋出 5.0,引入了 stage,建立了 plugin system 來支援自定義轉譯,同年又釋出 6.0 ,拆分了幾個核心包,引入了 presets 和 plugin/presets options 的概念,18 年,釋出了 7.0,除了效能方面的提升以外,增加了對 typescript 的支援,對安裝包使用「@babel」的名稱空間。

  1. Babel 的作用

主要用於將採用 ECMAScript 2015+ 語法編寫的程式碼轉換為 es5 語法,讓開發者無視使用者瀏覽器的差異性,並且能夠用新的 JS 語法及特性進行開發。除此之外,Babel 能夠轉換 JSX 語法,並且能夠支援 TypeScript 轉換為 JavaScript。 總結一下:Babel 的作用如下

  • 語法轉換

  • 通過 Polyfill 方式在目標環境中新增缺失的特性

  • 原始碼轉換

二. Babel7 的使用

  1. 配置檔案

Babel 支援多種形式的配置檔案,根據使用場景不同可以選擇不同的配置檔案。如果配置中需要書寫 js 邏輯,可以選擇「babel.config.js」或者 「.babelrc.js」;如果只是需要一個簡單的 key-value 配置,那麼可以選擇「.babelrc」,甚至可以直接在 「package.json」 中配置。

這裡給出在各種配置檔案中配置 Babel 的書寫形式,以 plugins 和 presets 配置為例:

// babel.config.js
module.exports = function(api) {
api.cache(true);
const plugins = ["@babel/plugin-transform-arrow-functions"];
const presets = ["@babel/preset-env"];
return {
plugins,
presets
};
}
// .babelrc.js
const plugins = ["@babel/plugin-transform-arrow-functions"];
const presets = ["@babel/preset-env"];
module.exports = { plugins, presets };
// .babelrc
{
plugins: ["@babel/plugin-transform-arrow-functions"],
presets: ["@babel/preset-env"]
}
// package.json
{
"name": "my-package",
"version": "1.0.0",
// ...省略其他配置
"babel": {
"plugins": ["@babel/plugin-transform-arrow-functions"],
"presets": ["@babel/preset-env"]
}
}
  1. 小試個牛刀

所有 Babel 的包都發布在 npm 上,並且名稱以 @babel 為字首(自從版本 7.0 之後),接下來,我們一起看下 @babel/core 和 @babel/cli 這兩個 npm 包。

  • @babel/core - 核心庫,封裝了 Babel 的核心能力

  • @babel/cli - 命令列工具, 提供了「babel」 這個命令

接下來我們試著通過 Babel 將 es6 中的「箭頭函式」語法轉換為 es5 中「function 函式申明」語法:

// 安裝這兩個依賴包
npm install --save-dev @babel/core @babel/cli
/*
package.json 檔案中配置 babel 執行命令
以下命令含義為:將 src 目錄中的檔案經過 babel 轉換,並將轉換後的檔案輸出到 lib 目錄中
*/

"script": {
"compile": "babel src --out-dir lib --watch"
}
//  src/index.js
const fn = () => {
console.log('xuemingli');
};
//  lib/index.js
const fn = () => {
console.log('xuemingli');
};

此時,我們執行 npm run compile 後,發現 lib/index.js 中的依然是箭頭函式,說明 src/index.js 中的程式碼並沒有沒 babel 轉換,為什麼?請大家記住一句話: Babel 構建在外掛之上的。 預設情況下,Babel 不做任何處理,需要藉助外掛來完成語法的解析,轉換,輸出。

  1. 外掛

接下來我們一起嘗試使用 @babel/plugin-transform-arrow-functions 外掛來將箭頭函式轉換成函式宣告:

//.babelrc
{
"plugins": ["@babel/plugin-transform-arrow-functions"]
}
//  lib/index.js
const fn = function () {
console.log('xuemingli');
};

再次執行 npm run compile 後發現 lib/index.js 中已經是函式聲明瞭,說明 src/index.js 中的程式碼被 babel 轉換了。到此為止,大家可能想迫切的知道外掛究竟做了什麼事,別急,請大家慢慢往後看。

外掛的配置形式常見有兩種,分別是字串格式與陣列格式(見下面程式碼),並且可以傳遞引數,如果外掛名稱為 @babel/plugin-XXX,可以使用簡寫成@babel/XXX,例如 @babel/plugin-transform-arrow-functions 便可以簡寫成 @babel/transform-arrow-functions。請大家再記住一句話: 外掛的執行順序是從前往後

// .babelrc
/*
* 以下三個外掛的執行順序是:
@babel/proposal-class-properties ->
@babel/syntax-dynamic-import ->
@babel/plugin-transform-arrow-functions
*/

{
"plugins": [
// 同 "@babel/plugin-proposal-class-properties"
"@babel/proposal-class-properties",
// 同 ["@babel/plugin-syntax-dynamic-import"]
["@babel/syntax-dynamic-import"],
[
"@babel/plugin-transform-arrow-functions",
{
"loose": true
}
]
]
}

到此為止,我們瞭解了外掛的使用,對於特定的 ES6+ 語法,我們需要使用特定的外掛將其轉換為 ES5 中的程式碼,設想一下,如果我們的專案中使用了大量的 ES6+ 語法,我們是不是需要一個個的配置相應的外掛呢?當然不需要,那如何解決呢?有請 「預設」出場。

  1. 預設

預設是一組外掛的集合。與外掛類似,預設的配置形式也是字串和陣列兩種(見下面程式碼),預設也可以將 @babel/preset-XXX 簡寫為 @babel/XXX 。 預設的執行順序是從後往前,並且外掛在預設之前執行

我們常見的預設有以下幾種:

  • @babel/preset-env: 可以無視瀏覽器環境的差異而盡情地使用 ES6+ 新語法和新特性;

    • 注:語法和特性不是一回事,語法上的迭代是讓我們書寫程式碼更加簡單和方便,如展開運算子、類,結構等,因此這些語法稱為語法糖;特性上的迭代是為了擴充套件語言的能力,如 Map、Promise 等,事實上,Babel 對新語法和新特性的處理也是不一樣的,對於新語法,Babel 通過外掛直接轉換,而對於新特性,Babel 還需要藉助 polyfill 來處理和轉換。

  • @babe/preset-react: 可以書寫 JSX 語法,將 JSX 語法轉換為 JS 語法;

  • @babel/preset-typescript:可以使用 TypeScript 編寫程式,將 TS 轉換為 JS;

    • 注:該預設只是將 TS 轉為 JS,不做任何型別檢查

  • @babel/preset-flow:可以使用 Flow 來控制型別,將 Flow 轉換為 JS;

預設使用示例如下:

// .babelrc
/*
* 以下配置中,外掛比預設先執行
* 預設的執行順序為:
@babel/preset-react ->
@babel/preset-typescript ->
@babel/preset-env
*/

{
"plugins": [
// 同 "@babel/plugin-proposal-class-properties"
"@babel/proposal-class-properties",
// 同 ["@babel/plugin-syntax-dynamic-import"]
["@babel/syntax-dynamic-import"],
[
"@babel/plugin-transform-arrow-functions",
{
"loose": true
}
]
],
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": {
"version": 3,
"proposals": true // 使用尚在提議階段特性的 polyfill
}
}
],
"@babel/preset-typescript",
// 同 @babel/preset-react
"@babel/react"
]
}

對於 @babel/preset-env ,我們通常需要設定目標瀏覽器環境,可以在根目錄下的 .browserslistrc 檔案中設定,也可以在該預設的引數選項中通過 targets(優先順序最高) 或者在 package.json 中通過 browserslist 設定。如果我們不設定的話,該預設預設會將所有的 ES6+ 的新語法( 注意:這裡我說的只是新語法,不包含新特性 )全部做轉換,否則,該預設只會對目標瀏覽器環境不相容的新語法做轉換。我推薦設定目標瀏覽器環境,這樣在中大型專案中可以明顯縮小編譯後的程式碼體積,因為有些新語法的轉換需要引入一些額外定義的 helper 函式的,比如 class。

目標瀏覽器配置示例如下:

// .browserslistrc
> 0.25%
not dead
// .babelrc
{
"presets": [
[
"@babel/preset-env",
{
"targets": "> 0.25%, not dead"
}
]
]
}
// package.json
{
"name": "my-package",
"version": "1.0.0",
// ...省略其他配置
"browserslist": "> 0.25%, not dead"
}

設定不同目標瀏覽器環境,編譯情況示例如下:

src/index.js 中的是原始碼,lib/index.js 是轉換後的目的碼:

// src/index.js
// 新語法
const fn = () => {
console.log('xuemingli');
};
const arr = ["name", "age"];
const [a, b] = arr;
const arr2 = [...arr, "school"];
const arr3 = [["key1", "value1"], ["key2", "value2"]]
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayName() {
console.log(this.name);
}
}
const person = new Person('xuemingli', '26');
person.sayName();

// 新特性
const isHas = [1,2,3].includes(2);
const resArr = [1,[2,3]].flat();
const p = new Promise((resolve, reject) => {
resolve(100);
});
const map = new Map(arr3);
const value1 = map.get('key1');
const arr4 = Object.keys({ name: 'xuemingli', age: 26 })

結論 1:通過如下轉換後的目的碼可以驗證:如果不設定瀏覽器環境的話,@babel/preset-env 會將新語法全部轉換,並且對於一些特殊的新語法,如 class,還額外定義了一些 helper 函式,而新特性並沒有做任何轉換。

/*
lib/index.js
不設定目標瀏覽器環境
*/

"use strict";
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
// 新語法
var fn = function fn() {
console.log('xuemingli');
};
var arr = ["name", "age"];
var a = arr[0],
b = arr[1];
var arr2 = [].concat(arr, ["school"]);
var arr3 = [["key1", "value1"], ["key2", "value2"]];
var Person = /*#__PURE__*/function () {
function Person(name, age) {
_classCallCheck(this, Person);
this.name = name;
this.age = age;
}
_createClass(Person, [{
key: "sayName",
value: function sayName() {
console.log(this.name);
}
}]);
return Person;
}();
var person = new Person('xuemingli', '26');
person.sayName();

// 新特性
var isHas = [1, 2, 3].includes(2);
var resArr = [1, [2, 3]].flat();
var p = new Promise(function (resolve, reject) {
resolve(100);
});
var map = new Map(arr3);
var value1 = map.get('key1');
var arr4 = Object.keys({
name: 'xuemingli',
age: 26
});

結論 2:通過如下轉換後的目的碼可以驗證:如果設定了目標瀏覽器環境,@babel/preset-env 只會對目標瀏覽器環境不相容的新語法做轉換。如程式碼所示,當前目標瀏覽器環境下,該預設只轉換了「結構賦值」語法,而其他新語法在已經被瀏覽器實現了,便不需要轉換了。

/*
lib/index.js
目標瀏覽器環境設定為 last 53 Chrome versions
*/

"use strict";
// 新語法
const fn = () => {
console.log('xuemingli');
};
const arr = ["name", "age"];
const a = arr[0],
b = arr[1];
const arr2 = [...arr, "school"];
const arr3 = [["key1", "value1"], ["key2", "value2"]];
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayName() {
console.log(this.name);
}
}
const person = new Person('xuemingli', '26');
person.sayName();

// 新特性
const isHas = [1, 2, 3].includes(2);
const resArr = [1, [2, 3]].flat();
const p = new Promise((resolve, reject) => {
resolve(100);
});
const map = new Map(arr3);
const value1 = map.get('key1');
const arr4 = Object.keys({
name: 'xuemingli',
age: 26
});

對於新特性,@babel/preset-env 能否轉換呢?答案當然是能的。但是需要通過 useBuiltIns 這個引數選項實現,值需要設定為 usage,這樣的話,只會轉換我們使用到的新語法和新特性( 注意:這裡既包括新語法也包括新特性 ),能夠有效減小編譯後的包體積,並且還要設定 corejs: { version: 3, proposals } 選項,因為轉換新特性需要用到 polyfill,而 corejs 就是一個 polyfill 包。如果不顯示指定 corejs 的版本的話,預設使用的是 version 2 ,而 version 2 已經停更,諸如一些更新的特性的 polyfill 只會更行與 version 3 裡,如 Array.prototype.flat()。

示例如下:

// .babelrc
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": {
"version": 3,
"proposals": true // 使用尚在提議階段特性的 polyfill
}
}
]
]
// lib/index.js
// 從 corejs 這個包裡引入了 polyfill 並對新特性做了轉換
"use strict";
require("core-js/modules/es.object.define-property.js");
require("core-js/modules/es.array.concat.js");
require("core-js/modules/es.array.includes.js");
require("core-js/modules/es.array.flat.js");
require("core-js/modules/es.array.unscopables.flat.js");
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.promise.js");
require("core-js/modules/es.array.iterator.js");
require("core-js/modules/es.map.js");
require("core-js/modules/es.string.iterator.js");
require("core-js/modules/esnext.map.delete-all.js");
require("core-js/modules/esnext.map.every.js");
require("core-js/modules/esnext.map.filter.js");
require("core-js/modules/esnext.map.find.js");
require("core-js/modules/esnext.map.find-key.js");
require("core-js/modules/esnext.map.includes.js");
require("core-js/modules/esnext.map.key-of.js");
require("core-js/modules/esnext.map.map-keys.js");
require("core-js/modules/esnext.map.map-values.js");
require("core-js/modules/esnext.map.merge.js");
require("core-js/modules/esnext.map.reduce.js");
require("core-js/modules/esnext.map.some.js");
require("core-js/modules/esnext.map.update.js");
require("core-js/modules/web.dom-collections.iterator.js");
require("core-js/modules/es.object.keys.js");
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
// 新語法
var fn = function fn() {
console.log('xuemingli');
};
var arr = ["name", "age"];
var a = arr[0],
b = arr[1];
var arr2 = [].concat(arr, ["school"]);
var arr3 = [["key1", "value1"], ["key2", "value2"]];
var Person = /*#__PURE__*/function () {
function Person(name, age) {
_classCallCheck(this, Person);

this.name = name;
this.age = age;
}
_createClass(Person, [{
key: "sayName",
value: function sayName() {
console.log(this.name);
}
}]);
return Person;
}();
var person = new Person('xuemingli', '26');
person.sayName();

// 新特性
var isHas = [1, 2, 3].includes(2);
var resArr = [1, [2, 3]].flat();
var p = new Promise(function (resolve, reject) {
resolve(100);
});
var map = new Map(arr3);
var value1 = map.get('key1');
var arr4 = Object.keys({
name: 'xuemingli',
age: 26
});

以下示例演示了使用 @babel/preset-react 、@babel/preset-typescript 以及 @babel/preset-env 來轉換 src/index.tsx 中的原始碼,lib/index.js 中的是轉換後的目的碼:

// .babelrc
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": {
"version": 3,
"proposals": true // 使用尚在提議階段特性的 polyfill
}
}
],
"@babel/preset-typescript", "@babel/react"
]
// src/index.tsx
import React, { useState } from 'react';
const App: React.FC = () => {
const [value, setValue] = useState(0);
const fn = (): void => {
setValue(value => value++);
};
const arr: string[] = ["name", "age"];
const [a, b] = arr;
const arr2: string[] = [...arr, "school"];
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
sayName() {
console.log(this.name);
}
}
const person = new Person('xuemingli', 26);
person.sayName();

// 新特性
const isHas: boolean = [1,2,3].includes(2);
const resArr: number[] = [1,[2,3]].flat();
const p: Promise<number> = new Promise((resolve, reject) => {
resolve(100);
});
const map: Map<string, string> = new Map();
map.set('key1', 'value1');
const arr4: string[] = Object.keys({ name: 'xuemingli', age: 26 })

return (
<div>
<div>{value}</div>
<button onClick={fn}>+</button>
</div>

)
};

export default App;
// lib/index.js
"use strict";
function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
require("core-js/modules/es.symbol.js");
require("core-js/modules/es.symbol.description.js");
require("core-js/modules/es.symbol.iterator.js");
require("core-js/modules/es.array.slice.js");
require("core-js/modules/es.array.from.js");
require("core-js/modules/es.regexp.exec.js");
require("core-js/modules/es.object.define-property.js");
require("core-js/modules/es.weak-map.js");
require("core-js/modules/esnext.weak-map.delete-all.js");
require("core-js/modules/es.object.get-own-property-descriptor.js");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
require("core-js/modules/es.array.concat.js");
require("core-js/modules/es.array.includes.js");
require("core-js/modules/es.array.flat.js");
require("core-js/modules/es.array.unscopables.flat.js");
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.promise.js");
require("core-js/modules/es.array.iterator.js");
require("core-js/modules/es.map.js");
require("core-js/modules/es.string.iterator.js");
require("core-js/modules/esnext.map.delete-all.js");
require("core-js/modules/esnext.map.every.js");
require("core-js/modules/esnext.map.filter.js");
require("core-js/modules/esnext.map.find.js");
require("core-js/modules/esnext.map.find-key.js");
require("core-js/modules/esnext.map.includes.js");
require("core-js/modules/esnext.map.key-of.js");
require("core-js/modules/esnext.map.map-keys.js");
require("core-js/modules/esnext.map.map-values.js");
require("core-js/modules/esnext.map.merge.js");
require("core-js/modules/esnext.map.reduce.js");
require("core-js/modules/esnext.map.some.js");
require("core-js/modules/esnext.map.update.js");
require("core-js/modules/web.dom-collections.iterator.js");
require("core-js/modules/es.object.keys.js");

var _react = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]( "Symbol.iterator") method."); }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
var App = function App() {
var _useState = (0, _react.useState)(0),
_useState2 = _slicedToArray(_useState, 2),
value = _useState2[0],
setValue = _useState2[1];
var fn = function fn() {
setValue(function (value) {
return value++;
});
};
var arr = ["name", "age"];
var a = arr[0],
b = arr[1];
var arr2 = [].concat(arr, ["school"]);
var Person = /*#__PURE__*/function () {
function Person(name, age) {
_classCallCheck(this, Person);

this.name = name;
this.age = age;
}
_createClass(Person, [{
key: "sayName",
value: function sayName() {
console.log(this.name);
}
}]);
return Person;
}();
var person = new Person('xuemingli', 26);
person.sayName();

// 新特性
var isHas = [1, 2, 3].includes(2);
var resArr = [1, [2, 3]].flat();
var p = new Promise(function (resolve, reject) {
resolve(100);
});
var map = new Map();
map.set('key1', 'value1');
var arr4 = Object.keys({
name: 'xuemingli',
age: 26
});
return /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("div", null, value), /*#__PURE__*/_react.default.createElement("button", {
onClick: fn
}, "+"));
};
var _default = App;
exports.default = _default;

到此為止,我們瞭解了預設的概念和使用方式。雖然 @babel/env 可以幫我們做新語法和新特性的按需轉換,但是依然存在 2 個問題:

  • 從 corejs 引入的 polyfill 是全域性範圍的,不是模組作用域返回的,可能存在汙染全域性變數的風險,如上例中的 require("core-js/modules/es.promise.js")
  • 對於某些新語法,如 class,會在編譯後的檔案中注入很多 helper 函式宣告,而不是從某個地方 require 進來的函式引用,從而增大編譯後的包體積;

能不能解決如上兩個問題呢?當然能,有請 runtime 出場。

  1. runtime

runtime 是 babel7 提出來的概念,旨在解決如上提出的效能問題的。接下來我們實踐一下 @babel/plugin-transform-runtime 外掛配合 @babel/preset-env 使用,示例如下:

npm install --save-dev @babel/plugin-transform-runtime
// @babel/runtime 是要安裝到生產依賴的,因為新特性的編譯需要從這個包裡引用 polyfill
// 不錯,它就是一個封裝了 corejs 的 polyfill 包
npm install --save @babel/runtime
// .babelrc
{
"presets": [
"@babel/env"
],
"plugins": [
[
"@babel/plugin-transform-runtime",{
"corejs": 3
}
]
],
}
// src/index.js
// 新語法
const fn = () => {
console.log('xuemingli');
};
const arr = ["name", "age"];
const [a, b] = arr;
const arr2 = [...arr, "school"];
const arr3 = [["key1", "value1"], ["key2", "value2"]]
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayName() {
console.log(this.name);
}
}
const person = new Person('xuemingli', '26');
person.sayName();

// 新特性
const isHas = [1,2,3].includes(2);
const resArr = [1,[2,3]].flat();
const p = new Promise((resolve, reject) => {
resolve(100);
});
const map = new Map(arr3);
const value1 = map.get('key1');
const arr4 = Object.keys({ name: 'xuemingli', age: 26 })

npm run compile 編譯後,可以明顯看到,引入的 polyfill 不再是全域性範圍內的了,而是模組作用域範圍內的;並且不再是往編譯檔案中直接注入 helper 函數了,而是通過引用的方式,既解決了全域性變數汙染的問題,又減小了編譯後包的體積。

// lib/index.js
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/createClass"));
var _concat = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/concat"));
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));
var _flat = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/flat"));
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var _map = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/map"));
var _keys = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/object/keys"));
var _context, _context2, _context3;
// 新語法
var fn = function fn() {
console.log('xuemingli');
};
var arr = ["name", "age"];
var a = arr[0],
b = arr[1];
var arr2 = (0, _concat.default)(_context = []).call(_context, arr, ["school"]);
var arr3 = [["key1", "value1"], ["key2", "value2"]];
var Person = /*#__PURE__*/function () {
function Person(name, age) {
(0, _classCallCheck2.default)(this, Person);
this.name = name;
this.age = age;
}
(0, _createClass2.default)(Person, [{
key: "sayName",
value: function sayName() {
console.log(this.name);
}
}]);
return Person;
}();
var person = new Person('xuemingli', '26');
person.sayName();

// 新特性
var isHas = (0, _includes.default)(_context2 = [1, 2, 3]).call(_context2, 2);
var resArr = (0, _flat.default)(_context3 = [1, [2, 3]]).call(_context3);
var p = new _promise.default(function (resolve, reject) {
resolve(100);
});
var map = new _map.default(arr3);
var value1 = map.get('key1');
var arr4 = (0, _keys.default)({
name: 'xuemingli',
age: 26
});
  1. 結合 webpack

在 webpack 中,通過 babel-loader 的方式來接入 babel 的能力,babel 配置檔案與單獨使用 babel 時相同。在 webpack 中使用 babel 時建議開啟 babel 的快取能力,即 cacheDirectory,統計顯示,在大型專案中,構建速度可以提升 1 倍,webpack 的配置檔案示例如下:

// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.[j,t]sx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader?cacheDirectory'
}
}
]
},
};

到此為止,我們已經瞭解了 babel 的使用,但是 babel 的執行原理是怎麼樣的呢?請大家往下看。

三. 原理

  1. 簡述

Babel 的執行原理可以通過以下這張圖來概括。整體來看,可以分為三個過程,分別是:1. 解析,2. 轉換,3. 生成。而解析過程又可分為詞法解析和語法解析兩個過程。

我先以 箭頭函式 轉換成 函式宣告 為例,大致描述下每一個過程,然後再通過具體的程式碼來演示每個過程做了什麼事。

  • 解析:將箭頭函式原始碼字串解析成箭頭函式對應的抽象語法樹,如圖中紅色框部分。

    /*
    const fn = () => { console.log('xuemingli'); }; 經過詞法分析得到如下Tokens
    */

    [
    { type: 'Keyword', value: 'const' },
    { type: 'Identifier', value: 'fn' },
    { type: 'Punctuator', value: '=' },
    { type: 'Punctuator', value: '(' },
    { type: 'Punctuator', value: ')' },
    { type: 'Punctuator', value: '=>' },
    { type: 'Punctuator', value: '{' },
    { type: 'Identifier', value: 'console' },
    { type: 'Punctuator', value: '.' },
    { type: 'Identifier', value: 'log' },
    { type: 'Punctuator', value: '(' },
    { type: 'String', value: "'xuemingli'" },
    { type: 'Punctuator', value: ')' },
    { type: 'Punctuator', value: ';' },
    { type: 'Punctuator', value: '}' },
    { type: 'Punctuator', value: ';' }
    ]
    • 語法分析:將詞法分析得到的 Tokens 解析成箭頭函式對應的 AST(抽象語法樹)。從 Tokens 提供的資訊來分析出程式碼之間的邏輯關係,這種邏輯關係抽象成樹狀結構,就叫做 AST,對應到資料型別來說,它就是一個 JSON 物件。大家可以在 AST 解析器 [3] 這個工具中的 JSON Tab 來檢視,由於太大,我就不貼在這裡了,下圖展示了 AST 樹狀結構的樣子。

    • 詞法分析: 將箭頭函式原始碼字串分割成 Tokens。 Tokens 是由 JS 中的識別符號、運算子、括號、數字以及字串等等獨立且有意義的最小單元組成的集合,對應到資料型別來說,它就是一個數組,陣列項是包含 type 和 value 這兩個屬性的物件,如下所示。

  • 轉換:通過 @babel/transform-arrow-functions 外掛操作(包括增、刪、改)箭頭函式對應的抽象語法樹上的節點得到函式宣告對應的抽象語法樹,如圖中藍色框部分。在這裡就能回答 「外掛做了什麼事?」這個問題了,所有的 babel 外掛都是在 轉換 這個過程中起作用的,都是操作原始碼對應的抽象語法樹上的節點來得到目的碼對應的抽象語法樹。我們可以簡單的把外掛視為一個 visitor,可以 visit 以及 operate 抽象語法樹上的任意一個節點。

  • 生成:將函式宣告對應的抽象語法樹按照 JS 語法規則,拼接成函式宣告程式碼字串,如圖中黃色框部分。

  1. 程式碼實現

接下來我們通過實現一個最簡易的編譯器來將 (add 2 (subtract 4 2)) 這種語法轉換成我們熟悉的 add(2, subtract(4, 2))這種語法。最終我們提供如下方法:

// 詞法分析器
function tokenizer(sourceCode) {
const tokens = [];
...
return tokens;
}
// 語法解析器
function parser(tokens) {
const ast = {};
...
return ast;
}
// 遍歷器
function traverse(ast, visitor) {
/*
將原始碼對應的ast通過visitor的遍歷和操作
得到目的碼對應的newAst
*/

}
// 轉換器
function transformer(ast) {
const newAst = {};
traverse(ast, visitor);
return newAst;
}
// 生成器
function codeGenerator(newAst) {
let targetCode = '';
...
return targetCode;
}
// 編譯器
function compiler(sourceCode) {
const tokens = tokenizer(sourceCode);
const ast = parser(tokens);
const newAst = transformer(ast);
const targetCode = codeGenerator(newAst);
return targetCode;
}
module.exports = {
tokenizer,
parser,
traverse,
transformer,
codeGenerator,
compiler,
};

事實上,我們實現的這個簡易編譯器的功能和組織結構與 Babel 生態系統中的功能和組織結構是一一對應的,如下圖。

2.1 解析

2.1.1 詞法分析

將 (add 2 (subtract 4 2)) 程式碼字串分割成如下 Tokens:

 [
{ type: 'paren', value: '(' },
{ type: 'name', value: 'add' },
{ type: 'number', value: '2' },
{ type: 'paren', value: '(' },
{ type: 'name', value: 'subtract' },
{ type: 'number', value: '4' },
{ type: 'number', value: '2' },
{ type: 'paren', value: ')' },
{ type: 'paren', value: ')' },
]

具體程式碼實現如下,並添加了較為詳細的註釋:

// tokenizer.js
function tokenizer(sourceCode) {
// 宣告一個遊標用來記錄遍歷的位置
let current = 0;
let tokens = [];
// 通過一個迴圈,不斷的遍歷原始碼字串
while (current < sourceCode.length) {
let char = sourceCode[current];
if (char === '(') {
tokens.push({
type: 'paren',
value: '(',
});
current++;
continue;
}
if (char === ')') {
tokens.push({
type: 'paren',
value: ')',
});
current++;
continue;
}
// 由於空白字元對我們來說意義不大,故不放到Tokens中,直接跳過
let WHITESPACE = /\s/;
if (WHITESPACE.test(char)) {
current++;
continue;
}
// 因為數字可以是任意數量的字元,我們想要將整個數字序列捕獲為一個標記。
//(如 123 456)
let NUMBERS = /[0-9]/;
if (NUMBERS.test(char)) {
let value = '';
while (NUMBERS.test(char)) {
value += char;
char = sourceCode[++current];
}
tokens.push({ type: 'number', value });
continue;
}
// 這裡是想要獲取運算函式名稱,如 add 和 subtract
let LETTERS = /[a-z]/i;
if (LETTERS.test(char)) {
let value = '';
while (LETTERS.test(char)) {
value += char;
char = sourceCode[++current];
}
tokens.push({ type: 'name', value });
continue;
}
throw new TypeError('I dont know what this character is: ' + char);
}
return tokens;
}

2.1.2 語法分析

將 Tokens 解析成 (add 2 (subtract 4 2)) 對應的 AST 的物件形式,如下所示:

{
type: 'Program',
body: [{
type: 'CallExpression',
name: 'add',
params: [{
type: 'NumberLiteral',
value: '2'
}, {
type: 'CallExpression',
name: 'subtract',
params: [{
type: 'NumberLiteral',
value: '4'
}, {
type: 'NumberLiteral',
value: '2'
}]
}]
}]
}

具體程式碼實現如下,並添加了較為詳細的註釋:

// parse.js
function parser(tokens) {
let current = 0;
// 這裡定義了一個被遞迴的函式walk,每次遍歷Tokens時都會呼叫
function walk() {
let token = tokens[current];
// 如果遍歷到 number 就返回如下物件
if (token.type === 'number') {
current++;
return {
type: 'NumberLiteral',
value: token.value,
};
}
// 如果遍歷到'(',跳過並取下一個Token,並返回如下物件,因為'('之後肯定是運算函式名稱
// 如 add 和 subtract
if (
token.type === 'paren' &&
token.value === '('
) {
token = tokens[++current];
let node = {
type: 'CallExpression',
name: token.value,
params: [],
};
token = tokens[++current];
// 如果沒有遍歷到')',則遞迴walk,知道遍歷到')'返回一個
// {type: 'CallExpression', name: token.value, params: [xxx]}
while (
(token.type !== 'paren') ||
(token.type === 'paren' && token.value !== ')')
) {
node.params.push(walk());
token = tokens[current];
}
current++;
return node;
}
throw new TypeError(token.type);
}
let ast = {
type: 'Program',
body: [],
};
// 遍歷Tokens並呼叫walk
while (current < tokens.length) {
ast.body.push(walk());
}
return ast;
}

2.2 轉換

2.2.1 遍歷 AST 節點

現在我們已經通過 parse 方法得到了 (add 2 (subtract 4 2)) 對應的 AST 了,這就輪到外掛起作用了,外掛可以遍歷並操作 AST 上的任意節點,將節點修改成我們想要的樣子。最終我們想得到 add(2, subtract(4, 2)) 對應的 AST 物件,如下所示:

{
type: 'Program',
body: [{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'add'
},
arguments: [{
type: 'NumberLiteral',
value: '2'
}, {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'subtract'
},
arguments: [{
type: 'NumberLiteral',
value: '4'
}, {
type: 'NumberLiteral',
value: '2'
}]
}
}
}]
}

我這裡用物件宣告模擬了一個外掛,可以視為 Babel 系統概念中的 visitor ,可以看到,外掛最終對外暴露的就是一個包含處理各種節點邏輯的物件,只要匹配上節點,就走處理節點的邏輯,將其處理成我們希望的樣子。如下所示:

{
NumberLiteral: {
enter(node, parent) {
parent._context.push({
type: 'NumberLiteral',
value: node.value,
});
}
},
// 將CallExpression節點處理成我們希望得到的節點樣子
CallExpression: {
enter(node, parent) {
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name,
},
arguments: [],
};
node._context = expression.arguments;
if (parent.type !== 'CallExpression') {
expression = {
type: 'ExpressionStatement',
expression: expression,
};
}
parent._context.push(expression);
}
}
}

具體程式碼實現如下,並添加了較為詳細的註釋,traverse 方法需要結合 transformer 方法一起看,會更加清晰:

// transformer.js
function traverse(ast, visitor) {
// 用來處理節點集合,即陣列
function traverseArray(array, parent) {
array.forEach(child => {
traverseNode(child, parent);
});
}
// 用來處理單個節點,即物件
function traverseNode(node, parent) {
let methods = visitor[node.type];
if (methods && methods.enter) {
methods.enter(node, parent);
}
switch (node.type) {
case 'Program':
traverseArray(node.body, node);
break;
case 'CallExpression':
traverseArray(node.params, node);
break;
case 'NumberLiteral':
break;
default:
throw new TypeError(node.type);
}
if (methods && methods.exit) {
methods.exit(node, parent);
}
}
traverseNode(ast, null);
}

function transformer(ast) {
let newAst = {
type: 'Program',
body: [],
};
const visitor = {
NumberLiteral: {
enter(node, parent) {
parent._context.push({
type: 'NumberLiteral',
value: node.value,
});
},
},
CallExpression: {
enter(node, parent) {
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name,
},
arguments: [],
};
node._context = expression.arguments;
if (parent.type !== 'CallExpression') {
expression = {
type: 'ExpressionStatement',
expression: expression,
};
}
parent._context.push(expression);
},
}
};
ast._context = newAst.body;
traverser(ast, visitor);
return newAst;
}

2.3 生成

生成的過程比較簡單,就是將 add(2, subtract(4, 2)) 對應的 AST 物件按照拼接成 add(2, subtract(4, 2)) 程式碼字串便可,具體程式碼如下:

// generator.js
function codeGenerator(node) {
switch (node.type) {
// 遍歷到'Program'節點,拼接個換行符即可
case 'Program':
return node.body.map(codeGenerator)
.join('\n');
// 遍歷到'ExpressionStatement'節點,又將expression節點作為引數遞迴codeGenerator
// 並拼接個 ';'
case 'ExpressionStatement':
return (
codeGenerator(node.expression) +
';'
);
// 遍歷到'CallExpression'節點,拼接運算表示式,
// 如 (4, 2)
case 'CallExpression':
return (
codeGenerator(node.callee) +
'(' +
node.arguments.map(codeGenerator)
.join(', ') +
')'
);
// 遍歷到'Identifier'節點,拼接運算表示式,
// 如 subtract add
case 'Identifier':
return node.name;
case 'NumberLiteral':
return node.value;
default:
throw new TypeError(node.type);
}
}

到此為止,我們實現了一個簡易的 babel 編譯器,github 地址

四. 寫到最後

限於篇幅,自定義 Babel 外掛的部分將於下篇文章中帶著大家手摸手實現,敬請期待。本文更偏向於科普性質,感興趣的內容歡迎大家自行搜尋瞭解和學習。本文所有內容皆源於官網學習以及個人理解與實踐,如有疑問,請回復討論。希望通過本文,你能對 Babel 有一個基本的瞭解,能對你的學習和工作有所幫助。

五. 參考材料

  1. https://www.babeljs.cn/

  1. https://juejin.cn/post/6844904008679686152#heading-17

參考資料

[1]

ESNext: https://juejin.cn/post/7028417636811669534

[2]

巴別塔: https://baike.baidu.com/item/%E5%B7%B4%E5%88%AB%E5%A1%94/67557

[3]

AST解析器: https://astexplorer.net/