初次相識(shí)Babel v7,多多指教

Babel 是一個(gè) JavaScript 編譯器。

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

Babel的功能:

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

Babel` 編譯過程:

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

常見使用方式:

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

初始化項(xiàng)目

  • 創(chuàng)建一個(gè)空目錄,執(zhí)行 npm init -y
  • 新建src/index.js
    const fn = () => { };
    
  • 安裝必要依賴:@babel/core、@babel/cli
    • @babel/core 核心庫,包含了所有的核心API
    • @babel/cli 提供的CLI命令行工具,主要是 babel 命令,適合安裝在項(xiàng)目中
    • @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。當(dāng)前沒有配置任何插件,所以編譯前后的代碼完全一樣。

插件

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

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

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

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

//.babelrc
{
    "plugins": ["@babel/plugin-transform-arrow-functions"]
}
# 也可以指定插件的絕對(duì)/相對(duì)路徑
"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ù)添加插件,一個(gè)個(gè)配置的話必然很繁瑣!
為此,Babel提供了預(yù)設(shè)。

預(yù)設(shè)

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

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

注:Babel v7 開始,所有針對(duì)標(biāo)準(zhǔn)提案階段的功能所編寫的預(yù)設(shè)(stage preset)都已被廢棄,官方移除了@babel/preset-stage-x

@babel/preset-env

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

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

安裝與配置.babelrc

npm i @babel/preset-env -D

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

Browserslist 集成

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

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

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

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

再比如,.browserslistrc配置為:

last 2 Chrome versions

編譯后發(fā)現(xiàn)代碼并沒有轉(zhuǎn)換,這是因?yàn)?code>Chrome瀏覽器最新的兩個(gè)版本都支持箭頭函數(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)換!--因?yàn)?code>@babel/preset-env轉(zhuǎn)換的是語法,但新的內(nèi)置函數(shù)、方法無法轉(zhuǎn)換。
這時(shí),就需要 polyfill 登場(chǎng)了!

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,新的實(shí)例方法如Array.prototype.includes,以及generator函數(shù)(前提是使用@babel/plugin-transform-regenerator插件)。
但為了添加這些功能,polyfill將添加到全局范圍和類似String這樣的內(nèi)置原型中(如你所想,會(huì)污染全局環(huán)境,后面會(huì)講避免全局污染的方法)。

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

// Babel v7.4.0 之前
npm i -S @babel/polyfill  // 不使用 -D 是因?yàn)檫@是一個(gè)需要在源碼之前運(yùn)行的墊片

// 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ù)的話,提供對(duì) 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)境中都能正常運(yùn)行了!

按需引入

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

useBuiltIns的可選值

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

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

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

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

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

// 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);
});

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

提取輔助函數(shù)

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

修改src/index.js

class Point { }

編譯之后:

// lib/index
"use strict";

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

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

npm i -D @babel/plugin-transform-runtime #僅編譯時(shí)使用
npm i -S @babel/runtime #編譯和運(yùn)動(dòng)時(shí)都需要

.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)換,是一個(gè)可以重復(fù)使用Babel注入的幫助程序,以節(jié)省代碼大小的插件。
除此之外,該轉(zhuǎn)換器還有另外一個(gè)作用:為我們的代碼創(chuàng)建一個(gè)沙盒環(huán)境。

如果直接使用core-js@babel/polyfill,它們所提供的諸如內(nèi)置程序Promise、Set、Map等,將會(huì)污染全局環(huán)境。雖然這對(duì)于應(yīng)用程序或命令行工具可能是可以的,但如果代碼是要發(fā)布供他人使用的庫,或者無法完全控制代碼運(yùn)行環(huán)境,那么這種污染將成為一個(gè)問題。
@babel/plugin-transform-runtime 會(huì)將這些內(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通過導(dǎo)入模塊的方式引入所需功能,直接去修改Array.prototype 或 新增Promise函數(shù),避免了全局環(huán)境的污染。

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

更多插件/預(yù)設(shè)的補(bǔ)充知識(shí)

  1. 插件的排列順序很重要!??!
    如果兩個(gè)轉(zhuǎn)換插件都將處理程序的某個(gè)代碼片段,則將根據(jù)轉(zhuǎn)換插件或 preset 的排列順序依次執(zhí)行。
    • PluginPresets 前運(yùn)行
    • 插件順序從前往后排列
    • Presets順序是從后往前的
      { "presets": ["@babel/preset-env", "@babel/preset-react"] }
      
      先執(zhí)行@babel/preset-react, 后執(zhí)行@babel/preset-env
  2. 插件參數(shù)
    插件和preset都可以接受參數(shù),參數(shù)由插件名和參數(shù)對(duì)象組成一個(gè)數(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
    • 可以簡(jiǎn)單的返回一個(gè)插件數(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ù)使用場(chǎng)景可以選擇不同的配置文件。
所有的Babel API參數(shù)都可以配置,但如果該參數(shù)需要使用JS代碼,那可能需要使用JS代碼版的配置文件。

  • 如果希望以編程的方式創(chuàng)建配置文件或編譯node_modules目錄下的模塊,那么 babel.config.js 可以滿足需求;
  • 如果只需要一個(gè)簡(jiǎn)單的且只用于單個(gè)軟件包的配置,那可以選擇.babelrc
  1. babel.config.js
    在項(xiàng)目根目錄下創(chuàng)建babel.config.js文件,配置文檔:babel.config.js
    module.exports = function(api) {
        api.cache(true);
        const presets = [...];
        const plugins = [...];
        return {
            presets,
            plugins
        };
    }
    
  2. .babelrc
    在項(xiàng)目根目錄下創(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)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容