Babel文件沒那麼難讀,帶你一步步瞭解Babel主要的幾個@babel/x-x包

limingcan發表於2023-01-19

前言

相信很多人對Babel都瞭解,但是差不多是一知半解,很多地方估計是懵懵懂懂的感覺。配置的時候,在網上搜尋,然後複製貼上,能跑通就好,但偶爾會出現一些稀奇古怪的問題。後面想深入瞭解學習Babel,又發現官網讀起來晦澀難懂,或者照著官網敲Demo,又發現實際結果不是官網說的那樣(其實是因為我們安裝的依賴版本有問題,安裝成最新的了,所以輸出的效果跟官網的不一樣)。這樣就造成我們對Babel更加的困惑。

因為Babel內容實在太多了,所以這篇文章不講原理,也不講怎麼配置(配置後續會出專門的文章說),但會帶著大家一起理解讓人覺得“晦澀難懂”的官網,然後梳理、瞭解,我們平時接觸Babel用到的主要幾個包,搞清楚Babel是什麼、作用又是什麼。

為了讓大家更好的感受Babel對我們日常專案的作用,有些例子會結合Webpack,畢竟我們平時的專案,基本都會透過Webpack等打包工具跟Babel相結合輸出最後的包,然後在瀏覽器中執行。

章節中的案例,程式碼都放到Github上了,建議大家邊閱讀,邊跟著案例看。如果大家覺得有幫助到,歡迎StarFork學習。

備註:

  • 當前@babel/core最新版本是:7.20.12
  • 當前@babel/preset-env最新版本是:7.20.2

Babel

官網解釋:Babel是一個工具鏈,主要用於將採用ECMAScript 2015+語法編寫的程式碼轉換為向後相容的 JavaScript語法,以便能夠執行在當前和舊版本的瀏覽器或其他環境中。

我們可以這麼理解,Babel就是一個工具。它是一個可以將ES6+等新特性,轉換成低版本瀏覽器或其他環境能支援並正常執行的一個工具。

結構

很多人以為Babel只有pluginspresets等幾個配置。其實不止,我們看看Babel配置檔案大致架構:

// babel.config.js
module.exports = {
    ...,
    envName: "development",
    plugins: [],
    presets: [],
    passPerPreset: false,
    targets: {},
    browserslistConfigFile: true,
    browserslistEnv: undefined,
    inputSourceMap: true
    ...
}

我們一般主要用到的就是pluginspresets這兩個

功能

從大體上看,Babel提供以下兩個功能組成:

  • 編譯ES6+最新語法(letclass() => {}等)
  • 實現舊版本瀏覽器不支援的ES6+APIPromiseSymbolArray.prototype.includes等)

參考文章:

@babel/core

core可以看出,它是Babel實現編譯的核心。所以我們如果要使用Babel@babel/core這個包一定是必不可少的。另外我們平常說的Babel 6Babel 7指的就是@babele/core的版本

參考文章:@babel/core

@bable/cli

官網解釋:Babel自帶了一個內建的CLI命令列工具,可透過命令列編譯檔案

簡單地說就是,讓我們可以在終端裡使用命令來編譯(這樣可以更好的除錯列印資訊):

npx babel index.js

安裝的話,我們最好安裝到我們專案的本地目錄下,儘量不要安裝到全域性(影響全域性的東西,都很可怕)

參考文章:@babel/cli

@bable/preset-env

官網解釋:@babel/preset-env是一個智慧預設,它允許您使用最新的JavaScript,而無需微觀管理目標環境需要哪些語法轉換(以及可選的瀏覽器polyfill)。這既讓你的生活更輕鬆,也讓JavaScript包更小!

理解

@bable/preset-env這個名字,我們可以拆開兩部分來看,這樣方便理解:

  • preset預設
  • env環境

preset

Babel編譯ES6+語法,是透過一個個外掛plugin去實現的。每年都會有不同新的提案、新的語法,但我們不可能一個個外掛去配置,所以就有了preset這個東西。因此我們可以理解成preset就是一個語法外掛集合包,這樣我們只用安裝這一個包,不需要一個個配外掛,就可以很方便的編譯最新的語法了。

