初次相識Babel v7,多多指教

Babel 是一個 JavaScript 編譯器。

Babel 是一個工具鏈,主要用于將 ECMAScript 2015+ 版本的代碼轉(zhuǎn)換為向后兼容的 JavaScript 語法,以便能夠運行在當前和舊版本的瀏覽器或其他環(huán)境中。
通俗地講,Babel只是轉(zhuǎn)義新標準引入的語法,如 ES6中的箭頭函數(shù)、解構(gòu)等。而新標準中新增的方法、函數(shù)等就需要通過 Polyfill 在目標環(huán)境中添加這些缺失的特性來解決。

Babel的功能:

  • 語法轉(zhuǎn)換
  • 通過 Polyfill 方式在目標環(huán)境中添加缺失的特性,對應模塊@babel/polyfill
  • 源碼轉(zhuǎn)換

Babel` 編譯過程:

  • 解析:將代碼字符串解析成抽象語法樹。
  • 轉(zhuǎn)換:對抽象語法樹進行轉(zhuǎn)換操作。(本文重點)
  • 輸出:根據(jù)變換后的抽象語法樹再生成代碼字符串。

常見使用方式:

  • .babelrc/babel.config.jsonBabel配置文件)
  • babel-loader (webpack/rollup等)

初始化項目

  • 創(chuàng)建一個空目錄,執(zhí)行 npm init -y
  • 新建src/index.js
    const fn = () => { };
    
  • 安裝必要依賴:@babel/core、@babel/cli
    • @babel/core 核心庫,包含了所有的核心API
    • @babel/cli 提供的CLI命令行工具,主要是 babel 命令,適合安裝在項目中
    • @babel/node 提供了 babel-node 命令,更適合全局安裝
    npm i -D @babel/core @babel/cli
    
  • package.json 中配置 scripts,添加babel的編譯命令
    "scripts": {
        "compiler": "babel src --out-dir lib --watch"
    }
    
    執(zhí)行編譯:npm run compiler,生成lib/index.js。當前沒有配置任何插件,所以編譯前后的代碼完全一樣。

插件

雖然Babel是開箱即用的,但如果沒有為其添加任何插件,那么它什么也不會做。
Babel構(gòu)建在插件之上,使用現(xiàn)有的或自己編寫的插件可以組成一個轉(zhuǎn)換通道,Babel的插件分為兩種:語法插件、轉(zhuǎn)換插件

  • 語法插件
    只允許Babel解析(不是轉(zhuǎn)換)特定類型的語法,可以在 AST 轉(zhuǎn)換時使用,以支持解析新語法
    // for Example
    import * as babel from "@babel/core";
    const code = babel.transformFromAstSync(ast, {
        //支持可選鏈
        plugins: ["@babel/plugin-proposal-optional-chaining"],
        babelrc: false
    }).code;
    
  • 轉(zhuǎn)換插件
    會啟用相應的語法插件(因此不需要同時指定這兩種插件),先解析,后轉(zhuǎn)換!

在項目根目錄下創(chuàng)建配置文件.babelrc,安裝并配置插件

// 一個編譯箭頭函數(shù)的babel插件
npm i @babel/plugin-transform-arrow-functions -D

//.babelrc
{
    "plugins": ["@babel/plugin-transform-arrow-functions"]
}
# 也可以指定插件的絕對/相對路徑
"plugins": ["./node_modules/@babel/plugin-transform-arrow-functions"]

重新編譯的結(jié)果:

// lib/index.js
"use strict";

const fn = function () { };

OK! 現(xiàn)在可以轉(zhuǎn)換箭頭函數(shù)了,如果繼續(xù)轉(zhuǎn)化其他的JS新特性,還需要繼續(xù)添加插件,一個個配置的話必然很繁瑣!
為此,Babel提供了預設

預設

通過使用或創(chuàng)建一個 Preset 即可輕松使用一組插件。

// 官方 preset
@babel/preset-env  @babel/preset-flow  @babel/preset-react  @babel/preset-typescript

注:Babel v7 開始,所有針對標準提案階段的功能所編寫的預設(stage preset)都已被廢棄,官方移除了@babel/preset-stage-x

@babel/preset-env

官方:@babel/preset-env 是一個靈活的預設,你不需要管理目標環(huán)境需要的語法轉(zhuǎn)換或瀏覽器polyfills,就可以使用最新的JavaScript,同時也會讓JavaScript打包后的文件更小。
總之,主要作用是:轉(zhuǎn)換目標瀏覽器中缺失的JavaScript新語法,加載Polyfills

在不做任何配置的情況下,@babel/preset-env所包含的插件支持所有最新的JS特性(ES2015、ES2016等,不包含 stage 階段),并將其轉(zhuǎn)換為ES5代碼。
但如果代碼中包含了可選鏈(目前仍在stage階段),還需要安裝相應的插件。

安裝與配置.babelrc

npm i @babel/preset-env -D

// .babelrc
{
    "presets": ["@babel/preset-env"]
}

Browserslist 集成

@babel/preset-env會根據(jù)配置的目標環(huán)境,生成插件列表來編譯。對基于瀏覽器或Electron的項目,官方推薦使用 .browserslistrc 文件來指定目標環(huán)境。默認情況下,如果沒有在 .babelrc 中設置 targetsignoreBrowserslistConfig@babel/preset-env會使用 browserslist 配置源。

比如,僅包括瀏覽器市場份額超過0.25%的用戶所需的 polyfill 和代碼轉(zhuǎn)換(忽略沒有安全更新的瀏覽器,如IE10BlackBerry)
三種配置方式:

  • .babelrc:@babel/preset-envtargets可以設置支持哪些平臺、哪些版本等等目標信息
    {
        "presets": [
            ["@babel/preset-env", {
                "targets": {
                      "browsers": "> 0.25%, not dead",
                      "node": "xxx",
                      ...
                }
            }]
        ]
    }
    
  • 在項目跟目下創(chuàng)建browserslist配置文件 .browserslistrc
    > 0.25%
    not dead
    
  • package.json:官方建議這種方式
    "browserslist": [
        "> 0.25%",
        "not dead"
    ]
    

如果不是要兼容所有的瀏覽器和環(huán)境,推薦指定目標環(huán)境,這樣在編譯代碼能保持最小!

再比如,.browserslistrc配置為:

last 2 Chrome versions

編譯后發(fā)現(xiàn)代碼并沒有轉(zhuǎn)換,這是因為Chrome瀏覽器最新的兩個版本都支持箭頭函數(shù)。

更多 browserslist 配置:browserslist 配置

Polyfill

修改一下src/index.js

const isHas = [1,2,3].includes(2);

const p = new Promise((resolve, reject) => {
    resolve(100);
});

編譯后發(fā)現(xiàn)代碼并沒有轉(zhuǎn)換!--因為@babel/preset-env轉(zhuǎn)換的是語法,但新的內(nèi)置函數(shù)、方法無法轉(zhuǎn)換。
這時,就需要 polyfill 登場了!

polyfill 的譯文是墊片,顧名思義,就是墊平不同瀏覽器或不同環(huán)境下的差異,讓新的內(nèi)置函數(shù)、方法在低版本瀏覽器中也可以使用。

core-js/stablePolyfill ECMAScript特性)和 regenerator-runtime/runtime(需要使用轉(zhuǎn)換后的generator函數(shù))模塊,可以模擬完整的ES2015+環(huán)境(不包含第4階段前的提議)。

這就意味著可以使用:新的內(nèi)置函數(shù)如Promise、WeakMap,新的靜態(tài)方法如Array.from、Object.assign,新的實例方法如Array.prototype.includes,以及generator函數(shù)(前提是使用@babel/plugin-transform-regenerator插件)。
但為了添加這些功能,polyfill將添加到全局范圍和類似String這樣的內(nèi)置原型中(如你所想,會污染全局環(huán)境,后面會講避免全局污染的方法)。

注意:Babel v7.4.0之前,@babel/polyfill包含core-js(默認v2)regenerator-runtime兩個模塊;但從Babel v7.4.0開始,@babel/polyfill被廢棄了,支持直接安裝并導入core-js(默認v3)regenerator-runtime
core-js是能夠使用新API的最重要的包,與Babel高度集成,core-js@3廢棄了@babel/polyfill,實現(xiàn)了完全無污染的API轉(zhuǎn)譯。

// Babel v7.4.0 之前
npm i -S @babel/polyfill  // 不使用 -D 是因為這是一個需要在源碼之前運行的墊片

// Babel v7.4.0
npm i core-js regenerator-runtime -S
# core-js: ^3.6.4:提供 es 新的特性
# regenerator-runtime: ^0.14.4:代碼中用到generator、async函數(shù)的話,提供對 generator 支持

兩種使用方式:

  • src/index.js 的首部
    // Babel v7.4.0 之前
    require("@babel/polyfill");
    or import "@babel/polyfill";
    
    // Babel v7.4.0
    import "core-js/stable";
    import "regenerator-runtime/runtime";
    
  • webpack 里配置入口:
    entry: ["@babel/polyfill", "./src/index.js"],
    

現(xiàn)在的代碼不管在低版本還是高版本瀏覽器,甚至Node環(huán)境中都能正常運行了!

按需引入

然而,很多時候都不需要完整的引入Polyfill,這樣會增大構(gòu)建包的體積。
Babel@babel/preset-env中提供了參數(shù) useBuiltIns,設置為 usage 時就只會包含代碼中需要的polyfill。另外還必須同時配置corejs@babel/polyfill默認安裝core-js v2,而core-js v2分支中已經(jīng)不再添加新特性,為了可以使用更多新特性,請安裝core-js v3

useBuiltIns的可選值

  • false 不對 Polyfill 做操作,引入所有的Polyfill
  • usage 根據(jù)配置的瀏覽器兼容性,以及代碼中使用到的API來進行Polyfill ,實現(xiàn)按需加載;
  • entry 根據(jù)browserslist的配置,引入目標環(huán)境不兼容的polyfill,還需要在入口文件中手動添加import "@babel/polyfill"

還有一個常用的參數(shù) modules,可以取值 amd, umd, systemjs, commonjs, false,這可以讓Babel以特定的模塊化格式來輸出代碼。false 表示不進行模塊化處理。

// Babel v7.4.0 之前還需要額外安裝core-js v3
npm install -S core-js@3

// .babelrc
{
    "presets": [
        ["@babel/preset-env", {
            "useBuiltIns": "usage",
            "corejs": 3  // 默認使用的時corejs@2,因此必須設置corejs@3
        }]
    ]
}

編譯時,Babel會檢查所有代碼,查找在目標環(huán)境中缺失的功能,然后僅僅把需要的polyfill包含進來。

// lib/index
"use strict";

require("core-js/modules/es.array.includes");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");

var isHas = [1, 2, 3].includes(2);
var p = new Promise(function (resolve, reject) {
  resolve(100);
});

此時用webpackproduction模式構(gòu)建,包體積要比引入整個@babel/polyfill小很多了。

提取輔助函數(shù)

Polyfill雖然解決了Babel不轉(zhuǎn)換非語法的新API問題,但會使用很小的輔助函數(shù)來實現(xiàn)類似_classCallCheck、_createClass等公共方法。默認情況下,這些輔助函數(shù)將被inject(添加)到需要它的每個文件中。

修改src/index.js

class Point { }

編譯之后:

// lib/index
"use strict";

function _classCallCheck ...
var Point = function Point() ...

試想一下,100個文件都是用了class,那就意味著諸如_classCallCheck之類的輔助函數(shù)被inject了100次,導致編譯后的bundle(包)體積變大!
為此,Babel提供了單獨的模塊@babel/runtime,用于提供編譯模塊的輔助函數(shù)。啟用@babel/plugin-transform-runtime插件后,Babel就會使用@babel/runtime中的工具函數(shù),以 閉包 的形式注入。

npm i -D @babel/plugin-transform-runtime #僅編譯時使用
npm i -S @babel/runtime #編譯和運動時都需要

.babelrc 中配置 @babel/plugin-transform-runtime 插件

{
//    "presets": [
//        ["@babel/preset-env", {
//            "useBuiltIns": "usage",
//            "corejs": 3
//        }]
//    ],
    "plugins": [
        ["@babel/plugin-transform-runtime", { corejs: 3 }]
    ]
}

編譯之后:

// lib/index
"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var Point = function Point() {
  (0, _classCallCheck2.default)(this, Point);
};

可以看出,幫助函數(shù)已經(jīng)不是直接被 inject 到代碼中,而是從@babel/runtime中引入的。

全局污染問題

@babel/plugin-transform-runtime構(gòu)建過程的代碼轉(zhuǎn)換,是一個可以重復使用Babel注入的幫助程序,以節(jié)省代碼大小的插件。
除此之外,該轉(zhuǎn)換器還有另外一個作用:為我們的代碼創(chuàng)建一個沙盒環(huán)境

如果直接使用core-js@babel/polyfill,它們所提供的諸如內(nèi)置程序Promise、Set、Map等,將會污染全局環(huán)境。雖然這對于應用程序或命令行工具可能是可以的,但如果代碼是要發(fā)布供他人使用的庫,或者無法完全控制代碼運行環(huán)境,那么這種污染將成為一個問題。
@babel/plugin-transform-runtime 會將這些內(nèi)置別名作為 core-js 的別名,因此可以無縫使用它們,無需使用polyfill(官網(wǎng))

  • 安裝依賴:npm i @babel/runtime-corejs3 -S
  • 配置 .babelrc
    {
        "presets": ["@babel/preset-env"],
        "plugins": [
            ["@babel/plugin-transform-runtime", { "corejs": 3 }]
        ]
    }
    
  • 修改 src/index.js,加入 include()、Promise
    let isHas = [1,2,3].includes(2);
    Array.from([1, 2, 3], x => x + x); // [2, 4, 6]
    let promsie = new Promise();
    
    async function fn() {
        return 1
    }
    

重新編譯可知,@babel/plugin-transform-runtime通過導入模塊的方式引入所需功能,直接去修改Array.prototype 或 新增Promise函數(shù),避免了全局環(huán)境的污染。

搭配webpack時,如果使用commonJs編寫模塊,且使用了@babel/plugin-transform-runtime,則應該安裝@babel/plugin-transform-modules-commonjswebpack需要利用它處理ES6Module

更多插件/預設的補充知識

  1. 插件的排列順序很重要!!!
    如果兩個轉(zhuǎn)換插件都將處理程序的某個代碼片段,則將根據(jù)轉(zhuǎn)換插件或 preset 的排列順序依次執(zhí)行。
    • PluginPresets 前運行
    • 插件順序從前往后排列
    • Presets順序是從后往前的
      { "presets": ["@babel/preset-env", "@babel/preset-react"] }
      
      先執(zhí)行@babel/preset-react, 后執(zhí)行@babel/preset-env
  2. 插件參數(shù)
    插件和preset都可以接受參數(shù),參數(shù)由插件名和參數(shù)對象組成一個數(shù)組
        "plugins": [
            ["@babel/plugin-proposal-class-properties", { "loose": true }]
        ]
    
  3. 插件的短名稱
    • 如果插件名稱為@babel/plugin-XXX,可以使用短名稱@babel/XXX
          "plugins": [
              "@babel/transform-arrow-functions" // @babel/plugin-transform-arrow-functions
          ]
      
    • 如果插件名稱為babel-plugin-XXX,可以使用短名稱XXX,該規(guī)則同樣適用于帶有scope的插件
          "plugins": [
              "newPlugin", // babel-plugin-newPlugin
              "@scp/myPlugin" // @scp/babel-plugin-myPlugin
          ]
      
  4. 創(chuàng)建自己的Preset
    • 可以簡單的返回一個插件數(shù)組
      module.exports = function() {
          return {
              plugins: ["A", "B", "C"]
          }
      }
      
    • preset中也可以包含其他的preset,以及帶有參數(shù)的插件
      module.exports = function() {
          return {
              presets: [require("@babel/preset-env")],
              plugins: [
                  [require("@babel/plugin-proposal-class-properties"), { loose: true }],
                  require("@babel/plugin-proposal-object-rest-spread")
              ]
          }
      }
      

配置文件

Babel支持多種格式的配置文件,根據(jù)使用場景可以選擇不同的配置文件。
所有的Babel API參數(shù)都可以配置,但如果該參數(shù)需要使用JS代碼,那可能需要使用JS代碼版的配置文件。

  • 如果希望以編程的方式創(chuàng)建配置文件或編譯node_modules目錄下的模塊,那么 babel.config.js 可以滿足需求;
  • 如果只需要一個簡單的且只用于單個軟件包的配置,那可以選擇.babelrc
  1. babel.config.js
    在項目根目錄下創(chuàng)建babel.config.js文件,配置文檔:babel.config.js
    module.exports = function(api) {
        api.cache(true);
        const presets = [...];
        const plugins = [...];
        return {
            presets,
            plugins
        };
    }
    
  2. .babelrc
    在項目根目錄下創(chuàng)建.babelrc文件,配置文檔:.babelrc
    {
        "presets": [],
        "plugins": []
    }
    
  3. package.json
    可以將 .babelrc 中的配置信息作為 babel 鍵(key) 添加到 package.json 文件中:
    {
        "name": "my-package",
        "babel": {
            "presets": [],
            "plugins": []
        }
    }
    
  4. .babelrc.js
    .babelrc 配置相同,但是支持使用JS編寫。
    //可以在其中調(diào)用 Node.js 的API
    const presets = [];
    const plugins = [];
    module.exports = { presets, plugins };
    

使用Webpack構(gòu)建

  • 安裝依賴:
    npm install -D webpack-cli webpack babel-loader clean-webpack-plugin
    
  • 在根目錄下創(chuàng)建配置文件webpack.config.js
    const path = require('path')
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    
    module.exports = {
        mode: 'production',
        entry: './src/index.js',
        output: {
            path: path.resolve(__dirname, 'dist'),
            filename: '[name].[hash].js'
        },
        module: {
            rules: [
                {
                    test: /\.js$/,
                    loader: 'babel-loader',
                    exclude: /node_modules/ //排除 node_modules 目錄
                }
            ]
        },
        plugins: [
            new CleanWebpackPlugin() //清理 output 指定的目錄
        ]
    }
    
  • package.json
    "scripts": {
        "build": "webpack
    }
    
  • 構(gòu)建:npm run build
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374