我們透過一個不用預設的案例 no-preset ,感受一下如果不用preset有多麻煩。

//  入口檔案 index.js
const senses = ['eye', 'nose', 'ear', 'mouth'];

const lMC = {
    senses,
    like: ['eat', 'drink', 'play', 'fun'],
    information: {
        sex: 'male',
        age: '18+'
    },
    play: (sport = 'badminton') => {
        console.log(`play ${sport}`);
    }
};

const { like, information } = lMC;

這段程式碼,我們用了幾個ES6新語法:

  • const宣告
  • 屬性的簡潔表示法
  • 箭頭函式
  • 函式預設值
  • 模板字串
  • 解構

如果不用preset我們Babel配置如下:

// Babel配置檔案 babel.config.js
const plugins = [
    '@babel/plugin-transform-arrow-functions',
    '@babel/plugin-transform-block-scoping',
    '@babel/plugin-transform-destructuring',
    '@babel/plugin-transform-parameters',
    '@babel/plugin-transform-shorthand-properties',
    '@babel/plugin-transform-template-literals'
];
module.exports = {plugins};

編譯後的檔案:

// 編譯後的檔案 compile.js
var senses = ['eye', 'nose', 'ear', 'mouth'];
var lMC = {
  senses: senses,
  like: ['eat', 'drink', 'play', 'fun'],
  information: {
    sex: 'male',
    age: '18+'
  },
  play: function () {
    var sport = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'badminton';
    console.log("play ".concat(sport));
  }
};
var like = lMC.like,
  information = lMC.information;

在不用preset的情況下,實現上述編譯的過程,我基本是用一個ES6新語法,我就要去查一個外掛,首先我不記得那麼多外掛,其次一個個外掛找真的很累。

ok,那我們再用一個使用了預設的案例 use-preset ,感受一下預設到底有多方便。
我們npm i @babel/preset-env -D,修改babel.config.js使用preset預設:

// 修改babel.config.js
const presets = [
    '@babel/preset-env'
];

module.exports = {presets};

編譯後的檔案:

// 編譯後的檔案 compile.js
"use strict";

var senses = ['eye', 'nose', 'ear', 'mouth'];
var lMC = {
  senses: senses,
  like: ['eat', 'drink', 'play', 'fun'],
  information: {
    sex: 'male',
    age: '18+'
  },
  play: function play() {
    var sport = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'badminton';
    console.log("play ".concat(sport));
  }
};
var like = lMC.like,
  information = lMC.information;

我們會發現,用preset(預設)方式輸出的程式碼,跟plugins(不用預設)方式輸出的程式碼是幾乎是一模一樣的。但是presetbabel.config.js更簡潔,我也不需要一個個外掛去找,也不需要安裝那麼多外掛,只用安裝@babel/preset-env這一個包,就可以很愉快的寫ES6+

env

env指的是環境。因為@babel/preset-env還有一個配置功能,我們可以透過配置我們程式碼執行的目標環境,來控制polyfill(一個提供低版本瀏覽器缺失的ES6+新特性的方法與實現的集合 ,後面會有更詳細的講解)的匯入跟語法編譯,從而使ES6+新的特性可以在我們想要的目標環境中順利執行。

備註:@babel/preset-env還有一個配置功能,本文不講配置,關於配置後續會有文章說明

功能

透過上面對presetenv的理解跟案例感受,我們能總結出@babel/preset-env主要提供以下功能:

  • 編譯ES6+語法(上述案例只使用了ES6+的語法,並沒有用ES6+API)
  • 它並不提供polyfill,但是可以透過配置我們程式碼執行的目標環境,從而控制polyfill的匯入跟語法編譯,使ES6+的新特性可以在我們想要的目標環境中順利執行

注意

我們先看看TC39提案分為幾個階段:

  • 階段0  (stage-0)——草根(Strawman):只是一個想法,可能是Babel外掛。
  • 第一階段(stage-1)——提案(Proposal):這是值得研究的。
  • 第二階段(stage-2)——草案(Draft):初步規範。
  • 第三階段(stage-3)——候選(Candidate):完整的規範和最初的瀏覽器實現。
  • 第四階段(stage-4)——完成(Finished):將被新增到下一年度的版本中。

再看看官網中這段話:

Note: @babel/preset-env won't include any JavaScript syntax proposals less than Stage 3 because at that stage in the TC39 process, it wouldn't be implemented by any browsers anyway. Those would need to be included manually.

大致意思是:

  • Babel 7以後,@bable/preset-env捨棄了Stage presets@babel/preset-stage-x)這種預設
  • @bable/preset-env只提供TC39大於stage-3的提案(即只包含stage-4階段),因此如果要用小於stage 4的提案語法,則必須先安裝再手動引入對應外掛

第一點相信大家都很好理解,我們來理解一下第二點是什麼意思。

意思是,如果我們想用一些小於stage-4階段的語法的話,光安裝@babel/preset-env這一個包是沒有用的,因為這個包裡只包含編譯stage-4的預設,所以我們就得安裝並配置相應的plugin去編譯。

在寫這篇文章的時候,有一個新的語法 do expressions ,它當前是處於stage-1階段的語法,用外掛@babel/plugin-proposal-do-expressions可以編譯這個語法。

官網解釋:do { .. } 表示式執行一個塊(其中有一個或多個語句),塊內的最終語句完成值成為 do 表示式的完成值。

我們藉助官網,整理成這個案例 compile-stage-1 來看看怎麼使用小於stage-4的語法。

我們先只用@babel/preset-env,看看能不能編譯do {...}這個語法。

// do expressions stage-1語法
let x = 100;
let y = 20;

let a = do {
    if (x > 10) {
        if (y > 20) {
            ("big x, big y");
        } else {
            ("big x, small y");
        }
    } else {
        if (y > 10) {
            ("small x, big y");
        } else {
            ("small x, small y");
        }
    }
};

Babel.config.js配置:

const presets = [
    '@babel/preset-env'
];

// const plugins = [
    // '@babel/plugin-proposal-do-expressions'
// ];

// module.exports = {plugins, presets};

module.exports = {presets};

我們會發現,終端會報錯:

大致意思是:@babel/preset-env當前未啟用對實驗語法doExpressions的支援(因為doExpressions當前是stage-1的語法,@babel/preset-env只包含必編譯stage-4的語法外掛),需要我們加入@babel/plugin-proposal-do-expressions外掛去編譯。

那我們npm i @babel/plugin-proposal-do-expressions -D,修改一下babel.config.js

const presets = [
    '@babel/preset-env'
];

const plugins = [
    '@babel/plugin-proposal-do-expressions'
];

module.exports = {plugins, presets};

// module.exports = {presets};

我們可以看到,可以正常輸出編譯後的檔案:

"use strict";

var x = 100;
var y = 20;
var a = x > 10 ? y > 20 ? "big x, big y" : "big x, small y" : y > 10 ? "small x, big y" : "small x, small y";

所以,當我們想使用小於stage-4階段的語法時,我們要先找到其對應的編譯外掛安裝,然後在plugins裡面配置就好了。

參考文章:

補充

有時我們也可能需要知道我們當前的preset(預設)包含了哪些外掛,那我們怎麼檢視當前@babel/preset-env包含了哪些預設呢?

我們可以透過檢視@babel/preset-env --> package.json --> dependencies裡面可以找到。我目前安裝的@babel/preset-env版本為7.20.2,它包含了以下預設:

// @babel/preset-env@7.20.2預設
"dependencies": {
    "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6",
    "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9",
    ...,
    "@babel/plugin-transform-sticky-regex": "^7.18.6",
    "@babel/plugin-transform-template-literals": "^7.18.9",
    "@babel/plugin-transform-typeof-symbol": "^7.18.9",
    "@babel/plugin-transform-unicode-escapes": "^7.18.10",
    "@babel/plugin-transform-unicode-regex": "^7.18.6",
  },

polyfill

功能

ES6+除了提供很多簡潔的語法(letclass() => {}等)外,還為我們提供了很多便捷的APIPromiseSymbolArray.prototype.includes等)。但舊版本瀏覽器是不支援這些API,而polyfill存放了這些API的方法與實現,所以它可以使得這些不支援的瀏覽器,支援這些API

理解

我們可以把所有這種存放了ES6+ API方法與實現的集合叫做polyfill,也就是我們經常說的墊片

polyfill也分很多種,像core-js是會提供舊版本瀏覽器缺失的所有API;還有一些只提供缺失API某塊,例如 promise-polyfillproxy-polyfill 等。

Babel配置polyfill的過程,就是實現舊版本瀏覽器對這些API支援的過程。

@babel/polyfill

上面我們解釋了polyfill是什麼,從包名@babel/polyfill就知道,它就是一個polyfill(其核心是依靠core-js@2.x.x實現)。雖然這個包已經被廢棄了,但我們還是稍微瞭解一下它。

官網解釋:
? 從Babel 7.4.0開始,這個包已經被棄用,轉而直接包含core-js/stable(用於polyfill ECMAScript功能)

使用:

import "core-js/stable";

初識

我們透過這個例子 know-babel-polyfill,來了解一下@babel/polyfill的組成。
know-babel-polyfill 什麼都沒安裝,只安裝了@bable/polyfill這個依賴,我們可以很清楚看到,@bable/polyfill由以下兩個包組成:

<img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8ff1acb6c6e2463aad5798f805f3efd8~tplv-k3u1fbpfcp-watermark.image?" alt="babel-polyfill.jpeg" width="100%" />

  • core-js版本為2
  • regenerator-runtime

我們來大致理解一下這兩包是什麼:

core-js

這個包就是我們上述polyfill模組所說的,裡面存放了很多ES6+ API的方法與實現。如果要在舊瀏覽用到PromiseSymbolArray.prototype.includes等方法時,這個包會為我們提供。它可以使那些不支援API的瀏覽器,支援這些API,它就是一種墊片。

特別注意:由上圖可知,@babel/polyfill是與2版本的core-js繫結的,2版本的core-js並不包含stable這個資料夾的。因此官網說的import "core-js/stable",實際上是要我們安裝core-js@3.x.x版本來代替@babel/polyfill,因為從3版本開始,才有stable這個資料夾

regenerator-runtime

我們的原始碼裡面使用了async function() {}等非同步函式,或者fuction* myGenerator() {}這種Generator函式的話,就會需要用到這個包來編譯。

總結

所以對於@babel/polyfill,我們有以下總結:

  • 這個包由core-js版本為2.x.x)與regenerator-runtime兩個包組成
  • 這個包在Babel 7.4.0以後就被廢棄了,所以在Babel 7.4.0以後,我們想讓一些不支援ES6+ API的瀏覽器支援這些API,就不應該安裝這個包,應該安裝core-js@3.x.x的包(不要安裝2.x.x的版本,已經不維護了,目前最新版本為3.x.x

參考文章:@babel/polyfill

core-js

概述

透過上面polyfill@babel/polyfill兩個模組,我們可以知道@babel/polyfill已經不再使用,而且@babel/polyfill實現的核心就是core-js,所以如果我們想要在舊瀏覽器用到PromiseSymbolArray.prototype.includes等方法時,我們直接安裝core-js@3.x.x這個包。

透過 官方的介紹,我們可以知道:

import '@babel/polyfill';

等同於

// core-js必須是3.x.x版本,因為2.x.x版本,不包含stable資料夾
import "core-js/stable";
import "regenerator-runtime/runtime";

Babel >= 7.18.0等同於

// core-js必須是3.x.x版本,因為2.x.x版本,不包含stable資料夾
// Babel >= 7.18.0後 不需要再 import "regenerator-runtime/runtime";
import "core-js/stable";

注意

我們針對不需要再import "regenerator-runtime/runtime"這塊,稍微解釋一下,加深一下我們對Babel跟官網文件的理解。

我們看官方這段話:

If you are compiling generators or async function to ES5, and you are using a version of @babel/core or @babel/plugin-transform-regenerator older than 7.18.0, you must also load the regenerator-runtime package

大家看這句話的時候可能有點疑惑,其實它的意思就是:

如果我們要把async function() {}等非同步函式,或者fuction* myGenerator() {}這種Generator函式編譯成ES5,並且@babel/core@babel/plugin-transform-regenerator小於7.18.0,我們就需要手動import "regenerator-runtime/runtime"這個包。

但在Babel 7.18.0或者@babel/plugin-transform-regenerator 7.18.0及其以後的版本,regenerator-runtime包裡面的內容會被內聯編譯到我們的程式碼中,所以我們只用引入import "core-js/stable"這一個包就可以了。

我們來用兩個例子結合Webpack打包出來,在瀏覽器執行,這樣更直觀的理解感受一下。

Babel < 7.18.0

我們用這個例子 import-regenerator-runtime 看看在Babel 7.18.0之前為什麼要手動引入regenerator-runtime這個包。

特別說明: 我們例子安裝Babel的版本為7.16.7@babel/plugin-transform-regenerator這個外掛必須手動安裝為小於7.18.0的版本(因為我們安裝依賴的時候,即使指定了依賴的版本,但依賴的依賴安裝時,可能會是最新的,這樣可能會看不出效果。所以為什麼有時我們對著官網敲Demo實際出來的結果不一樣,因為版本沒對上)。可以透過package-lock.json檢視各個依賴版本

ok,來看看我們的入口檔案(index.js):

// 先不引入regenerator-runtime/runtime

// import 'regenerator-runtime/runtime';
const sleep = async function() {
    setTimeout(() => console.log('get up'), 1000);
}
sleep();

接著我們打包(Webpack打包出來的檔案在dist/dist.js)在瀏覽器執行。正常情況下,瀏覽器應該會過一秒後輸出get up。但實際情況如下,我們會發現之前網友們經常出現的一個問題——regeneratorRuntime is not defined

說明缺失了regeneratorRuntime,我們再看看Babel編譯後的檔案(compile.js):

我們發現在全域性中,regeneratorRuntime根本沒有定義,所以才報了regeneratorRuntime is not defined的錯。

如果我們再手動引入一下import "regenerator-runtime/runtime"

import 'regenerator-runtime/runtime';

const sleep = async function() {
    setTimeout(() => console.log('get up'), 1000);
}
sleep();

此時瀏覽器輸出:

當我們手動引入以後,瀏覽器可以正常執行了。

這說明,在@babel/core@babel/plugin-transform-regenerator的版本小於7.18.0的時候,使用了非同步函式(async function() {}),或者Generator這種函式(fuction* myGenerator() {})的話,是需要我們手動引入regenerator-runtime這個包的,因為regenerator-runtime這個包會為我們提供regeneratorRuntime這個全域性物件

Babel >= 7.18.0

我們用這個例子 no-import-regenerator-runtime 看看在Babel 7.18.0之後為什麼不需要手動引入regenerator-runtime這個包。(@babel/core版本為7.20.12

ok,來看看我們的入口檔案,這時不再手動引入regenerator-runtime這個包:

const sleep = async function() {
    setTimeout(() => console.log('get up'), 1000);
}
sleep();

編譯出包以後在瀏覽器執行,得到跟上述手動引入regenerator-runtime這個包一模一樣的效果:

我們再看看Babel編譯後的檔案:

我們會發現,regenerator-runtime包裡的內容,會以區域性變數的方式內聯注入到我們的程式碼中,這樣我們就不需要全域性提供一個regeneratorRuntime物件了。

所以,在Babel >= 7.18.0以後,我們直接import "core-js/stable";就好

參考文章:

@babel/runtime

官方解釋:@babel/runtime是一個包含Babel模組化執行時助手的庫

Babel編譯的時候,會有一些輔助函式,這些函式就是ES6+一些語法糖的實現,我們用這個案例 helper-functions 看看輔助函式是什麼。

我們用Babel編譯一下class這個語法糖:

class People {
    constructor() {
    }
}
const person  = new Person();

編譯以後:

我們先看紅色框,它是Babel編譯後的程式碼。我們會發現,編譯以後生成很多函式,並且會以內聯的方式插入到我們的程式碼中,這些函式就是我們說的輔助函式

我們再看藍色框,它是@babel/runtime的內容,它在node_modules/@babel/runtime/helpers

我們最後來看看白色框,會發現Babel編譯後的輔助函式,都可以在@bable/runtime裡面找到,所以@babel/runtime存放了Babel輔助函式的一個集合包

參考文章:@babel/runtime

@babel/plugin-transform-runtime

官方解釋:一個外掛,可以重用Babel注入的幫助程式程式碼以節省程式碼大小

透過上面@babel/runtime模組的瞭解,我們知道當我們使用了一些ES6+的語法糖時,Babel會生成一些輔助函式來編譯這些語法糖,並以內聯的方式插入到程式碼中。

那如果我們有10個檔案都用到了語法糖,那這些輔助函式,是不是會生成10次,並內聯插入10次呢?我們用這個案例 no-use-transform-runtime 來感受一下。

我們定義了三個檔案,每個檔案都用了class這個語法糖。

// babel.config.js 配置檔案
const presets = [
    '@babel/preset-env'
];
module.exports = {presets};

// Animal.js 檔案
export default class Animal {
    constructor() {}
};

// Country.js 檔案
export default class Country {
    constructor() {}
};

// index.js 檔案
import Animal from "./class/Animal";
import Country from "./class/Country";

class People {
    constructor() {
    }
};

const lMC = new People();
const cat = new Animal();
const usa = new Country();

最後打包出來檔案:

看看紅色的框框,我們會發現實現的方法都是一樣的,所以在每個使用到class語法糖的檔案中,輔助函式都被生成並插入了一次,這些基本重複的程式碼,無疑是會大大增加我們的打包體積的。目前打包出來的體積是:6KB

為了解決上述的弊端,我們就得使用@babel/plugin-transform-runtime外掛。從@babel/runtime模組我們知道,它裡面存放了Babel輔助函式的集合,@babel/plugin-transform-runtime會將我們用到的輔助函式,從@babel/runtime中以require或者import的方式,引入到我們的檔案中,實現複用,從而減小我們最終輸出包的體積。

所以@babel/runtime@babel/plugin-transform-runtime兩者通常是配合一起使用。

備註:@babel/plugin-transform-runtime還有一個配置功能,本文不講配置,關於配置後續會有文章說明

我們用這個案例 use-transform-runtime 看看使用了@babel/plugin-transform-runtime外掛以後有什麼變化。

我們的案例程式碼跟上述一樣,只是在babel.config.js增加了@babel/plugin-transform-runtime配置

// babel.config.js 配置檔案
// 增加了@babel/plugin-transform-runtime 配置
const plugins = [
    '@babel/plugin-transform-runtime'
]
const presets = [
    '@babel/preset-env'
];
module.exports = {plugins, presets};

// Animal.js 檔案
export default class Animal {
    constructor() {}
};

// Country.js 檔案
export default class Country {
    constructor() {}
};

// index.js 檔案
import Animal from "./class/Animal";
import Country from "./class/Country";

class People {
    constructor() {
    }
};

const lMC = new People();
const cat = new Animal();
const usa = new Country();

編譯跟打包後的檔案:

我們會發現:

  • 輔助函式會以require引用的方式加到我們的程式碼中
  • 打包後,輔助函式只用了一次,而且不是插入三次,很好的實現了複用
  • 打包出來的體積也變成了3KB,很好的縮小了最後包的體積(不要小看縮小了3KB,只是因為我用最簡單的方式寫了ES6+語法,實際中我們專案肯定沒那麼簡單)

最後

因為Babel的知識體系實在太大了,所以我們應該先把Babel主要的幾個包弄清楚,再深入配置。關於Babel的配置,會後續再出文章。

我們平常專案中Babel用到的包,基本就是這篇文章中講解的幾個包,這篇文章算是十分詳細的介紹了這幾個包了。如果大家能把這幾個包弄得很清楚,Babel的大部分知識也瞭解的差不多了。

如果之前對Babel還有點懵懵的你,希望讀完這篇文章後,可以很好的理解Babel大致是個什麼東西,也能更清楚的看懂官網寫的內容。

中間有用到Webpack,我自己用Webpack5搭了個腳手架 webpack5-boilerplate,如果你也想了解Webpack的知識,也可以看看之前我寫的這篇文章—— webpack5最佳化的地方及搭建的一些體會

如果覺得真的有幫助到,歡迎點贊收藏;如果有異同點,歡迎在評論區討論

相關文章